Repository navigation
Conversation
…程 include `packages/client/features/dsh-client-html-resources` 是双面包(package.json 里有 `dsh.client`)。 `dsh-typert-generator` 对双面包只在 `tsconfig.json` 这一个入口建 registration,而它的 `tsconfig.json` extends `tsconfig.host.json`——include 里没有 `src/client/types.ts`。 于是 `./client` 导出指向的源文件不在 program 里,生成插件参考时抛 `TypertAnalysisError`。 干净树上 `pnpm check:plugin-docs` 就是红的(实测,a5bb881)。 把这个文件加进 include 即可。同一个包的两面各有一份 tsconfig 是仓库既有写法; `tsconfig.client.json` 本来就只 include 它。 复核: - 修前 `pnpm check:plugin-docs` → TypertAnalysisError - 修后 → `Plugin developer API documentation is current: 16 bundles, 107 interfaces, 135 Remote methods.` - `pnpm typecheck`、`pnpm check:plugin-types`、`pnpm check:architecture` 仍绿
在 0.2.13 的布局上建起错误契约的纯逻辑层,不含调用点、不含闸、不含呈现。 新增 `packages/shared/src/contracts/errors/`: - `codes.ts`:失败码表扩到 **29 条**。原来 13 条(ADR 0002 首批)只盖得住约六成真实失败; 2026-10-17 在 0.2.13 上清点出 **903 处中文抛出 / 97 文件**,按语义并成 21 个桶后再落到码上, 新增 `data/shape-invalid`、`data/legacy-mismatch`、`input/unsupported`、`lookup/not-found`、 `lookup/duplicate`、`state/mismatch`、`state/precondition`、`security/denied`、 `lifecycle/not-ready`、`lifecycle/disposed`、`plugin/load-failed`、`net/operation-failed`、 `db/schema-mismatch`、`model/config-invalid`、`media/invalid`、`ipc/call-failed`。 三个桶并进已有码(`state/version-conflict`→`conflict/stale-revision`、`fs/operation-failed`→`io/failed`、 `data/corrupt`→`storage/corrupt`)。码表仍是内部枚举,不冻结。 - `details.ts`:每条码的参数化详情形状 + `EleckoiFailureDetailParams`(`Record<码, 参数名[]>` 类型上强制一一对应,漏一条码编译不过)+ 参数名闭集扩到八个(新增 `expected` / `actual`)。 `what` 的约定写进文件头:**名词短语**,会被用来拼用户句子;id 只进开发者诊断。 - `boundaries.ts`:六条边界闭集,① 的折法表跟着新码补齐;适配器落点改成 0.2.13 的真实路径。 - `ipc.ts`:边界⑤ 的线上失败形状;复用 `isDSHRemoteError` 标记的代价写进文件头。 新增 `packages/shared/src/foundation/`: - `failure.ts`:`foldFailure`——六条边界共用的唯一折叠规则。按形状识别,不用 `instanceof` (跨 bundle 副本原型不同);上游 6 个 legacy 码走防御映射,没有唯一对应的三个(`NOT_FOUND` / `CONFLICT` / `RUNTIME_UNAVAILABLE`)码走兜底、原码进 `details.what`,不猜;网关的合同违例载体码 (`gateway/bad-request` 等)折成 `contract/invalid-*` 并把 `issues` 投影出来。 与参考实现相比修了一处自身的洞:`productFailureOf` 原来用 `failureMessageOf`(`instanceof Error`) 取 message,对"形状对但不是 Error 实例"的对象会得到 `[object Object]`;现在先读自有属性上的 string。 - `ipcFailure.ts`:边界⑤ 的折叠与线上判定。只认标记不校验码——码不认识也要当失败,不能变静默成功。 改 `DesktopError.ts`:码的类型放宽成 `EleckoiFailureCode | DesktopErrorCode`,仍是一个类、一个 `name`。理由写进文件头:多一个类就得多认一套标记 = 第二套失败方言。同时把上游实测分布记进注释 (8 个文件 35 处,第三参 `details` 一次没写过)。 用例(`packages/shared/tests/`,47 条):码表形态与唯一性、六条边界闭集、详情表一一对应、 折叠规则(已带码保真 / legacy 映射 / 未知码兜底 / 网关载体码折 issues / 裸 Error / 详情形状防护 / JSON 往返)、边界⑤ 的线上形状与诊断指针。 复核: - `pnpm test:unit` → 5 files / 47 tests 通过 - `pnpm typecheck`、`pnpm check:architecture` 通过 下一步(未做):六条边界的适配器接线、渲染层唯一文案入口与两个通道、闸(规则 1/2/3 绝对值 + 「不许裸抛」规则)、903 处抛出改成带码。
## 边界②(宿主 Remote 出口):让产品失败自己就是网关载体
网关的折叠函数把**非**载体的一切抛出换成 `gateway/internal` 并把 `details` 变 `{}`
(`@deepseek-ai/dsh-api-gateway/lib/index.js:1425-1446`),而载体判据只是结构标记
(`isDSHRemoteError === true && typeof code === 'string'`)。参考实现在出口包一层 `RemoteError`
再抛,但它要求 `dsh-product-api`(tsc 直出、不打包)在**运行时** import 契约包的 `foldFailure`,
而契约包的 exports 指向 TS 源码(`packages/shared/package.json`),运行时解析不了。
所以这里改成更小的一步:`DesktopError` 自带 `isDSHRemoteError = true`。
码与详情**原样**过网关,不需要任何翻译包装,也不需要那条运行期依赖。
代价(与 IPC 边界复用同一标记是同一个决定)写进了 `DesktopError.ts` 与 `boundaries.ts` 的 ② 行。
同时纠正了参考实现自身的一个洞:`productFailureOf` 原来用 `instanceof Error` 取 message,
形状对但不是 Error 实例的对象会得到 `[object Object]`;改成先读自有属性上的 string。
## 边界⑥(`dsh-client-*` → 宿主):信封拆解只有一份实现
新增 `packages/host/product-api/src/client/invalidResponse.ts`(`invalidResponseFailure`,
带 `eleckoi/contract/invalid-response`)与 `unwrapFailure.ts`(`unwrapOrFail`),
并从 client 入口导出:14 个 bundle 里 `dsh.client.inject` 本来就声明了 `@eleckoi/dsh-product-api`
(10 个包的 package.json),运行时用 `require('@eleckoi/dsh-product-api')` 取。
- 27 处 `if (!result?.ok) throw new Error(result?.error?.message || '…')` 全部改成
`unwrapOrFail(result, '…')`:失败分支**抛宿主给的失败对象本身**,码与详情不再被压成一句话。
- 3 个 bundle 自带的 `unwrap(result, fallback)` 方法改成委托。
- 顺带修了 `product-api/src/client/index.ts` 自己的丢码点(`throw status.error`)——
那是「禁止丢码」规则盯的同一形状。
- client 面不 import 契约包(`rootDir: src` 的增量工程 + bundle 边界),所以码字符串有一份本地副本,
由 `packages/host/product-api/tests/error-contract-client.test.ts` 断言逐字等于码表。
- 9 个 bundle 里 `error instanceof Error ? error.message : String(error)` 改成按形状取
`message`:跨 realm(测试的 VM、将来的 iframe)副本 `instanceof` 为假,会把
`RemoteError: 消息` 甚至 `[object Object]` 当文案。
## 测试装载器与探针
- `tests/helpers/dshClientPlugin.ts` 的 `require` 桩认 `@eleckoi/dsh-product-api`,
并把同一个桩导出成 `dshClientRequire` 给 4 个直接调 `factory()` 的用例用。
两个 shell 用例、`mobile-slots` 的依赖表也接上。
- 用例里的失败桩从 `{ message }` 改成带码的官方形状(或真 `RemoteError` 实例):
它们原来在模拟一个**生产里不存在**的和 RemoteResult——真实 `result.error` 一定是 `RemoteError`。
- `scripts/generate-plugin-api-reference.mjs` 的接口探针加同形状最小桩(它只要求插件能装配起来,
不会走进失败分支)。
## 复核
- `pnpm test:ui` → 168 文件 / 1175 用例通过,1 文件失败是**环境原因**
(`workspace-navigation-startup.test.js` 调 `spawnSync python`,本机只有 `python3`;该文件未被改动)。
- `packages/host/product-api/tests/error-contract-client.test.ts` 8 条通过。
- `pnpm typecheck`、`pnpm check:architecture`、`pnpm check:plugin-types`、`pnpm check:plugin-docs` 通过。
下一步(未做):边界③(DSH 子进程 fatal 帧带码与详情)、④(运行时插件失败出口)、
⑤(Electron IPC 失败对象与 preload 转换)。
## 边界③(宿主 ↔ DSH 子进程):fatal 帧要带码
`hostSessionProtocol.ts` 的 fatal 帧原来只有 `message`。现在带四样(都可选,老子进程仍合法):
`code` / `details` / `stack` / `logPath`。子进程用 `foldFailure` 折一次再发,父进程**不重新猜语义**——
它看不懂 DSH 的异常。新增 `apps/desktop-host/src/childHostFailure.ts`(纯函数,可单测):
`isChildFatalFrame` 只校验形状、**不校验码在不在码表里**(码不认识也不能把失败变成静默成功),
`pluginHostFailureOf` 认不出的码折兜底、原码进 `details.what`。
父进程其余几条失败也带码了:启动超时与任务超时 → 新增的 `eleckoi/timeout/expired`;
子进程意外退出 → `eleckoi/plugin/load-failed`;无效帧 → `eleckoi/contract/invalid-response`;
未连接 → `eleckoi/lifecycle/disposed`;任务回执失败 → `eleckoi/ipc/call-failed`。
顺带补上一个真洞:**子进程 stdout/stderr 以前 100% 被丢**(`desktopPluginHost.ts` 的 `onDiagnostic`
没有任何构造点传它),现在接进主进程日志。`onFailure` 的日志键从 `{ error }` 改成 `{ err }`
(pino 只认 `err`,写 `error` 会丢栈),并带上码与详情。
## 边界⑤(Electron IPC):送失败对象,不送写死的句子
`desktopShell.ts` 的 `host.subscribeFailure` 从 `(message: string)` 改成
`(notice: DesktopHostFailureNotice)`(形状见 `contracts/errors/ipc.ts`)。主进程用
`desktopHostFailureNoticeOf(foldFailure(error))` 造载荷;preload 只转发**带失败标记**的值
(没有标记说明送来的不是失败,宁可不显示也不显示错话);渲染层按码解析。
## P3 上半:渲染层唯一文案入口
新增 `apps/web/src/utils/failureText.js`:
- 29 条码各一句用户说法(**逐码用例守**:漏一条就红;说法里不许出现码、`undefined`、`[object`);
- 参数化详情进句子(`找不到角色分组。` / `保存失败:/tmp/x。`);
- 认不出的码 → 通用说法且原码留在详情;裸 `Error` → 用它自己的 message(唯一能让用户知道
发生了什么的东西);
- **通道(严重度)默认由码给、调用点只能上调**:`resolveFailure(..., { upgradeTo: "app" })`
只接受上调,参数里没有"下调"的位置——这条因此是结构保证,不是约定。默认应用级的只有
宿主/库/存储/来源/已关闭五类;
- `diagnostic` 一行自带码 + 开发者文案 + 参数化详情(看日志不用回查码表)。
`app:notify` 多收一个可选的失败对象;`AppErrorDialog` 显示错误码与详情,复制内容变成
「一句话 + 码 + 详情 + 诊断行」。
## 用例
- `apps/desktop-host/tests/child-host-failure.test.ts`(8 条):帧校验、码原样保留、
未知码折兜底、③ → ⑤ 一跳后码不变、JSON 往返。
- `apps/web/tests/ui/failure-text.test.js`(48 条):逐码文案、详情插值、未知码、裸失败、
通道只上不下、诊断行与详情行。
- `packages/shared/tests/ipc-failure.test.ts` 加一条完整一跳(折叠 → 线上形状 → JSON → 再折叠)。
## 复核
- `pnpm test:ui` → 169 文件 / **1223 用例通过**(唯一红是本机缺 `python` 的环境问题)
- `pnpm test:desktop` → 48 通过(唯一红是沙箱里 Electron 起不来:`Missing X server or $DISPLAY`)
- `pnpm typecheck`、`check:architecture`、`check:plugin-types`、`check:plugin-docs` 通过
下一步(未做):边界④(运行时插件失败出口)、P4 闸与 70 处空 catch、P5 903 处带码、
P6 日志(落盘 / 动作键 / 会话首尾)、P7 收口。
## 新增闸 `scripts/check-error-contract.mjs`(`pnpm check:error-contract`)
四条规则(判据见脚本头):
1. **禁止丢码**(绝对值):`new Error(<…>.error.message)` —— 宿主已交出带码的失败,调用方却压成一句话;
2. **契约违例必须带 `issues`**(绝对值):`contractInvalid*` 的对象字面量必须有结构完整的 `issues`;
3. **空 catch 必须写成具名声明**(绝对值):`.catch(() => {})` 与空/只有注释的 `catch {}` 一律红;
4. **不许裸抛**(P5 的进度尺):领域/宿主/客户端插件层的 `throw new Error(` 必须带码;
今天只报数(1128),`--strict` 才判失败,P5 收口后改成默认。
范围:扫 `packages/{host,client,shared}`、`apps/{desktop,desktop-host,android-host,web}` 的源码;
排除随包分发的第三方浏览器库(html-resources 的 `assets/`)、`tests/`、`examples/`、`scripts/`。
规则 2 跳过契约声明目录(那里是规则表,不是构造点)。
**突变演练**(实测):往 `apps/web/src/utils/failureText.js` 注入一行 `new Error(x.error.message)` →
闸红(规则 1 命中 1 处 + 文件名行号);还原 → 绿。闸是真闸。
`package.json`:新增 `check:error-contract`,并接进 `build` 链(`check:docs` 之后)。
工作区新增 `eh13-gates.sh`(`eh-gates.sh` 的 0.2.13 版:点名 11 步 + `NODE_OPTIONS` 堆上限)。
## 空 catch 清零(80 → 0)
三个具名声明:`packages/shared/src/foundation/swallowedFailure.ts`(语义表在文件头)、
客户端 bundle 侧同一份在 `packages/host/product-api/src/client/swallowedFailure.ts`、
`apps/web/src/utils/swallowedFailure.js` 转发第一份。
- 8 个 `dsh-client-*` bundle 的刷新/重载/绑定 → `failureAlreadyPublished`;写队列 → `rejectionIsAwaitedElsewhere`;
- `apps/web` 的窗口显示、事件队列清理、导入丢弃 → `bestEffort`;
- **真丢的三处写操作改成就地报告**(`reportFailure` / `console.error` 进主日志):
聊天宽度偏好、设置库视图状态、变量面板视图状态;
- `author/runtime.ts`、roleplay 两个 host 模块、`product-api/src/index.ts`:文件内定义同义空函数并注释指向契约那份。
## 顺带修掉的三处真丢码
`dsh-client-author` 的 `runtime.ts` / `frontends.ts` 与 `apps/web` 的 `nativeFrontendRequest.js` 用的
`Object.assign(new Error(x.error.message), x.error)` 改成直接抛失败对象——折叠按结构标记识别,
不需要 `Error` 实例,而 `Object.assign` 只是"碰巧保住"字段。
LLM 的 `LlmFailure`(DSH 的 provider 码,不在我们码表里)三处(会话生成失败 ×2、roleplay 回合失败)折成
兜底码 + provider `code`/`status` 进详情:新增 `internalFailure()`(客户端 bundle 侧,码常量受测守住)。
**不写 `details.what`**,这样渲染层退回开发者诊断,provider 那句(常是"余额不足"这类)仍能给用户看。
## 新增两个共用模块
- `packages/shared/src/foundation/describeFailure.ts`:一行诊断 `[码] 文案(详情) · 操作名`,
渲染层与主进程共用(`failureText.js` 的 diagnostic 已改用它,时间戳留给日志层);
- 上面那份 `swallowedFailure.ts`。
## 复核
- `pnpm check:error-contract` → 规则 1/2/3 全 **0** 处、规则 4 = 1128 处;`--strict` 会因规则 4 失败(预期)
- `pnpm test:ui` → 169 文件 / **1223 用例通过**(唯一红是已知环境问题:缺 `python`)
- `pnpm typecheck` 通过;`bash eh13-gates.sh` → 11 步全绿
下一步(未做):P5 把 1128 处裸抛逐文件改成带码(规则 4 归零后改成绝对值)、
P3 下半(就地失败面组件)、P6 日志(落盘 / 动作键 / 会话首尾)、P7 收口。
## 码表生成(`generate:error-codes` / `check:error-codes`)
`packages/host/product-api` 的 client 面是运行时独立加载的 bundle,取不到契约包源码,
而 27 个 `dsh-client-*` 接下来也要抛带码的失败。所以:
- 新增 `scripts/generate-error-codes.mjs`:从 `packages/shared/src/contracts/errors/codes.ts`
(Node 24 类型剥离直接 import)渲染出 `packages/host/product-api/src/client/errorCodes.generated.ts`,
产物头部写明"生成,请勿手改";
- `check:error-codes` 校验产物最新,接进 `build` 链与 `eh13-gates.sh`;
- `invalidResponse.ts` 的两份手写常量(`INVALID_RESPONSE_CODE` / `INTERNAL_FAILURE_CODE`)改成从生成物派生,
少一处漂移源;client 入口导出 `ELECKOI_FAILURE_CODES` 供 bundle 用。
## 头两个领域文件(48 处裸抛 → 带码)
`packages/host/product-data/src/domain/agentPresets/AgentPresetRepository.ts`(27 处)与
`.../regexRules/RegexRuleRepository.ts`(21 处):
- 原中文句保留为**开发者诊断**(日志里靠它定位),码与参数化详情补上;
- `details.what` 一律写**名词短语**(会被拼进用户句:"找不到{what}。");
- 4 处 legacy `DESKTOP_ERROR_CODES.CONFLICT` 改成契约码 `conflict/stale-revision`;
- 解析辅助(`parseObject` / `parseList` / `parseStringList`)区分两件事:**形状不对**是
`data/shape-invalid`,**解析不出来**是 `storage/corrupt`;带码的失败在 catch 里原样抛出,
不再被包成一条丢语义的裸 Error。
## 复核
- `pnpm check:error-contract` → 规则 1/2/3 仍全 0;规则 4 从 1128 降到 **1085**
- `pnpm test:host packages/host/product-data/tests` → 23 文件 / **233 用例通过**
- `pnpm typecheck` 通过;`pnpm check:error-codes` → 30 codes current
下一步:继续按领域逐文件转换(`SettingLibraryRepository` 36 处、`creatorStudio/capabilities` 30 处、
`ConversationArchiveRepository` 18 处……),并把客户端 bundle 的抛出也换成 `ELECKOI_FAILURE_CODES`。
两个文件、66 处裸抛改成带契约码的失败(原中文句保留为开发者诊断): - `packages/host/product-data/src/domain/settingLibraries/SettingLibraryRepository.ts`(36 处): 快照/差异/分支设定的损坏 → `storage/corrupt`,形状不对 → `data/shape-invalid`, 编号重复/名称为空/循环 → `input/invalid`,找不到分支设定或上级 → `lookup/not-found`, 同名占用 → `lookup/duplicate`,只读设定被改/被删 → `security/denied`, 与角色不匹配 → `state/mismatch`,缺内容版本/缺保存数据 → `internal`。 - `packages/host/product-data/src/domain/creatorStudio/capabilities.ts`(30 处): 编辑约束(EJS 正文、脚本源码、排序、变量父级)→ `input/invalid`, 固定开场白/固定运行配置不能删 → `safety/refused`, 角色未选/需先处理文件夹内容 → `state/precondition`, 其它角色的配置 → `security/denied`,并发冲突(脚本库/显示模板/预设脚本库)→ `conflict/stale-revision`。 约定(与批次一一致):`message` 保留原中文;`details.what` 是名词短语;参数名只用闭集里的八个。 ## 复核 - `pnpm check:error-contract` → 规则 1/2/3 仍全 0;规则 4 **1085 → 1018** - `pnpm test:host packages/host/product-data/tests` → 23 文件 / **233 用例通过** - `pnpm typecheck` 通过
`packages/host/product-data/src/domain/conversations/ConversationArchiveRepository.ts` 18 处裸抛改成带契约码: - 格式不对 / 身份无效 / 缺少活动分支或分支设定状态 → `data/shape-invalid`; - 与当前数据库版本不兼容 → `data/legacy-mismatch`; - 不满意的文件格式或版本 → `input/unsupported`; - 找不到所属角色的聊天记录、绑定的变量版本 → `lookup/not-found`; - 会话身份/分支归属/角色对不上 → `state/mismatch`; - 包含其他会话的数据 → `security/denied`; - 当前角色不存在 → `character/not-found`。 ## 复核 - `pnpm check:error-contract` → 规则 1/2/3 全 0;规则 4 **1018 → 1000** - `pnpm test:host packages/host/product-data/tests` → 23 文件 / **233 用例通过** - `pnpm typecheck` 通过
## 闸的范围补全 `scripts/check-error-contract.mjs` 的扫描根补上三处之前漏掉的:`apps/desktop/resources/dsh/` (**运行时插件层**,属于六条边界的第 4 条)、`packages/sdk/`、`packages/compatibility/`。 补上之后立刻抓到一处真丢码:`packages/sdk/author/src/variable-bindings.ts` 的 `Object.assign(new Error(result.error.message), result.error)` → 改成直接抛失败对象。 ## 四个文件、76 处裸抛改成带码 - `domain/characterTransfer/characterCardFormats.ts`(17):格式不认/版本不支持 → `input/unsupported`; 大小超限/正则无效 → `input/invalid`;缺角色数据/解析不出 → `data/shape-invalid`; ElecKoi 数据损坏 → `storage/corrupt`; - `domain/frontends/FrontendProjectRepository.ts`(25):清单/形状/globalStyles → `data/shape-invalid`; 不认识的绑定目标/方法/清单版本 → `input/unsupported`;项目/资源/入口找不到 → `lookup/not-found`; 重名 → `lookup/duplicate`;跨归属/不可变字段/越界路径 → `security/denied`;清单不可删 → `safety/refused`; - `src/index.ts`(22):数据服务未初始化 → `lifecycle/not-ready`;Session/聊天归属、脚本库归属 → `state/mismatch`; 配置期并发 → `conflict/stale-revision`;数据库结构与外键 → `db/schema-mismatch`; 缺目录/路径的宿主装配 → `internal`; - `domain/variables/variableSchema.ts`(12):变量状态不是合法 JSON / 规则处理结果不对 → `data/shape-invalid`; 规则用了不支持的依赖或导出形态 → `input/unsupported`。 ## 复核 - `pnpm check:error-contract` → 规则 1/2/3 全 0;规则 4 **1038 → 962** - `pnpm test:host packages/host/product-data/tests` → 23 文件 / **233 用例通过** - `pnpm typecheck` 通过
60 处裸抛改成带契约码: - `storage/sqlite/installSchema.ts`(15):库结构/业务表清单/迁移链 → `db/schema-mismatch`; 旧库结构标识/旧快照/不可迁移文件 → `data/legacy-mismatch`;整理与迁移后的完整性校验 → `internal`; - `storage/sqlite/migrations/0002RuntimeClean.ts`(18):预设工具配置/子代理模型/角色扮演计划/联网搜索配置 的「已损坏」→ `storage/corrupt`,「不是对象/格式无效」→ `data/shape-invalid`, 「版本/模式/数量无法识别、拒绝截断」→ `data/legacy-mismatch`; - `storage/media/PngTextChunkCodec.ts`(12):超过大小上限 → `input/invalid`;不是 PNG / 文本编码不支持 → `input/unsupported`;数据块长度/类型/结束标记 → `data/shape-invalid`; - `domain/scripts/ScriptSourceFiles.ts`(15):越界路径 → `security/denied`;非普通文件 → `data/shape-invalid`; 链接文件 → `input/unsupported`;已删除 → `lookup/not-found`;外部修改 → `conflict/stale-revision`; 事务状态/事务目录 → `internal`;事务与记录不匹配 → `state/mismatch`。 ## 复核 - `pnpm check:error-contract` → 规则 1/2/3 全 0;规则 4 **962 → 902** - `pnpm test:host packages/host/product-data/tests` → 23 文件 / **233 用例通过** - `pnpm typecheck` 通过
product-api 的 host 面与 client 面都缺一个"造一条带契约码的失败"的工具:域层有 `@shared/foundation/DesktopError`,而 product-api 不能在运行时 import 契约包(本包是 tsc 直出,契约包 exports 指向 TS 源码,宿主进程里解析不到)。 新增 `packages/host/product-api/src/productFailure.ts`:普通对象工厂,形状与官方 `RemoteError` 的线上形状一致(`isDSHRemoteError` + `code` + `message` + `details`), 网关的 `rpcFailure` 只认这个标记,所以码与详情逐字穿过边界②。详情参数名闭集写成本地 常量 `PRODUCT_FAILURE_DETAIL_KEYS`,client 面拿不到契约包,靠用例守住。 为了让 host 工程(`include: src/*.ts`)也能编译到同一份码表,把生成物从 `src/client/errorCodes.generated.ts` 挪到 `src/errorCodes.generated.ts`:两个 tsc 工程 都编译它,内容与输出相同,不产生冲突。生成器、两个 tsconfig、`tsconfig.node.json` 的显式清单跟着改。client 面从 `../productFailure.js` 转出。 用例补在 `packages/host/product-api/tests/error-contract-client.test.ts`(15 条全绿): 形状过网关判据、普通对象能过 JSON、码表每一条都能构造、详情闭集覆盖契约表用到的参数。 证据: - `pnpm typecheck` EXIT=0 - `pnpm test:host packages/host/product-api/tests/error-contract-client.test.ts` 15 通过 - `node scripts/check-error-contract.mjs` 规则 1/2/3 = 0,规则 4 = 902(本提交不改处数) - `node scripts/generate-error-codes.mjs --check` 30 条码,产物最新 - `bash eh13-gates.sh` 11 个 check + typecheck 全绿
39 处裸抛改成带契约码(`DesktopError` + `ELECKOI_FAILURE_CODES`):
- `ConversationRepository.ts`(12):聊天存档/开场白/变量版本找不到 → `lookup/not-found`;
不能更改存档所属角色、聊天标题为空、开场白为空 → `input/invalid`(带 `field`);
- `ConversationStateRepository.ts`(11):历史变量/设定状态形状不对、历史设定差异标识或操作不对 →
`data/shape-invalid`;历史设定差异重复目标 → `lookup/duplicate`;会话 / 历史运行状态找不到 →
`lookup/not-found`;缺 Session 与轮次、缺正式消息标识 → `input/invalid`;已完成轮次不可覆盖 →
`state/precondition`;历史状态与分支不匹配、一轮多个状态 → `state/mismatch`;
- `legacyConversationStates.ts`(9):旧账本字段缺失 / 正式轮次不对 / 归属不一致 / 快照格式不对 /
轮次种类不支持 → `data/legacy-mismatch`(轮次种类带 `expected`);
- `ConversationVariableStateStore.ts`(4):聊天存档找不到 → `lookup/not-found`;
当前变量状态不是合法 JSON 或不是 JSON object → `data/shape-invalid`;
- `MessageRepository.ts`(2):待提交输入的图片引用格式 / 标识不对 → `data/shape-invalid`;
- `conversationSeed.ts`(1):开场白绑定的变量版本不存在 → `lookup/not-found`。
原中文 message 全部保留(既有用例按子串断言);`details.what` 只写名词短语,
具体 id 留在 message 里。去掉的是 `{ cause }`:`DesktopError` 只收(码、message、details)三参,
且 `details` 是给程序看的闭集参数,不塞异常对象。
## 复核
- `node scripts/check-error-contract.mjs` → 规则 1/2/3 全 0;规则 4 **902 → 863**(本批 39 处)
- `grep -rn "throw new Error" packages/host/product-data/src/domain/conversations/` → 0 处
宿主侧(`packages/host/runtime`、`apps/desktop-host`)要造带码失败,但本包根入口 没有转出 `productFailure` 与码常量,实现者只能绕道或写码字符串字面量。补上两行转出 (运行期 + 类型),与 client 面 `src/client/index.ts` 的出口保持一致。 证据:`pnpm typecheck` EXIT=0;`lib/types/index.d.ts` 里两个符号都可解析。
44 处裸抛改成带契约码(`DesktopError` + `ELECKOI_FAILURE_CODES`):
- `characterTransfer/CharacterTransferService.ts`(10):角色卡找不到 → `character/not-found`;
图片总大小/文件为空/超过 96 MB/一次超过 50 张 → `input/invalid`(带 `field`);
未选角色卡、导入令牌失效 → `state/precondition`;卡片全部导入失败 → `data/shape-invalid`;
导出目录同名文件过多 → `lookup/duplicate`;
- `characterTransfer/mvuImport.ts`(12):开场白变量初始化失败、初始化内容为空/不是对象/解析不出、
YAML 解析错误、JSONPatch 操作列表/路径/delta 目标/根节点、标签未闭合 → `data/shape-invalid`;
变量值不是 JSON 类型 → `input/unsupported`(带 `expected`);变量键长度不受支持 → `input/invalid`;
- `characterTransfer/portableSnapshots.ts`(6):设定库快照/变量配置文件格式不认 → `input/unsupported`;
设定库为空、快照解析不出 → `data/shape-invalid`;角色卡设定类型不受支持 → `input/unsupported`;
- `characterTransfer/mvuSchema.ts`(16):MVU Schema 的注册模块/注册函数/注册参数/依赖/注册上下文
无法静态提取 → `input/unsupported`(外部依赖带 `expected: 'zod 或 lodash'`)。
**保持原样的 2 处**(`mvuSchema.ts:111`、`:115`):它们在一个**生成器模板字符串**里,
生成出来的脚本由 `loadVariableSchema` 放进隔离 VM 执行(只注入 `z` 与 `_` 两个绑定,
`variableSchema.ts:89`)。那里没有 `DesktopError` 与码表,要让它们带码就得把两个符号注入被求值的
脚本,属于本工作包之外的契约面变更,所以保留裸 `Error`。其余 16 处都是宿主侧抽取器自己的抛出,
正常折码。
原中文 message 全部保留;`details.what` 只写名词短语,具体名字/编号留在 message 或 `path` 里。
`{ cause }` 去掉:`DesktopError` 只收(码、message、details)三参。
## 复核
- `node scripts/check-error-contract.mjs` → 规则 1/2/3 全 0;规则 4 **863 → 819**(本批 44 处)
- `grep -rn "throw new Error" packages/host/product-data/src/domain/characterTransfer/` → 只剩上述 2 处模板内抛出
范围:apps/web/src 下的 chatApi、usePersonaCharacters、scriptEditing、 applicationFrontendTargets、useChatInputImages。47 处 throw new Error 全部换成 DesktopError(@shared/foundation/DesktopError + @shared/contracts/errors/codes), 原中文句原样保留为 message,details.what 一律写名词短语;chatApi 里的 Promise.reject(new Error(...)) 一并折成 lifecycle/not-ready。 用到的码:lifecycle/not-ready(聊天服务、用户资料服务、角色目录服务)、 lookup/not-found(脚本项目、目标文件夹、脚本设置、未注册的窗口/页面/挂载点)、 input/unsupported(文件夹嵌套、前端清单版本、前端 SDK 版本)、input/invalid (destinationIndex、entryKey)、data/shape-invalid(脚本 JSON 编辑缓冲)、 lookup/duplicate(root 绑定、重复绑定)、state/mismatch(页面与所属窗口)、 state/precondition(生成中不能加图)、media/invalid(图片数量/格式/内容/大小)。 判据(证据): - 5 个文件 grep "throw new Error" 0 处 - node scripts/check-error-contract.mjs:规则 1/2/3 = 0;规则 4 全仓 902 → 855 (本包 47 → 0) - pnpm typecheck:EXIT=0 - pnpm exec vitest run --project ui:Test Files 1 failed | 169 passed (170)、 Tests 1 failed | 1223 passed (1224),唯一红是已知红 workspace-navigation-startup.test.js(本机只有 python3,spawnSync python ENOENT) - node scripts/check-architecture.mjs:EXIT=0
## 八个文件、61 处
- `sessionHistoryRecovery.ts`(13):快照/输入身份/请求记录序号/活动账本/消息引用 → `data/shape-invalid`;
会话身份与版本对不上、历史与原生消息无法唯一对应 → `state/mismatch`;日志恢复期间被改 →
`conflict/stale-revision`;写入校验 → `internal`;回滚失败 → `io/failed`;缺少可验证的历史回复 →
`lookup/not-found`;无法重定位的外部插件记录 → `input/unsupported`。
- `sessionMessageEdit.ts`(11):参数 → `input/invalid`;找不到消息/会话 → `lookup/not-found`;角色或
内容不一致 → `state/mismatch`;旧格式 → `data/legacy-mismatch`;写入校验 → `internal`;日志被并发
修改 → `conflict/stale-revision`;回滚失败 → `io/failed`。
- `sessionRewind.ts`(11):轮次参数 → `input/invalid`;找不到轮次/消息 → `lookup/not-found`;上一轮
未结束 → `state/precondition`;日志被并发修改 → `conflict/stale-revision`;生成日志无效 → `internal`;
回滚失败 → `io/failed`。
- `trajectory.ts`(8):会话头格式/版本 → `input/unsupported`、`data/shape-invalid`;会话身份对不上 →
`state/mismatch`;损坏或解不出的记录 → `storage/corrupt`;历史会话日志解码失败 →
`data/legacy-mismatch`。
- `sessionEditPlugin.ts`(6):缺 DSH_SESSION_ROOT → `internal`;会话日志读不到 → `lookup/not-found`;
工具结果删除服务缺席 → `lifecycle/not-ready`;分镜编号不一致 → `state/mismatch`。
- `sessionRequestContextRepair.ts`(6):身份/版本、记录与步骤对应 → `state/mismatch`;日志被并发修改
→ `conflict/stale-revision`;写入/完整性校验 → `internal`;回滚失败 → `io/failed`。
- `generatedImageCleanup.ts`(4):附件存储未配置 → `state/precondition`;路径/目录越界、非本地普通
文件 → `security/denied`。
- `sessionTranscriptImport.ts`(2):未开始对话的 Session → `state/precondition`;导入校验 → `internal`。
## 写法
`productFailure(ELECKOI_FAILURE_CODES.…, <原中文句>, { what / field / operation, path / guard })`:
原诊断句逐字保留;`details.what` 一律名词短语(`state/precondition` 按码表用"该做什么"的动词短语)。
原 `throw new Error(msg, { cause })` 的 cause 被丢弃:`productFailure` 的详情是闭集八键,没有 cause
的位置,底层异常不再随行,诊断靠原句 + 参数化详情(与批次四/五同做法)。
`sessionRewind.ts` 的三处 `throw new DshSessionRewindUnavailableError(...)` 未动:它是同目录
`sessionEditPlugin.rewind()` 用 `instanceof` 消费的"回退不可用"控制信号(转成返回 undefined),不跨
边界、没有码语义;改掉它会改调用约定,留给后续判断。
## 复核
- `node scripts/check-error-contract.mjs`:规则 1/2/3 全 0;规则 4 **902 → 841**(本包 61 处归零)。
- `pnpm exec vitest run --project host packages/host/runtime/tests`:7 文件 / **54 用例通过**,Errors 0。
- `tsc -p packages/host/runtime/tsconfig.json --noEmit` 的报错与基线逐行相同(93 行;该 tsconfig 没有
`@shared` 别名,属既有状况),本批未新增类型错误。
把 ScriptLibraryRepository 的 9 处裸抛折成契约码(internal / state-mismatch / lookup-not-found / lookup-duplicate / character-not-found / agent-preset-not-found), 原中文句保留为 message,details.what 用名词短语;4 处 legacy DESKTOP_ERROR_CODES.CONFLICT 一并折成 conflict/stale-revision(与批次一的 RegexRuleRepository 同法),本文件不再抛 legacy 码。 判据:该文件 grep "throw new Error" 为空;规则 1/2/3 不动。
范围:product-data 六份文件 —— scripts/scriptLibraryImport、settingLibraries/settingLibraryNormalization、 settingLibraries/settingLibraryCodec、variables/variableConfigCodec、variables/VariableStateRepository、 personas/characterLookup(本批 23 处裸抛)。 码选取对齐已转换的邻居 SettingLibraryRepository.validateConversationLibrary:编号重复/循环用 input/invalid + field,找不到上级/所属文件夹用 lookup/not-found + what,同名用 lookup/duplicate + what; 导入附件的路径级错误带 path(附件路径、附件内容、脚本列表)与 field(总大小),解码不出用 storage/corrupt。 证据:规则 4 全仓 902 → 879(本包 50 → 27);规则 1/2/3 仍 0;pnpm typecheck 见下一条提交。
把 VariableConfigRepository 的 11 处裸抛折成契约码:展开状态解析失败折 storage/corrupt(原两条裸抛合成一个具名失败,不再有空的 catch 体), 版本/备份查找折 lookup/not-found,角色与归属对不上折 state/mismatch, 变量版本被聊天或开场白占用时折 state/precondition(what 写要做的动作), 备份初始状态不是对象折 data/shape-invalid(带 path: initialStateJson)。 原中文句保留为 message。 判据:该文件 grep "throw new Error" 为空;规则 1/2/3 仍 0。
范围:product-data 六份文件 —— editorVersions/index、editorVersions/EditorLibraryVersionsService、 personas/CharacterRepository、displayTemplates/index、frontends/FrontendIndex、 creatorStudio/CreatorProjectRepository(本批 26 处裸抛)。 码选取:乐观并发之外的“归属/版本/编辑内容对不上”统一 state/mismatch(带 what); 归档目录的符号链接与路径越界用 security/denied(what 写被拒的动作);归档内容/摘要损坏用 storage/corrupt;前端索引的未知条目与分区形状用 data/shape-invalid(带 path 指到具体条目/分区); 角色编号为空或重复按 input/invalid + field、lookup/duplicate 分开;找不到角色用 character/not-found。 displayTemplates 的“所属角色或预设”按 scope 选 character/not-found 或 agent-preset/not-found(专属域码)。 证据:规则 4 全仓 902 → 853(本包 50 → 1,剩余 1 处是 guides.ts 生成示例脚本字符串里的 throw,属 brief §三 第 1 类“生成代码模板”,保留原样并在报告里说明);规则 1/2/3 仍 0。
把 FrontendCharacterDeletion 的 13 处裸抛折成契约码:角色不存在折 character/not-found,索引身份/归属/删除状态对不上折 state/mismatch, 删除期间被外部改动折 conflict/stale-revision,删除记录形状不对与目录不是 普通目录折 data/shape-invalid(后者带 path),回滚时目录缺失折 lookup/not-found, 越出删除根目录折 security/denied,删除未提交折 internal。 原英文句保留为 message(这些句子来自上游,本文件原本就是英文文案)。 保留不动:run() 里 settle 二次失败时的 AggregateError——它聚合的是两个失败的 完整现场,折成单码会丢掉其中一个,规则 4 也不计它(不是 throw new Error)。 判据:该文件 grep "throw new Error" 为空;规则 1/2/3 仍 0。
WP5 的 4 个 bundle / 宿主文件要用 `require('@eleckoi/dsh-product-api')` 取
`productFailure` 与 `ELECKOI_FAILURE_CODES`,测试装载器的 require 桩只认它显式列出的名字,
所以先把两个名字加进桩(与 `packages/host/product-api/src/productFailure.ts` 同一份实现)。
范围:只加两个导出与一段注释,不改桩的既有行为,其余 bundle 的既有用例不受影响。
判据:packages/client/features/dsh-client-creator-studio/tests 与
dsh-client-conversations/tests 在后续提交里全绿。
`dsh-client-creator-studio/src/client.js` 的 11 处裸抛全部换成 `productFailure`, 原来的中文诊断句逐字保留,`details` 只用闭集里的参数: - `lifecycleDisposed`(服务已关闭:工作室服务 1、创作项目服务 4),`what` 写服务名; - `statePrecondition`(用户的下一步动作:打开工作室对话 4、等待当前消息操作完成 1); - `dataShapeInvalid`(创作项目列表形状不对 1),`what: '创作项目列表'`。 同一类失败只用一条码:4 处「未打开」全用 statePrecondition + 同一个 `what`。 规则 4:全仓 902 → 891(本文件 11 → 0) 规则 1/2/3:0 / 0 / 0 (typecheck 与两个包的用例在下一次提交后一起跑,结果见后续提交信息)
`dsh-client-creator-studio/src/host/runtime.mjs` 的 13 处裸抛换成 `productFailure`, 原中文诊断逐字保留: - `characterNotFound`(找不到所选角色,1); - `lookupNotFound`(不是工作室会话 / 没有可重新生成的用户输入,2); - `lifecycleNotReady`(Agent 未完成恢复 / 未完成创建,2); - `statePrecondition`(先切创作预设、先关角色对话工具、先停止生成、换可安全删除/重新生成的消息,7); - `internal`(缺 Host 运行目录:宿主组合根没给环境变量,属内部不变式,1)。 同一类失败只用一条码:「正在生成」两处都用 statePrecondition + `what: '停止生成'`。 规则 4:全仓 891 → 878(本文件 13 → 0) 规则 1/2/3:0 / 0 / 0
`dsh-client-creator-studio/src/host/agent.mjs` 的 12 处裸抛换成 `productFailure`, 原中文诊断逐字保留: - `internal`(插件配置形状、挂载作用域、缺调用会话:三条都是宿主组合根的不变式,2+1); - `securityDenied`(会话不属于本作用域,1,`guard: 'creator-studio-scope'`); - `stateMismatch`(会话缺少正式预设绑定,1); - `inputInvalid`(operation_ids 两处、capability_id 一处,3); - `lookupNotFound`(未知能力组 / 未知创作操作,2); - `statePrecondition`(请先在菜单选择编辑角色,1); - `dataShapeInvalid`(新建角色没返回有效编号,1)。 错误码只做这些:工具入参不合法一律 inputInvalid + `field`,找不到对象一律 lookupNotFound + `what`。 规则 4:全仓 878 → 866(本文件 12 → 0) 规则 1/2/3:0 / 0 / 0
`dsh-client-conversations/src/client.js` 的 49 处裸抛全部换成 `productFailure`, 原中文诊断逐字保留,`details` 只用闭集里的参数。同一类失败只用一条码: - `statePrecondition`(用户要先做的事:先打开聊天 5、选导出/导入目标 2、切换开场白 1、 当前聊天不可用/会话不可用/时间线不可用 3、等消息修改 1、等生成 1、开始/重新生成前校验 2,共 15); - `lifecycleNotReady`(依赖尚未就绪:DSH 会话 5、轨迹投影 1、会话客户端 4、 官方输入框 1、文件上传 1、聊天正文 1,共 13); - `lifecycleDisposed`(服务已关:会话目录 2、聊天服务 3,共 5); - `dataShapeInvalid`(返回数据形状不对:会话目录/详情/新建结果/投影/上传结果/更早消息/ 时间线/生图编号/模型重试状态/执行轮次,共 10); - `lookupNotFound`(图片附件、这条输入对应的 DSH 消息,共 2); - `stateMismatch`(宿主回退/接受状态对不上,共 2); - `inputInvalid`(输入框字段 field: text、文件 field: file,共 2)。 另有两处同族裸抛(reject 一个裸 Error)一并带码:等待会话完成超时、等待会话关闭通知超时 (`timeoutExpired`)。剩下 5 处 `new Error(...)` 不动:`lastAgentError`(1)与 `openError`(1) 包装保住 Error 身份(用例按 Error 相等断言)、两个 `$stream` 的 `ended` 回调(2)、 以及带 `name: 'AbortError'` 的取消哨兵(1,不是失败)。 规则 4:全仓 866 → 817(本文件 49 → 0) 规则 1/2/3:0 / 0 / 0
规则 4:0 → 0(本包不改投掷面)
规则 1/2/3:0 / 0 / 0(check-error-contract EXIT=0)
typecheck:EXIT=0
测试:
--project unit packages/shared/tests → Test Files 6 passed (6) Tests 76 passed (76) EXIT=0
--project desktop apps/desktop/tests → Test Files 11 passed | 1 failed | 1 skipped (13)
Tests 65 passed | 1 failed | 4 skipped (70) EXIT=1
唯一红:updater-network.test.js(已知环境红:本机 Electron 起不来——Missing X server or $DISPLAY;
补 X 后仍红在 Windows autoUpdater 平台断言,见 notes/eh13-progress.md「环境红的准确口径」)
check:architecture → EXIT=0(ipcMain.handle 只剩胶水 1 处;ipcRenderer 只在 preload)
冲突:2 处,按并集解:
- apps/desktop/src/main/modules/updates/updatesPlugin.ts:HEAD 的 logAction 检查更新开始/成功/失败
三行日志 + wp19 的 handleDesktopIpcCall 包装;保留 async handler(内部有 await),两边语义都在
- apps/desktop/src/main/platform/electron/mainWindowPlugin.ts:HEAD 的 logAction/relayRendererActionLog
/DesktopError 导入 + wp19 的 handleDesktopIpcCall 导入;5 个 handler 的包装与日志埋点都保留
- packages/shared/src/foundation/ipcFailure.ts 自动合并:wp18 的 logPath「宿主本机指针」语义与
wp19 的 wrap/throw/unwrap 四个纯函数并存,已逐段核过
异常:1 处闸假红与处理——check:architecture 的 ipcRenderer 规则按原文匹配(含注释),wp19 在
ipcFailure.ts 的 JSDoc 里写了 `ipcRenderer.invoke` 触发假红。保持闸的严格度、只把这 2 行注释里的
标识符改成 `invoke`(preload 语义不变,无代码改动)。闸本身未动,建议后续把该规则也改成「只看代码位置」
(与 wp15a 对 check-error-contract 的改法一致)。
规则 4:0 → 0(本包不改规则 4 正则内的投掷面;实现在旧 tip 上是 82 → 82)
规则 1/2/3:0 / 0 / 0(check-error-contract EXIT=0,扫 724 个源文件)
typecheck:EXIT=0
测试:
--project integration tests/integration/tool-result-recovery.test.js
→ Test Files 1 passed (1) Tests 25 passed (25) EXIT=0
(25 而不是 24:wp20 的 03ee0f4 新增「归属记录损坏折 storage/corrupt」一条;
f7c60cf 修的「真实目录」聚合错误断言照旧通过)
--project host product-data + product-api + web-search-tavily + web-deepseek +
dsh-client-roleplay/tests → Test Files 50 passed (50) Tests 409 passed (409) EXIT=0
--project unit → Test Files 10 passed (10) Tests 84 passed (84) EXIT=0
--project ui(apps/web 8 个面 + author/roleplay 客户端)
→ Test Files 121 passed | 1 failed (122) Tests 867 passed | 1 failed (868) EXIT=1
唯一红是 wp19 合并暴露的源码文本断言(rightbar-shell-layout.test.jsx:97-98 仍在找
`ipcMain.handle(...)`,而 wp19 后注册走 handleDesktopIpcCall)——行为没变,是断言过期;
按「合并后暴露的缺口」另起一个提交修(见紧随其后的提交),不计入 wp20。
冲突:2 处,已解:
- packages/client/features/.../host/tool-result-store.mjs:与集成分支 f7c60cf 同一处
(removeSessionResults 的 [object Object])。保留一份——沿用 f7c60cf 的模块级
failureMessage()(Error/普通对象/字符串取 .message),wp20 的内联 message 变量删除,
两边的意图注释保留;wp20 的 readResult() 归属记录 JSON.parse → storage/corrupt 整段保留。
- packages/host/product-api/src/variable-processing.ts:wp20 的改动(自造 VARIABLE_* 码 → 契约码)
在集成分支上已有等价形态,取集成分支侧:this.closed 抛 lifecycle/disposed(wp20 分支上是
裸 Error)、{ what: '等待当前变量修改结束' } 与 { what: '变量状态或配置' } 保留集成措辞;
导入顺序取集成侧。无一边丢语义。
异常:1)上述 rightbar-shell-layout 断言过期(wp19 引入、wp19 自己的验证没跑 UI 套件,
本批第一次跑 UI 才发现),follow-up 提交修;2)wp20 报告里的「规则 4 = 82」是旧 tip 的计数,
合入后集成分支仍是 0。
wp19 把 9 处 ipcMain.handle 收进 handleDesktopIpcCall(desktopIpcHandler.ts), 通道注册行为不变;但 apps/web/tests/ui/rightbar-shell-layout.test.jsx:97-98 是按源码 文本断 `ipcMain.handle(DESKTOP_SHELL_IPC.browserAcquire/Release`,合并后变红。 wp19 的验证只跑了 unit / desktop / architecture,没有跑 UI 套件,所以这是本批第一次跑 `--project ui` 才发现(wp20 合并复核时暴露)。 改法:断言对象从 ipcMain.handle 换成 handleDesktopIpcCall,仍是「mainWindowPlugin 注册了 两条浏览器桥通道」这条行为断言,不是放宽(通道常量与 preload 侧的断言原样保留)。 与 wp21 的 6 条行为变更后断言更新同类,理由记在本提交。 红 → 绿: - 修前:--project ui(apps/web 8 面 + author/roleplay 客户端) → Test Files 121 passed | 1 failed (122),Tests 867 passed | 1 failed (868) - 修后:同一组 → Test Files 122 passed (122) Tests 868 passed (868) EXIT=0 - 单文件:Test Files 1 passed (1) Tests 6 passed (6) - check:architecture 不受影响(mainWindowPlugin 里没有 ipcRenderer 字样)
规则 4:0 → 0(本包不改投掷面)
规则 1/2/3:0 / 0 / 0(check-error-contract EXIT=0)
typecheck:EXIT=0
测试:
--project ui apps/web/tests/ui → Test Files 25 passed (25) Tests 150 passed (150) EXIT=0
--project ui(ui + mobile + persona + authorFrontend + appearance + displayTemplates +
presets + chat)→ Test Files 114 passed (114) Tests 741 passed (741) EXIT=0
冲突:15 个文件,全是「wp12 的 FailureAlert 失败面(集成分支,较新)」vs「wp21 的
setError(resolveFailure(...).text) 文本形态(分支基线早于 FailureAlert)」。按 brief
「实现者取了旧形态时以合并后正确语义为准」:
- 13 个文件取集成分支侧:状态里存**原始失败对象**,渲染交给 FailureAlert——它在内部调
resolveFailure,所以 wp21 的目标(用户句来自码、不直显 .message)照样成立,而且码与
详情能展示(wp21 的 .text 字符串会丢掉这两样)。涉及 mobile 三页、AutoIllustrationSettings
(含 catalogError 的 FailureAlert 结构)、ChatFileCards、CreatorStudio 三件、
CharacterManager、CustomPositionManager、DynamicSettingsPanel、VariableConfigInspector、
ChatImageGallery。
- useAppUpdates.js:两边等价(wp19 的 failureMessage 就是 resolveFailure(...).text 的别名),
统一取 wp21 形态。
- PresetPanel.jsx:该文件错误是直显 <p>{error}</p>(无 FailureAlert),取 wp21 的
resolveFailure 文本,并保留集成分支的 logAction(LOG_ACTIONS.presetSave, 'failure', cause)。
- 清理:9 个文件因为取了对象形态,wp21 带进来的 resolveFailure 导入不再使用,已删。
异常(合并暴露的过期断言,均为「行为/文案变新之后的必要更新」,不是放宽):
1. apps/web/tests/ui/renderer-failure-text-wiring.test.jsx(wp21 新增):它在 wp21 分支上断的是
`<p role="alert">{text}</p>` 的整段文本;合并后 ChatFileCards 走 FailureAlert,role=alert
里还有码与详情。两条断言改成 toContain(仍断「按码句子在、开发者诊断不在」)。
2. authorFrontend 两处:wp21 的 04086b0 按它分支上的旧文案断「不支持这个格式或版本:…」,
集成分支的 failureTexts.js(wp15a 后)是「不支持这个格式、版本或能力:…」;更新成合并后的
唯一文案源。两条都在同一批用例里,未动行为断言。
裸抛已归零(902 → 0),所以 `check:error-contract` 的规则 4 不再需要 `--strict` 才判失败: 默认就是绝对值。保留 `--progress-only` 只给未来做迁移的线路临时看数用,CI 不带它。 证据:`node scripts/check-error-contract.mjs` 四条规则 0/0/0/0,EXIT=0; `--progress-only` 仍可只报数。闸的 tooling 用例 43 条全绿。
WP9 与 WP20 把 `update-coordinator.ts` / `update-http-executor.ts` 的裸抛折成带码失败, 但没更新 `provenance.json` 的 `localSha256`,`check:desktop-licenses` 因此红(B1 的 tip 上就已经红)。 按仓库的归一化规则(`\r\n` → `\n`)重算两个文件的新哈希并补 `changes` 说明。 证据:`pnpm check:desktop-licenses` → "Desktop third-party license notice is current."; `bash eh13-gates.sh` 的 12 步全绿。
`apps/desktop-host/src/desktopPluginHost.ts` 的「DSH 插件宿主未连接」原来折成 `lifecycle/disposed`, 而 disposed 的用户句是「已关闭,请重新打开」,与"还没连上"的语义矛盾(审查一 D 类遗漏项; WP9 合入后该文件已在集成分支上,可以改)。 证据:`--project host apps/desktop-host/tests` 15 条全绿;`pnpm typecheck` EXIT=0。
全量门禁 `pnpm test` 的 `|browser|` 组里,`rich-message-corners-browser` 与
`rich-message-limit-browser` 红在 `Could not resolve "@shared/contracts/errors/codes"`:
本线给 `sharedFrontendDocument.js` 加了 `@shared/*` 别名 import,而这两个用例的 esbuild
夹具没有别名解析器(同组的 `rich-message-settling-browser` 早就有)。
照同组既有写法补 `alias: { '@shared': resolve('packages/shared/src') }`。
证据:接上 X socket 后跑 `--project browser` 这两个文件 → Test Files 2 passed / Tests 2 passed。
解决的问题: 1. side-panel-motion 在 desktop-ci 上确定性失败:runner 的操作系统可能报告 prefers-reduced-motion: reduce,titlebar.css 在 reduce 下把 grid 过渡设为 none,probe 等不到动画。probe 只在最后显式模拟 reduce,默认路径没有固定偏好。 2. 浏览器测试在 Windows CI 上偶发 spawn EBUSY:刚安装的 Electron 镜像被杀软或残留句柄锁住;上一轮只给 mobile-editor-layout 加了内联重试,这次同一个错误换到了 chat-bottom-control-layout。 怎么解决: 1. side-panel-motion.probe 在页面加载后、第一次点击前用 DevTools Emulation 固定 prefers-reduced-motion: no-preference;减少动效用例仍显式切成 reduce。 2. vitest 的 browser 项目加 retry: 2,覆盖所有浏览器探针的启动锁;mobile-editor-layout 的内联重试保留,恢复更快。
背景:代码里 21 处注释引用的「ADR 0002」不在本仓库(docs/adr 从 0012 起收录, 且明写不复制内部历史材料补齐),判据在仓库内无法解析。按 docs/MAINTENANCE.md 从尚未使用的 0040 起立档,把本轮错误处理契约的 15 条决定写进仓库,并在索引表 登记状态「已实施」。 范围:新增 docs/adr/0040-error-contract.md;docs/adr/README.md 加 0040 一行。 判据:node scripts/check-docs.mjs → Documentation check passed: 79 Markdown files, 28 unique ADRs,EXIT=0。
评审二认为 conversations / roleplay / useChatInputFiles 里的 22 处 details.what
写成动词短语会让用户句变病句,要求一律改名词短语。逐处核对后不成立:这 22 处
全部是 state/precondition,模板就是 `请先${what}。`,contracts/errors/details.ts:96-99
也写明这条码的 what 是「用户该先做的动作」;按名词短语改会得到
「请先一个打开的聊天。」「请先运行环境变量。」——正是要消灭的病句。
评审引用的「请先完成前置操作:打开一个聊天。」来自无参数兜底句「请先完成前置操作。」
加详情的误读,渲染层里不存在这句。
所以码与 what 都不动,改为把动作短语与应得用户句逐条钉进用例
(apps/web/tests/ui/failure-text.test.js):覆盖 8 个不同的 what、22 处调用点,
清单写在用例注释里;以后谁按「一律名词短语」改回去会立刻红。
判据:
- pnpm exec vitest run --project ui apps/web/tests/ui/failure-text.test.js:
Test Files 1 passed (1) Tests 57 passed (57) Errors 0
MainWindow 的错误边界兜底原来是 `页面加载失败:{error?.message || String(error)}`:
直接把开发者诊断(`Error.message`)当用户文案显示,绕过了渲染层唯一解析点
(ADR 0002 决定 4),而且把错误码整个丢掉——这是最显眼的一处还在直显诊断的地方。
改成渲染 `FailureAlert failure={error} context="页面加载失败"`,与 WP21 改过的
103 处同一做法:用户句按码给,码与参数化详情一起出现在失败面里;`context` 只在
失败本身给不出任何说法时兜底。`resolveFailure` 永不抛,放在错误边界里安全。
失败面同时受 WP23(3/3)的应用级通道约束:命中应用级码时自动升级到窗口级对话框。
背景:代码注释里的 21 处「ADR 0002」不在本仓库(docs/adr 从 0012 起收录,且明写 不复制内部历史材料补齐);另有 7 处「spec 决定 N」与 8 处文件名引用指向 /home/camel/travern 工作区的私有材料(spec-error-handling-remainder.md、 notes/eh-rebuild-0.2.13-plan.md)。按 docs/MAINTENANCE.md,私有知识库与内部调研 不进公开仓库。 范围: - 21 处「ADR 0002」→「ADR 0040」,13 处裸「ADR 决定 N」补成「ADR 0040 决定 N」; - 7 处「spec 决定 N」按 ADR 0040 的决定号或决定 2 的条款改写; - 8 处工作区文件名引用删掉或改成仓库内描述(logActions / actionLog / 两个用例 / boundaries ② 的翻译包装说明); - codes.ts 另删 1 处 notes/eh-cn-throws-inventory.tsv 路径,保留 903 处的事实描述。 判据:grep -rn 在 packages apps docs scripts 里查「ADR 0002」、 「spec-error-handling-remainder.md|notes/eh-rebuild-0.2.13-plan.md」、「spec 决定」、 裸「ADR 决定」均为空。node scripts/check-error-contract.mjs → 规则 1/2/3/4 全 0,EXIT=0。
原来 resolveFailure 给的 channel 只被 FailureAlert 当 CSS class 用:五条默认走应用级的码
(pluginLoadFailed / dbSchemaMismatch / storageCorrupt / senderUntrusted / lifecycleDisposed)
经失败面仍然只是"就地小面换个边框",不满足 User Story 10。
做法(任务卡的第二选,薄):
- 新增 apps/web/src/utils/appFailureOutlet.js:模块级订阅(subscribeAppFailure /
hasAppFailureListener / reportAppFailure)。不新增 IPC 通道——失败对象本来就在渲染层。
- FailureAlert 在 resolved.channel === "app" 时**不就地渲染**,把 resolveFailure 的结果
送进出口;没有窗口订阅时退回就地渲染(宁可留在角落,也不让失败消失)。
- MainWindow / ChatWindow / MobileWindow(三个有 AppToast 的宿主窗口)启动时订阅,
接到各自的 chat.notify("error", resolved.text, resolved) → AppErrorDialog,码与详情可见。
为什么不用逐层传 onEscalate:FailureAlert 在 23 个文件里用了 38 次,一路传回调会把"升级"
变成每个调用点都得记得写的东西,漏一处就把应用级失败留在角落;订阅让既有与新增调用点自动生效。
严重度只上不下不变:只有 channel === "app" 才走出口,普通失败不误升级。
用例:
- 新增 apps/web/tests/ui/failure-alert-escalation.test.jsx(jsdom):
storageCorrupt 经失败面升级(就地不渲染、窗口收到码/详情/用户句)、
普通 lookup/not-found 仍就地渲染、调用点上调同样升级、没有出口时就地兜底。
- app-toast.test.jsx 补一条:对话框里码与参数化详情仍然可见。
- 证据:4 个文件 75 tests passed(failure-alert / escalation / failure-text / app-toast),Errors 0
背景:docs/error-contract.md 是生成物,生成器里有两处与已拍定口径不符:
① 规则 4 仍写「收口进度尺、归零后 --strict 判失败」,而闸现在默认就是绝对值,
--strict 已换成 --progress-only;
② 读者段把内部码表写成对外契约(「非 TypeScript 实现者」「第三方插件作者按 code 判断」),
与「码表不做公开契约、对外表态要单独点头」冲突。另按评审补充:把 what 一律说成
「名词短语」是过度概括(state/precondition 的 what 是动作,模板「请先{what}。」)。
范围:
- 生成器:规则 4 改成「四条规则都是绝对值;--progress-only 只给迁移线路临时看数」;
读者段改成内部契约(本仓库维护者与客户端 bundle;第三方按 code 分支不是承诺过的接口);
顺带把「非 TypeScript 实现自己写文案」「跨语言实现」「规格把…列为 Out of Scope」三处
对外/仓库外口径改成内部表述;
- what 说明按码分两种:§3 加一段,§5 的 state/precondition 行加列内说明;PARAM_CELL_NOTES
随码表校验,写错码名会红;
- 真源注释同步:details.ts / codes.ts / productFailure.ts 里「what 一律名词短语」的
过度概括改成两种写法;
- 重渲 docs/error-contract.md。
判据:node scripts/generate-error-contract-doc.mjs → 30 codes、8 detail params、
5 app-level codes;--check → current,EXIT=0;node scripts/check-docs.mjs →
79 Markdown、28 unique ADRs,EXIT=0。
规则 4:0 → 0(绝对值;本包不改码,只改引用与生成器)
typecheck:EXIT=0
文档闸(四条逐个跑,非 && 串联):
check:docs EXIT=0(79 个 Markdown / 28 个 ADR)
check:error-contract-doc EXIT=0(30 码 / 8 参数 / 5 应用级)
check:error-codes EXIT=0(30 码)
check:plugin-docs EXIT=0(16 bundle / 107 接口 / 135 Remote 方法)
合并后 git status --porcelain:空
冲突:无(24 文件,+149/-73;源码侧全部是注释与文档引用,唯一非注释改动是
scripts/generate-error-contract-doc.mjs 的生成逻辑与其产物 docs/error-contract.md)
规则 4:0 → 0(绝对值;本包不新增裸抛) typecheck:EXIT=0 check:architecture:EXIT=0(只剩既有的行数提示) 测试(--project ui apps/web/tests/ui):Test Files 26 passed (26) Tests 163 passed (163) Errors 0 测试(--project host dsh-client-conversations + dsh-client-roleplay): Test Files 16 passed (16) Tests 107 passed (107) Errors 0 新增用例确实执行:failure-alert-escalation 4、app-toast 3、failure-text 57、failure-alert 11 合并后 git status --porcelain:空 冲突:无(FailureAlert.jsx 由 ort 自动合并:wp22 的 ADR 0040 注释与 wp23 的升级逻辑都在;已核对) 合并后收拾 1 处(无文本冲突,属语义冲突): apps/web/src/utils/appFailureOutlet.js:4 的「ADR 0002」→「ADR 0040」。 原因:wp23 从 wp22 之前的 tip 分叉,这个新文件带回了工作区私有 ADR 0002 的引用; wp22 的语义就是把失败契约改指仓库内 ADR 0040(仓库 docs/adr/ 下没有 0002, 工作区的 /home/camel/travern/docs/adr/0002-*.md 不在仓库里)。收拾后全仓源码 0 处 ADR 0002。
解决的问题: 1. 固定动效偏好后,用例继续跑到宽度断言:GitHub Windows runner 的屏幕只有 1024 宽,窗口被宿主限制,useSidePanelLayout 按 min-width 把侧栏压到 325(期望 328),确定性失败。 怎么解决: 1. 用 DevTools Emulation.setDeviceMetricsOverride 显式模拟 1400/960 视口,布局不再依赖宿主屏幕;resize 用例同步覆盖 960/1400。
KuramiWan
marked this pull request as ready for review
October 18, 2026 11:56
- docs/error-handling.md:写失败的 5 条规则、加码 5 步、严重度、日志与明确不做;判据指向 ADR 0040 - docs/README.md:当前架构索引补一条 - docs/adr/0040-error-contract.md:状态行指向协作声明 - .github/PULL_REQUEST_TEMPLATE/desktop.md:错误契约勾选项 验:node scripts/check-docs.mjs(80 文件 / 28 ADR)、node scripts/check-error-contract.mjs(四条规则 0 处)
- scripts/tests/check-error-contract.test.mjs 直接 import 闸脚本;Windows 检出把行尾转成 CRLF,
带 shebang 的模块在 vitest 里报 SyntaxError: Invalid or unexpected token(CI 上 0 test)
- 已在本机把两份文件转成 CRLF 复现,去掉 shebang 后 8 条用例通过
- 脚本由 `node scripts/check-error-contract.mjs` 调用,文件无可执行位,不需要 shebang
验:pnpm exec vitest run --project tooling scripts/tests/check-error-contract.test.mjs(LF 与 CRLF 各 8 通过)、
node scripts/check-error-contract.mjs(四条规则 0 处)、node scripts/check-docs.mjs(80 文件 / 28 ADR)
并入上游 PR eleckoi#16(b04d614)的 5 个提交:Pillow、工具结果 fork 鉴权、 CRLF 比对、浏览器重试与视口/动效模拟、安装完整性退出码。 理由:0.2.13 基线在 windows-latest 上本来就红这 7 条;不并它,本 PR 的 Windows CI 无法反映错误处理改动的真实结果。等 eleckoi#16 落到上游后这些提交是 no-op。 验:真机(CAMEL-PC,Node 24.20.0)上 unit / ui / host / tooling 与关键 integration 用例共 258 文件 / 1907 用例全过;闸测试在 CRLF 检出下 8/8。
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- generate-error-contract-doc:读 details.ts 的联合声明前折叠行尾。Windows 检出是 CRLF, 正则里的 `\n\n` 匹配不到,pnpm build 在 windows-latest 上红在 check:error-contract-doc - generate-error-codes:产物比较前折叠行尾。干净的 Windows 检出会被判成「过期」 - 用 CRLF 版 details.ts / errorCodes.generated.ts / docs/error-contract.md 复现并验证修复 验:两个 --check 在 LF 与 CRLF 下各通过;pnpm typecheck 通过。
- tests/integration/dsh-transcript-import.test.js:建库 + 迁移 + 杀软扫描偶发超过默认 5s - packages/host/product-data/tests/persona-media.test.ts:图片外置用例同样偶发 - 只把超时放宽到 30s,断言不变 验:下一次 windows-latest CI 验证(此前 2260 用例里只偶发这 1 条超时)。
Windows 上裸绝对路径(D:\…)不是合法的 ESM 说明符,check:error-codes 在 windows-latest 上报 ERR_UNSUPPORTED_ESM_URL_SCHEME: Received protocol 'd:'。改用 pathToFileURL(...).href 后两个平台一致。 验:node scripts/generate-error-codes.mjs --check 通过(30 codes)。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
这套设计回答一个问题:一个失败从产生到用户看见,要过六条边界(领域 → Remote 出口 → 子进程 / 运行时插件 → IPC → 渲染层),怎么让它不丢语义,同时让三个读者各读到该读的东西——用户读一句话,开发者读一行日志,程序读一个码。
今天的问题正好相反:失败在第二条边界上就只剩一句话。用户看到开发者写的中文句,程序拿不到码、只能比字符串,日志里没有能回去查的东西。所以这套设计做四件事,外加一个执行者。
一、用户可见文案只有一个来源,而且不在抛出点。
抛出点保留原中文句,但它是开发者诊断——进日志,可以带 id 和路径。用户读的那句话由渲染层按码给:
好处有三个:同一个失败在聊天窗口和设置面板不会出现两种说法;改文案只改一处;「用户读到开发者诊断」这种事故在结构上不会再发生。这是对仓库「文案就地写中文」惯例的明确例外。
二、码表是内部枚举,不是对外契约。
29 条码 + 一个兜底码,形态
eleckoi/<域>/<原因>:加码自由,改名要一次全局替换;不承诺第三方或 Android 按码实现——实测生产代码里按eleckoi/*分支的地方是 0 处,没有消费者就不该有承诺。兜底码的规则是不猜:认不出的失败折成eleckoi/internal,把原码记进details.what。猜成某个域码,等于给调用方一个假的可判别失败。三、严重度是场景的属性,不是失败的属性。
同一个
io/failed,在读一张角色卡时是功能内失败,在启动读配置时是整个应用的失败。所以通道默认由码给(5 条应用级码:plugin/load-failed、db/schema-mismatch、storage/corrupt、sender/untrusted、lifecycle/disposed),调用点只能上调——解析函数的参数里根本没有下调方向,「只能上调」是结构保证。两条通道:inline就地失败面 + 重试;app对话框 / 窗口级提示。四、日志一行读完,不用回查码表。
一行是「动作键 + 码 + 开发者诊断 + 参数化详情 + 时间」:
动作键是有语义的操作名,不是随机 id。启动写一行、正常退出写一行——有 start 没 end,就是崩溃或强杀。日志自己写不了就降级 stdout + 一条 stderr,不让日志成为应用起不来的原因。
两条硬约束撑住上面这些:失败按形状识别,不用
instanceof(dsh-product-data把契约包内联了,跨 bundle 副本原型不同,instanceof必假,码与详情会整条丢掉);过边界不重新包信封,客户端拆 Remote 结果时直接throw result.error,保留官方失败实例。明确不做:冻结公开契约、按失败原因决定恢复策略、给副窗口补 toast、引入 lint / 格式化链、全面 Result 化、运行时错误服务。闸是这套设计的执行者。
check:error-contract四条规则:禁止丢码、契约违例必须带issues、空 catch 必须具名(failureAlreadyPublished/rejectionIsAwaitedElsewhere/bestEffort)、不许裸抛。四条都是绝对值,不建豁免账本;只看代码位置,作者脚本示例里出现的throw new Error不会被误判。这份 diff 里是什么。 373 个文件:55 个是新地基(契约、折叠、闸、失败面组件、日志);180 个是把裸抛换成带码抛出——同一句式替换,其中 127 个文件只做这一件事;77 个是接线(preload、IPC、窗口失败面、日志挂点),用户可见行为的变化在最后这一桶;43 个是既有测试的断言更新;18 个是闸 / 生成物 / 文档 / 配置。其中 8 个文件不是本线的改动,是并入的上游 PR eleckoi#16(Windows CI 基线修复):0.2.13 在
windows-latest上本来就红 7 条,不并它,本 PR 的 Windows CI 看不出错误处理改动的真实结果。想读的话按这个顺序:packages/shared/src/contracts/errors/→ 任选一条边界(ipc.ts+desktopIpcHandler.ts)→apps/web/src/utils/failureText.js。Evidence
闸:四条规则从基线收到绝对值。 同一脚本分别跑基线
a5bb881(696 个源文件)与 tipab4e437(725 个):Windows 真机(并入 eleckoi#16 后,
C:\Users\15985\Desktop\code\ElecKoi-eh13,Node 24.20.0 / pnpm 10.32.1):unit10 文件 / 84 用例 ·ui174 / 1256 ·host63 / 497 ·tooling9 / 43 ·integration关键 2 文件 27 用例——共 258 文件 / 1907 用例,0 失败;闸测试在真实 CRLF 检出下 8/8。GUI 组(desktop/browser与应用窗口)在这台机器上起不来:Electron 44 启动即 APPCRASH0x80000003(最小窗口脚本、交互式计划任务、--no-sandbox都一样),与本次改动无关,故未纳入。Windows CI(
windows-latest):tip3ba241f——pnpm test326 文件 / 2260 用例全过;pnpm build(含四条契约规则与全部生成物校验)、Electron SQLite ABI、DSH 握手三步也全过,整条 19m17s。这一轮 CI 抓出并修掉 3 个只在 Windows 出现的问题:闸测试的 CRLF + shebang、契约文档生成器读details.ts的正则、码表生成器把D:\裸路径当 ESM 导入。用户看到的是按码的句子,不是开发者诊断。
error?.message当文案;主窗口写着页面加载失败:{error?.message}resolveFailure(...).text。renderer-failure-text-wiring.test.jsx喂DesktopError(lookupNotFound, '开发者诊断:找不到 x', { what: '角色卡' }),断言界面显示找不到角色卡。且不含「开发者诊断」;裸Error仍用它的 message应用级码真的走应用级通道。
channel只被当成 CSS class,storageCorrupt在设置面板里就地显示成开发者诊断failure-alert-escalation.test.jsx:storageCorrupt升级到窗口级对话框,lookup/not-found仍就地,无订阅者时退回就地兜底IPC 不再跨进程抛异常。
ipcMain.handle的 handler 抛错,跨进程只剩messageipc-failure.test.ts断言码与详情原样过结构化克隆全量门禁(
bash eh13-final-gates.sh,合并前 tipab4e437;并入的 eleckoi#16 提交另由上游 CI 与上面的 Windows 真机验证)两个环境红:
android-apk-resources(缺 Pillow,本机python3没有 pip)、updater-network(Windows autoUpdater 用例,Linux 平台不匹配——补上 X server 后仍红在同一断言)。Merge Danger
Door: one-way(对约定)/ two-way(对代码)
合并即把「抛码 + 渲染层出文案」定成基线:规则 4 现在默认判失败,新写的裸抛当场红。代码可以整体 revert,但 revert 会连带撤掉闸、生成的契约文档与 ADR 0040,回退成本高于一般重构。上游 PR 已开:#17(本分支 head 相同)。并进来的 eleckoi#16 是上游已开的那条 Windows CI 修复:本 PR 需要它,Windows CI 才能反映错误处理改动的真实结果;eleckoi#16 合入后这些提交自动变成 no-op。
Blast Radius: repo-wide
error.message或instanceof的外部代码要改读.code(仓库内已扫干净)。isDSHRemoteError标记:拿到 IPC 失败对象的 DSH 代码会把它当 RemoteError,这是有意为之;代价与退路写在packages/shared/src/contracts/errors/ipc.ts文件头。docs/error-contract.md、插件 API 参考。<p role="alert">直显(关键窗口先接,组件与接线已就绪)。请求维护者
这套写法只有在后续改动里被使用才算成立。合入后请按错误处理协作声明写新的失败,要点六条:
DesktopError/productFailure;message写开发者诊断(日志用),不是给用户看的句子;RemoteResult走unwrapOrFail或throw result.error,不写new Error(result.error.message);resolveFailure(...).text;宿主、领域、运行时插件不拼用户句子;failureAlreadyPublished/rejectionIsAwaitedElsewhere/bestEffort)或写非空处理体;codes.ts→details.ts→ 文案表 → 重生成 → 测试的顺序加;生成物不手改;pnpm check:error-contract:四条规则都是绝对值,不建豁免账本。哪条做不到或有异议,请在合并前说——合入后这几条由闸执行,不再是建议。