茶器艺科智造HarmonyOS应用实战-58-五类智能体错误挤进一个字符串,取消操作为何还会留下旧提示:给AgentFrameworkKit回调建立可恢复状态
茶器艺科智造HarmonyOS应用实战-58-五类智能体错误挤进一个字符串,取消操作为何还会留下旧提示:给AgentFrameworkKit回调建立可恢复状态
两处入口分别用 switch 把 1022400010—1022400014 映射成字符串。表面上只是几段提示文案,实际上取消、隐私、账号、网络、服务和未知错误需要的后续动作并不相同;如果组件只保存上一次 errorText,一次无提示的取消还可能把旧错误留在界面上。

本文以 Gitee 仓库 aiding521/chaqi-app-gitee 的 master 分支、提交 f671fcd8de277973bd7518a3c462f68384575f1e 的静态源码为事实基线。本文没有修改项目源码,也没有执行 HAP 构建、模拟器或真机验证;建议代码属于演进方案,不能描述成已经落地的功能。
一、源码里的 switch 同时承担了三件事
静态阅读可见,两处入口都把 1022400010—1022400014 映射成字符串。当前代码可以概括为:
if (err.code === 1022400010) return '';
if (err.code === 1022400013) return '请检查网络';
这两行已经能说明边界:同一个返回类型既表达“取消后不显示文案”,又表达“发生了可向用户说明的问题”。当 switch 只返回字符串时,调用者拿不到错误类别,不知道能否重试,也不知道应该清空旧状态、跳转设置还是保留输入。
本文不根据片段猜测每一个数值码的完整平台语义。能确认的是已有范围与两个示例分支,以及两处入口存在重复映射。其余分类应在接入时逐项对照当前 SDK 和项目源码,不能把文章中的概念分类直接当成官方错误码表。
二、空字符串不是一个完整的“取消状态”
假设前一次网络失败令 errorText 变成“请检查网络”,下一次用户取消后映射函数返回空串。只有调用者明确执行 this.errorText = '',旧提示才会消失;若代码把空串理解为“没有新消息,所以不更新”,页面就会继续显示上一笔错误。

因此回调至少要区分两层信息:发生了什么,以及界面下一步怎么做。取消是一种终态,不等于“什么都没发生”;网络问题可能允许重试,账号或隐私问题可能需要引导,服务错误与未知错误则需要保留诊断信息。把它们全压成一个字符串,状态转换只能散落在各入口的 if 中。
三、先把错误翻译成类型化视图
共享映射函数可以返回稳定的分类、用户文案和重试能力:
export interface AgentFailureView {
kind: 'cancelled' | 'privacy' | 'account' | 'network' | 'service' | 'unknown';
message: string;
retryable: boolean;
}
这里的 AgentFailureView 是建议模型,并不声称仓库已存在。它的意义是让 switch 只负责一次“错误码到业务视图”的翻译,两个组件不再各维护一份数字分支。kind 用于状态判断,message 用于展示,retryable 用于决定是否开放重试;三者不能再由“字符串是否为空”间接推断。
映射函数还应为无法识别的码返回 unknown,而不是抛弃结果。未知分支不是为了掩盖问题,而是让界面有稳定退路、日志保留原始码,同时避免新增平台错误码后两处入口表现不一致。
四、回调接入的关键是覆盖旧状态
组件收到本次失败结果后,应无条件用新视图更新相关字段:
const view = mapAgentFailure(err.code ?? -1);
this.errorText = view.message;
this.canRetry = view.retryable;
对于 cancelled,message 可以为空、retryable 可以按已核对的产品规则给值,但组件仍要完成这次赋值。这样旧网络提示才会被空文案覆盖。对于下一次成功,也应有对称的清理动作;否则“失败后成功”仍可能把失败文案留在新结果旁边。
还要注意并发顺序。如果请求 A 失败后请求 B 又开始,A 的迟到回调不能覆盖 B 当前的界面状态。错误类型化解决的是“怎么表达”,任务身份或代次解决的是“这次结果还算不算数”,两者缺一不可。
五、按错误类别定义界面转换,而不是只列文案
| 分类 | 本次回调到达后 | 旧错误如何处理 | 可恢复入口 |
|---|---|---|---|
| cancelled | 进入明确取消终态 | 必须清空 | 回到可操作状态 |
| privacy | 保存分类并展示对应说明 | 覆盖 | 按产品规则提供引导 |
| account | 保存分类并展示对应说明 | 覆盖 | 按产品规则提供引导 |
| network | 展示网络文案 | 覆盖 | 仅在规则允许时重试 |
| service | 展示服务类文案 | 覆盖 | 保留输入,等待重试或退出 |
| unknown | 展示兜底文案并记录原始码 | 覆盖 | 避免卡在加载态 |
| success | 展示本次结果 | 清空 | 继续正常流程 |
表中故意不填写未从源码核对出的具体跳转地址或具体错误码映射。设计重点是每次回调都产生完整的新状态,而不是在旧字段上做零散补丁。尤其要检查加载标记:失败、取消和成功都必须结束当前等待,不能只有某几个 switch 分支负责收尾。
六、纯映射测试要覆盖码值,也要覆盖序列
已有的边界用例骨架可以承载映射测试:
interface BoundaryCase<T> {
name: string;
input: T;
expected: string;
}
const cases: BoundaryCase<string>[] = [
{ name: 'empty', input: '', expected: 'reject' },
{ name: 'normal', input: 'valid', expected: 'accept' },
{ name: 'repeat', input: 'valid', expected: 'idempotent' }
];
对本篇而言,单值用例至少应覆盖已知范围、缺失 code 与范围外 code,断言 kind/message/retryable 的组合,而不只比较最终字符串。随后再补序列用例:“网络失败→取消”必须清空旧提示,“服务失败→成功”必须移除失败状态,“A 失败→B 开始→A 迟到”不能污染 B。
纯函数测试能够验证映射一致性,却不能替代 AgentFrameworkKit 在真实账号、网络与设备环境中的回调证据。平台实际返回哪些码、取消时回调顺序如何,仍需在目标环境记录。
七、页面离开时要让迟到回调失效
组件创建的 Controller、监听或其他运行时资源,仍应由该组件对称释放:
private disposeOwnedResources(): void {
// timer/listener只由创建它的组件清理
// 异步结果写状态前核对taskId或pageGeneration
// 可恢复任务先保存业务事实,再释放进程句柄
}
如果用户发起请求后立刻离开,回调稍后才到达,单靠错误映射无法阻止它写回已经失效的页面。调用前记录 taskId 或 pageGeneration,回调落地前核对仍为当前值;页面离开时增加代次并释放自己创建的资源。这样“取消旧请求”和“销毁页面”才不会被误当成同一种空字符串。
是否需要持久化错误状态取决于产品规则,本文没有足够事实替项目做决定。通常至少要区分可恢复的业务输入和只在本次页面存在的展示提示,不能把 Controller 等进程句柄当成可恢复数据保存。
八、验证要从六条状态序列取证
- 无旧状态时触发取消,确认不出现错误提示且加载结束。
- 先制造网络失败,再触发取消,确认旧网络提示被清空。
- 先制造失败,再取得成功,确认错误分类、文案与重试按钮都复位。
- 连续发起 A、B,让 A 迟到,确认 A 不能覆盖 B。
- 输入无法识别的 code,确认进入 unknown 而不是卡住或静默。
- 从两个入口触发相同错误,确认映射结果一致。
- 快速离开页面,确认迟到回调不再写失效状态。
每条记录都应包含入口、原始 code、映射 kind、任务身份和最终界面状态;涉及账号与隐私时不记录令牌或完整用户输入。
九、常见误修为什么会制造新歧义
| 误修 | 残留问题 | 更合适的方向 |
|---|---|---|
| 取消时直接 return | 旧 errorText 没有被覆盖 | 写入明确 cancelled 结果并清理 |
| 两处 switch 同时手改 | 下次新增码仍可能漏一处 | 共享纯映射函数 |
| 只返回 message | 无法稳定决定重试和引导 | 返回 kind、message、retryable |
| 未知码返回空串 | 把新错误伪装成取消 | 独立 unknown 分类并记录原始码 |
| 所有失败都显示重试 | 账号、隐私等动作可能不同 | 由已核对规则决定 retryable |
| catch 后只打日志 | 页面可能永远停在加载态 | 进入明确终态并给恢复入口 |
文案润色不是本篇的主要修复。只要底层仍靠字符串反推状态,再好的提示也无法保证取消、重试和迟到回调按正确顺序工作。
十、共享映射与页面状态各有落点

项目中 entry 负责产品入口,libraryhsp 负责页面和交互,libraryhar 负责模型、算法、路由与通用服务。无 UI 依赖的错误分类、返回类型和映射函数适合放到两个入口都能引用的共享层;Toast、内联提示、重试按钮与 Controller 生命周期继续留在相应页面或组件。
entry / libraryhsp -> 页面、组件、生命周期
libraryhar -> 纯类型、纯函数、可持久化模型
tests -> 边界、幂等、恢复序列
runtime evidence -> 日志、设备行为、服务读回
共享的应是“同一个 code 得到同一种业务分类”,而不是强迫两个入口采用完全相同的布局。页面可以用不同控件呈现,但不能一个把某码当网络问题、另一个把它当未知错误。
十一、证据记录必须保留未验证项
baseline: f671fcd8de277973bd7518a3c462f68384575f1e
scope: source-level proposal
build: not run
simulator/device: not run
network/account: not run
result: static boundary identified; implementation pending
基线只证明源码存在重复 switch、给定码范围及示例分支,并不能证明设备上每类回调一定按设想到达。SDK 文档、真实账号授权、断网与服务异常都需要另行验证。源码或 SDK 版本改变后,应重新核对错误码,不沿用本文快照。
十二、用回调时序复现“旧提示留下来”
最有价值的回归不是逐个点击 Toast,而是把前后两次操作连起来。先让任务 A 进入可见错误,再发起任务 B 并取消;B 的 cancelled 结果到达后,A 的文案应消失。随后再验证 A 的迟到回调不会反向覆盖 B 的当前状态。
| 场景 | 回调序列 | 必须观察的证据 | 不能替代的证据 |
|---|---|---|---|
| 首次取消 | start→cancelled | 加载结束、错误为空 | 没有 Toast |
| 错误后取消 | network→start→cancelled | 旧错误被覆盖 | 新回调返回空串 |
| 错误后成功 | service→start→success | 错误、重试状态复位 | 结果区出现内容 |
| 乱序 | start A→start B→failure A | A 被身份检查拒绝 | 最终画面碰巧正确 |
| 离页 | start→离开→failure | 不写失效页面 | 页面没有崩溃 |
| 未知码 | start→unmapped code | unknown 与原始码日志 | catch 后静默 |
可以用操作轨迹记录回调属于哪一笔请求:
interface OperationTrace {
taskId: string;
generation: number;
startedAtMs: number;
finishedAtMs?: number;
terminalSource?: 'ui' | 'service' | 'restore' | 'cancel';
}
function canApplyResult(trace: OperationTrace, currentGeneration: number): boolean {
return trace.generation === currentGeneration &&
trace.finishedAtMs === undefined;
}
轨迹解决的是回调归属,AgentFailureView 解决的是失败含义。前者不能替代后者,后者也不能阻止旧请求迟到。两层都明确后,取消才会成为可测试、可恢复、不会遗留旧提示的真实状态。
本文没有实际改动仓库,也未执行构建、设备、网络和账号验证。现阶段结论仍是源码级方案;尤其是 1022400010—1022400014 的最终分类,应以目标 SDK 与运行证据复核。
更多推荐



所有评论(0)