茶器艺科智造HarmonyOS应用实战-58-五类智能体错误挤进一个字符串,取消操作为何还会留下旧提示:给AgentFrameworkKit回调建立可恢复状态

两处入口分别用 switch 把 1022400010—1022400014 映射成字符串。表面上只是几段提示文案,实际上取消、隐私、账号、网络、服务和未知错误需要的后续动作并不相同;如果组件只保存上一次 errorText,一次无提示的取消还可能把旧错误留在界面上。

第58篇封面

本文以 Gitee 仓库 aiding521/chaqi-app-giteemaster 分支、提交 f671fcd8de277973bd7518a3c462f68384575f1e 的静态源码为事实基线。本文没有修改项目源码,也没有执行 HAP 构建、模拟器或真机验证;建议代码属于演进方案,不能描述成已经落地的功能。

一、源码里的 switch 同时承担了三件事

静态阅读可见,两处入口都把 1022400010—1022400014 映射成字符串。当前代码可以概括为:

if (err.code === 1022400010) return '';
if (err.code === 1022400013) return '请检查网络';

这两行已经能说明边界:同一个返回类型既表达“取消后不显示文案”,又表达“发生了可向用户说明的问题”。当 switch 只返回字符串时,调用者拿不到错误类别,不知道能否重试,也不知道应该清空旧状态、跳转设置还是保留输入。

本文不根据片段猜测每一个数值码的完整平台语义。能确认的是已有范围与两个示例分支,以及两处入口存在重复映射。其余分类应在接入时逐项对照当前 SDK 和项目源码,不能把文章中的概念分类直接当成官方错误码表。

二、空字符串不是一个完整的“取消状态”

假设前一次网络失败令 errorText 变成“请检查网络”,下一次用户取消后映射函数返回空串。只有调用者明确执行 this.errorText = '',旧提示才会消失;若代码把空串理解为“没有新消息,所以不更新”,页面就会继续显示上一笔错误。

第58篇流程图

因此回调至少要区分两层信息:发生了什么,以及界面下一步怎么做。取消是一种终态,不等于“什么都没发生”;网络问题可能允许重试,账号或隐私问题可能需要引导,服务错误与未知错误则需要保留诊断信息。把它们全压成一个字符串,状态转换只能散落在各入口的 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;

对于 cancelledmessage 可以为空、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
  // 可恢复任务先保存业务事实,再释放进程句柄
}

如果用户发起请求后立刻离开,回调稍后才到达,单靠错误映射无法阻止它写回已经失效的页面。调用前记录 taskIdpageGeneration,回调落地前核对仍为当前值;页面离开时增加代次并释放自己创建的资源。这样“取消旧请求”和“销毁页面”才不会被误当成同一种空字符串。

是否需要持久化错误状态取决于产品规则,本文没有足够事实替项目做决定。通常至少要区分可恢复的业务输入和只在本次页面存在的展示提示,不能把 Controller 等进程句柄当成可恢复数据保存。

八、验证要从六条状态序列取证

  1. 无旧状态时触发取消,确认不出现错误提示且加载结束。
  2. 先制造网络失败,再触发取消,确认旧网络提示被清空。
  3. 先制造失败,再取得成功,确认错误分类、文案与重试按钮都复位。
  4. 连续发起 A、B,让 A 迟到,确认 A 不能覆盖 B。
  5. 输入无法识别的 code,确认进入 unknown 而不是卡住或静默。
  6. 从两个入口触发相同错误,确认映射结果一致。
  7. 快速离开页面,确认迟到回调不再写失效状态。

每条记录都应包含入口、原始 code、映射 kind、任务身份和最终界面状态;涉及账号与隐私时不记录令牌或完整用户输入。

九、常见误修为什么会制造新歧义

误修残留问题更合适的方向
取消时直接 return旧 errorText 没有被覆盖写入明确 cancelled 结果并清理
两处 switch 同时手改下次新增码仍可能漏一处共享纯映射函数
只返回 message无法稳定决定重试和引导返回 kind、message、retryable
未知码返回空串把新错误伪装成取消独立 unknown 分类并记录原始码
所有失败都显示重试账号、隐私等动作可能不同由已核对规则决定 retryable
catch 后只打日志页面可能永远停在加载态进入明确终态并给恢复入口

文案润色不是本篇的主要修复。只要底层仍靠字符串反推状态,再好的提示也无法保证取消、重试和迟到回调按正确顺序工作。

十、共享映射与页面状态各有落点

第58篇结构图

项目中 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 AA 被身份检查拒绝最终画面碰巧正确
离页start→离开→failure不写失效页面页面没有崩溃
未知码start→unmapped codeunknown 与原始码日志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 与运行证据复核。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐