Skip to content

fix(ask): 文字作答时在 stderr 提示答案去向,消除静默的空 stdout#435

Open
lRoccoon wants to merge 1 commit into
deepcoldy:masterfrom
lRoccoon:fix/ask-custom-reply-silent-stdout
Open

fix(ask): 文字作答时在 stderr 提示答案去向,消除静默的空 stdout#435
lRoccoon wants to merge 1 commit into
deepcoldy:masterfrom
lRoccoon:fix/ask-custom-reply-silent-stdout

Conversation

@lRoccoon

Copy link
Copy Markdown

Fixes #434

改了什么

用户在话题里打字作答(而非点选按钮)时,botmux ask(非 --json)的 stdout 落到空字符串、exit code 为 0、stderr 一片安静,与「多选/多问」的降级空值无法区分。调用方既不知道用户其实已作答,也不知道原文在 comment 里,shell 分支会静默走进兜底。

本 PR 不改 stdout、不改 exit code,只在检测到文字作答时往 stderr 打一行提示:

  • src/core/ask-types.ts — 新增纯函数 isCustomReply(),与既有 toLegacySelected 同款风格,判定「各问均无选中项 + comment 非 null」
  • src/cli.tscmdAsk 的非 JSON 分支调用它,console.error 指明用 --jsoncomment 取回原文
  • test/ask-types-shape.test.ts — 补 5 条用例

为什么这么改

三点前提先确认清楚,避免改错地方:

  1. daemon 把 pending ask 期间的话题消息吞成 custom reply 是有意设计daemon.ts 注释:「待回答 ask 会把它当答案吞掉」),不该动。
  2. submitCustomReplyanswers 各问为空数组、toLegacySelected 因此返回 null,同样是设计test/ask-types-shape.test.ts 已有用例锁定。
  3. ask-types.ts 注释写明 comment 回落是 hook adapter formatAnswer 的职责,不是 cmdAsk 的。

所以真正的缺口只在可观测性:answered + 空 stdout + exit 0 + 静默 stderr,让文字作答成为一个不可观测的降级。

为什么不把 comment 回落进 stdoutcomment 可以是多行的(实测回复含换行与空行),而 ask 的 stdout 约定是「一行一个 option key」,供 $(...) 与字符串比较使用。灌进去会撕碎该约定,且失败得比现状更隐蔽。故只走 stderr。

影响面

  • isCustomReply() 为新增导出,无既有调用方,无副作用
  • cmdAsk 仅在文字作答分支多一行 stderr;stdout 与 exit code 零变更,现有脚本不受影响
  • 未触及 CLI adapter、backend、IM 层;ask-hook/*formatAnswer 回落路径未改动
  • --json 输出结构不变

实际测试验证

$ ./node_modules/.bin/tsc --noEmit
TSC_EXIT=0

$ ./node_modules/.bin/vitest run --project unit test/ask-types-shape.test.ts
 ✓ unit  test/ask-types-shape.test.ts (9 tests) 4ms
 Test Files  1 passed (1)
      Tests  9 passed (9)

全部 ask 相关测试文件(ask-types-shape / ask-args / ask-broker / ask-api / ask-card / ask-hook-claude / ask-hook-codex / ask-hook-opencode / cmd-hook):

 Test Files  9 passed (9)
      Tests  185 passed (185)

全量单测回归对比(本机 node-pty 的 gyp 编译失败,需 pnpm install --ignore-scripts,故有一批与本改动无关的预存失败;用 git stash 做了基线对照):

Test Files Tests
基线(未改动) 76 failed / 397 passed 71 failed / 6791 passed
本 PR 76 failed / 397 passed 71 failed / 6796 passed

失败数完全一致,通过数 +5,正是本 PR 新增的 5 条用例。

未做 live daemon 验证:本机 node-pty 编译不过,且 pnpm switch:here 会抢占全局 botmux 指向(当前会话本身跑在 botmux 上)。鉴于改动只新增一行 stderr、stdout 与 exit code 零变更,判断单测 + typecheck 已足够覆盖。真实载荷则来自线上实测——{"selected":null,"answers":[[]],"comment":"我发个多行消息试试\n这是第二行\n\n这是第四行","timedOut":false},测试用例直接采用了这条真实数据的形状。

顺带一提

ask 命令在文档站 14 页与 CONTRIBUTING.md 的 agent-facing 子命令表里均无记载,使用者无从得知 --json 是取回 custom reply 的唯一途径。如果需要,我可以另开一个纯文档 PR 补上。

用户在话题里打字作答(而非点选按钮)时,broker 走 submitCustomReply
settle 成 `answers` 各问为空数组、`comment` 携带原文。此时
toLegacySelected 按设计返回 null,非 JSON 模式的 stdout 落到空字符串,
exit code 仍为 0。

问题在于这个空字符串与「多选/多问」的降级空值完全一致,stderr 也没有
任何线索:调用方无法区分「用户其实作答了、原文在 comment 里」和「用户
选了多项所以拿不到单一 key」,shell 分支会静默走进兜底。ask 的存在意义
是把决策引到人,stdout 是它唯一的契约面,这个静默降级正好落在契约上。

comment 可能含换行(用户可以发多行消息),直接回落进 stdout 会撕碎
「一行一个 option key」的约定,因此不改 stdout。本提交只:

- core/ask-types.ts 新增纯函数 isCustomReply(),与既有 toLegacySelected
  同款风格,判定「无任何选中项 + comment 非 null」
- cli.ts 的 cmdAsk 在该分支多打一行 stderr,指明用 `--json` 读 comment
- ask-types-shape.test.ts 补 5 条用例覆盖文字作答/多问未选/按钮路径/
  带附注的按钮路径/非 answered 结果

stdout 与 exit code 零变更,现有脚本不受影响。

影响面:新增函数无调用方副作用;仅 cmdAsk 非 JSON 分支多一行 stderr。
不触及 CLI adapter、backend、IM 层;ask-hook/* 的 formatAnswer 回落路径
未改动。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ask: 用户以文字作答时 stdout 静默返回空串且 exit 0,与多选/多问的降级值无法区分

1 participant