Uh oh!
There was an error while loading. Please reload this page.
refactor(plugin-chatbot): ApproveOutcome/RejectOutcome 改为从 spec 派生,id 归位到 reject 一侧 (#3783) - #3800
Conversation
… 归位到 reject 一侧 (#3783) `usePendingActions.ts` 里这两个类型是 spec approve/reject 响应的手写镜像,与 #3220 从同一文件清掉的 `PendingActionRow`/`PendingActionStatus` 同一失败类; 不同的是它们穿的是**本地名字**,所以 `check-spec-symbol-derivation.mjs`(按 spec 导出名被占用触发)对它们完全没有抓手 —— 换名手抄对名字型守卫天生隐形。 两者现在 re-export spec 的决策响应(`@objectstack/spec/api` 的 `ApproveAiPendingActionResponse` / `RejectAiPendingActionResponse`,也正是 `@objectstack/client` 的 `ai.pendingActions.approve()/.reject()` 用来标注返回值 的同一批 schema)。公开导出名不变,形状变三处: - `ApproveOutcome` 不再声明 `id`。approve 响应从来不带 `id`,`id` 在 reject 侧。 这是唯一一条不休眠的漂移:公开回调 `onDecided` 编译期承诺 `id: string`,运行期 给的是 `undefined`,编译器一声不响; - `status` 闭合:`'executed' | 'failed' | string` 与 `'rejected' | string` 都只是 `string`(与 `string` 的联合吸收字面量),现为 `'executed' | 'failed'` 与 `'rejected'`; - 去掉 `[k: string]: unknown`(objectstack#4075 机制:索引签名让任何结构比较恒答 "一致",给旧类型补 parity 测试也会从第一天就是绿的)。 **运行期行为零变更**,包括两处刻意保留的:非 2xx 时本地虚构的失败信封仍带 `id` (它不是 wire 响应,而是本地通知),以及 `decide()` 对 spec 词表之外的 status 仍 渲染成功 chip —— 后者的类型压力为零,因为 `status` 是从未解析的 `Record<string, unknown>` 上读出的 `string`,闭合枚举施加不了穷尽性检查。两者都新 补了测试钉住。`useHitlInChat` 剩下的消费侧容忍(该失败信封的契约、status 兜底 默认、未知 status 当成功)记入 #3790 交 maintainer 裁决。 守卫配套:`spec-symbol-batch6.test.ts` 补 `Assert<Equal<…>>` 钉子(该文件已进 `tsconfig.typetests.json` 执行面),另加两条反向钉 —— spec 仍导出被派生的两个名字、 两个本地名字仍不与 spec 撞名(后者正是名字型守卫看不见它们的前提)。
The latest updates on your projects. Learn more about Vercel for GitHub. |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
commented
Aug 8, 2026
✅ 验收通过(objectui 分片 PM,session_01GTRjn8xBqp75dk7kFupVRt)—— undraft + auto-merge。 git 实物核验( 采纳的核验更正:issue 正文「else 兜底继续对话」被 dev 证伪 —— changeset minor 依据成立:AGENTS.md fixed-group 策略 + Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#3783
packages/plugin-chatbot/src/usePendingActions.ts里手写的ApproveOutcome/RejectOutcome改为从@objectstack/spec派生。与 #3220 从同一文件清掉PendingActionRow/PendingActionStatus是同一失败类;不同之处在于这两个穿的是本地名字,所以scripts/check-spec-symbol-derivation.mjs(按"spec 导出名被本地声明占用"触发)对它们天生没有抓手 —— 换个名字手抄,对名字型守卫是隐形的。派生形态(#3220 范式,零新依赖)
@objectstack/spec已是本包运行时依赖(package.json,#3220 引入),且本次只用import type,编译期即擦除,不进 bundle:派生源实读(不凭 issue 转述),objectstack
origin/maind42a92fc6:packages/spec/src/api/protocol.zod.ts:1453-1462——ApproveAiPendingActionResponseSchema只有status: z.enum(['executed','failed'])/result?/error?;RejectAiPendingActionResponseSchema是status: z.literal('rejected')+id: z.string()。:1680-1681导出这两个类型名;packages/spec/src/api/index.ts:44export * from './protocol.zod',故@objectstack/spec/api子路径可达(装在本 worktree 的17.0.0-rc.5dist 里两个名字都实测在导出清单内)。packages/spec/src/contracts/ai-service.ts:298-301的IAIService.approvePendingAction返回Promise< { status: 'executed' | 'failed'; result?: unknown; error?: string } >—— 同样没有id。packages/client/src/index.ts:4109-4127,ai.pendingActions.approve()/.reject()正是用这两个 spec 类型标注返回值。也就是说本 PR 之后,objectui 与 objectstack client 对同一条 wire 的读法收敛到同一个声明。公开导出名保持
ApproveOutcome/RejectOutcome不变(src/index.tsx的已发布 API 面),只改形状。三处漂移的逐条处置
ApproveOutcome.id: string必填,而 approve 响应根本不带idid按 spec 归位到RejectOutcome(reject 响应确实带)outcome.id编译通过(运行期undefined),改后 TS2339status: 'executed' | 'failed' | string/'rejected' | string'executed' | 'failed'与'rejected'outcome.status === 'quarantined'编译通过,改后 TS2367[k: string]: unknown索引签名HasIndexSignature钉子在恢复手写形态后变红第 1 条是当下在跑的那条:公开回调
onDecided编译期承诺id: string,运行期给undefined,编译器全程沉默。顺手改正的第四处漂移(prose,不是类型)
被替换掉的注释写着 approve 失败是 HTTP 500。这与 spec 的注释(
status: 'failed'是带原因的 200 —— 审批成功、执行失败)相反,也与本文件自身设计自相矛盾:call()在!res.ok时抛错,若真是 500,AiPendingActionsInbox.tsx:204-206读out.error的那条 resolved-value 路径就永远不可达。新注释按 spec 写。这类"声称 canonical 的错注释"正是本守卫家族要消灭的东西(它会成为下一个 agent 的既定前提),所以在替换该声明时一并订正,而非留给下一张单。行为零变更论证(PM 判级边界指定的一节)
默认仅类型收紧。 逐处交代:
1.
useHitlInChat.ts的else兜底(未知 status)—— 保留status来自const status = (payload.status as string) ?? …,而payload是parseJson()返回的Record< string, unknown >—— 未经任何解析。它是string,不是闭合枚举,所以ApproveOutcome['status']收紧到两个字面量对这条分支施加不了穷尽性检查,tsc也不会把它判死。也就是说:不改行为完全能过 type-check(实测绿),不存在"exhaustiveness 逼死 else"的情形,无需停手。succeeded确实置 true,但buildContinuationPrompt对executed/rejected/failed之外的 status 返回undefined,if (prompt)不成立,continueConversation不被调用。所以后果只是一个乐观的 UI chip,不是"把假成功喂给模型"。已用测试钉住(keeps treating an unrecognised status as a success chip, without continuing)。safeParse(producer 说话),那会把 zod 拖进这个以 tiny bundle 为卖点的包 —— 属政策问题。已连同下面两条一起记入 观察类:useHitlInChat 决策结果的三处消费侧容忍(失败信封是本地虚构 / status 兜底默认 / 未知 status 当成功) #3790。2. 非 2xx 时本地虚构的失败信封 —— 保留,
id也保留这条路径上没有决策响应(服务端返回的是错误体),所以这个信封是本地造的通知。它自带
id是既有行为,外部消费者在这条路径上今天确实能读到,删掉即运行期回归 —— 所以留着。类型层面ApproveOutcome不再声明id(approve wire 从来没有),断言允许多余属性,故不需要改代码形状。已用测试钉住(still synthesizes the locally-built failure envelope, id included, on a non-2xx)。3.
useHitlInChat.ts:237的断言 —— 核对通过,无需改写payload as ApproveOutcome | RejectOutcome:payload是Record< string, unknown >,而收紧后的两个类型都是匿名对象类型(z.infer别名),带隐式索引签名,可比较关系成立,断言合法。实测tsc绿,未退化成as unknown as。sabotage 自证(方向:红。改动即钉子,所以是常规方向)
把派生临时改回手写漂移形态(
id: string+| string+ 索引签名),只跑 typetests 工程:八条红对应:
_ApproveIsSpec(270)、_RejectIsSpec(271)、_ApproveHasNoId(280,即那条在跑的缺陷)、_ApproveStatusNotString(288)、_RejectStatusNotString(291)、_ApproveVocabulary(293)、_RejectVocabulary(294)、_ApproveNoIndexSignature(298)。诚实标注:
_RejectNoIndexSignature(299)在 sabotage 下仍是绿的 —— 因为 reject 那份手抄本来就没有索引签名。这条钉子钉的是"以后别加",不是"当时有"。还原后
pnpm --filter @object-ui/plugin-chatbot type-check全绿(sabotage 未入 commit)。下游消费者探针(证明收紧穿透到已发布的 .d.ts,且没有擦成 any)
vite build后拿dist/index.d.ts当真实下游编译(探针文件已删除,未入 commit)。dist/usePendingActions.d.ts首行保留了import { ApproveAiPendingActionResponse, RejectAiPendingActionResponse } from '@objectstack/spec/api';,类型未擦除(objectstack#4171 是"spec 类型在 dist 里擦成 any"的先例,这里实测没有发生)。改后(期望:两条错,其余静默):
改前(把 origin/main
4028adfc3的手写形态原文抄进探针,同样两处读法):exit 0,零错。这就是本单要消灭的东西:一个 wire 从未兑现的承诺,和一个任何拼写都能通过的 status 比较。IsAny/IsUnknown探针、以及outcome.status === 'executed'、RejectOutcome.id这些合法读法在改后依然无错。消费半径清单(逐个确认)
全仓 grep
ApproveOutcome|RejectOutcome|onDecided(排除 node_modules)命中三个文件,全在本包内:packages/plugin-chatbot/src/usePendingActions.tsapprove()/reject()的返回标注packages/plugin-chatbot/src/useHitlInChat.tsas断言 +onDecided/ContinueContext类型面packages/plugin-chatbot/src/index.tsxpackages/plugin-chatbot/src/AiPendingActionsInbox.tsxout.status === 'executed'、out.errorerror);type-check 绿packages/app-shell/src/console/ai/AiChatPage.tsx:1637useHitlInChat({ messages, apiBase, continueConversation })onDecided、不读 outcome;options 接口未变。已单独跑pnpm --filter @object-ui/app-shell type-check(先 build 其全部依赖,避免 TS2307 假红)→ 绿apps/console/src/pages/system/AiPendingActionsPage.tsxAiPendingActionsInboxAiPendingActionsInboxProps不含任何 outcome 类型,零影响本仓没有读
outcome.id的外部消费者。仓外消费者若读了,那读的就是undefined,现在会在编译期显形 —— changeset 正文写了替代取值处(ContinueContext.pendingActionId)。守卫配套
src/__tests__/spec-symbol-batch6.test.ts的既有范式(Assert< Equal< … > >,由本包tsconfig.typetests.json编译,已在type-check执行面内)扩了一个 describe:the decision outcomes ARE the spec wire responses,含_ApproveIsSpec/_RejectIsSpec)与IsAny/IsUnknown探针;id键不存在 / status 不被string吸收 + 词表精确 / 无索引签名);node scripts/check-spec-symbol-derivation.mjs通过(1210 文件 / 4862 个 spec 名,13 declared dialects,3 untriaged collisions in 1 package —— 与 main 同数,本 PR 未新增也未消除该计数:两个本地名字不撞名,守卫按设计不计)。验证
新增测试(
useHitlInChat.test.tsx,4 条)与 fixture 处置:id)—— 顺带证明[HITL pa_42]提示语里的 id 来自 message 索引而非 payload,这也解释了那条假承诺为何能长期无人察觉;reject mock 保留id(按 spec 它就在那儿)。hands onDecided the approve payload verbatim — which carries no id:钉住 approve 侧运行期确实没有id。hands onDecided the reject payload verbatim — where id IS the wire:镜像。still synthesizes the locally-built failure envelope, id included, on a non-2xx:钉住行为零变更。keeps treating an unrecognised status as a success chip, without continuing:钉住刻意保留的else行为(含"不继续对话"这一被更正的事实)。changeset 档位
minor(@object-ui/plugin-chatbot)。依据 AGENTS.md §版本号策略:固定版本组的 major 跟随@objectstack,objectui 自身的破坏性变更一律标minor,在正文里写清 breaking 语义;scripts/check-changeset-no-major.mjs机械强制。#3220 当时标了major,但那正是该守卫因之诞生的四张单之一(17.x 期间会把 39 个包发成 18.0.0),所以此处不沿用它的档位,沿用的是它的处置范式。changeset 正文点名了收紧本身、以及outcome.id读法的替代取值处。关联
finding,不带pm:queue):useHitlInChat剩下的三处消费侧容忍 —— 失败信封不是 wire 响应、status 兜底默认、未知 status 当成功;含 A/B/C 三个选项与长期取舍分析,交 maintainer 裁决。Generated by Claude Code