# Ask UI

> 向用户提问的时候、调用 `AskUserQuestion` 时都使用本 skill 来替换提问方式

- Skill: `lazygophers/ask-ui` (Agent Skill, multi-file: 17 files)
- Install (CLI): `npx skillmds@latest add lazygophers/ask-ui`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lazygophers/ask-ui/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: lazygophers (https://skillmd.com/u/lazygophers)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lazygophers/ask-ui

---


# Ask UI

把 Ask UI 当作展示与持久化适配器使用。问题的生成和推理仍留在调用方工作流里。

## 总览：三条路径，只走一条

| 路径 | 何时走 | 答案怎么回来 |
|---|---|---|
| **标准路径 `ask`** | 默认。后台运行，阻塞到用户提交 | 进程退出，harness 推完成通知，读 stdout 文件 |
| **故障恢复 `resume`** | 仅「故障速查」表列出的情况 | `status: "submitted"` 里就是完整答案 |
| **手动回退 `create`** | `ask` 确实用不了的最后手段 | 用户回复「已提交」后跑 `resume` |

选错路径的代价都在后文用 🔴 标出。先读总览再往下走，不要跳进某条路径的细节里出不来。

## 永不变量

无论走到哪条路径，以下红线一次都不能破：

- 🔴 绝不要求用户回复「已提交」来推进 `ask`——后台任务的完成通知就是唤醒信号。
- 🔴 绝不覆盖已提交的问题或答案——更正和补充再发起一次新的 `ask`。
- 🔴 绝不用 `nohup ... &` 之类手写后台——用 harness 自己的后台机制。
- 🔴 绝不 `sleep` 轮询、催用户。
- 🔴 绝不从 harness 任务输出里解析答案，也绝不手拼 `.ask-ui/` 下的文件路径——答案读 `<run>.stdout.json`，或跑 `resume`。

## 判断是否使用 UI

🔴 **CHECKPOINT**：当前一批问题里包含至少两个用户当下就能回答的独立问题时，必须使用 UI。答案依赖前一题的问题写成同一次提问里的条件题（`showWhen`，见第 3 步）；只有需要 Agent 拿到答案后重新推理才能提出的问题，才留到下一次 `ask`。只有一个问题时直接在对话里问；唯一的例外是更正或补充已提交的答案——哪怕只有一题也再发起一次 `ask`，因为对话里口头确认不落盘，后续流程读不到它。

对 `grill-me`、`grill-with-docs`、头脑风暴，或其他确认与问题收集类工作流，只要一次超过两个问题，一律使用 UI。

### 回退顺序

按顺序往下退，退到能用的第一档为止：

1. **Ask UI**（本 skill）——默认。
2. **`AskUserQuestion`**——本 skill 用不了时改用它。用不了的情形有两种：harness 没有可执行命令的工具（Bash 或等价物），或服务/浏览器确实起不动。
3. **对话里的编号文本问题**——只有在 `AskUserQuestion` 也拿不到时才允许。

退档时说清真实原因，别把「没有 Bash 工具」写成「服务起不来」——前者换任何语言重写都没用，后者才是环境问题。用 `ToolSearch` 确认过工具确实不存在，再下结论。

## 标准路径：`ask` 八步

1. 把包含本 `SKILL.md` 的目录解析为 `ASK_UI_SKILL_DIR`。
2. 创建 JSON 前先读两份文件：[references/questionset.schema.json](references/questionset.schema.json) 是字段清单本身（JSON Schema 2020-12，每个字段带中文说明），[references/schema.md](references/schema.md) 讲 schema 表达不了的部分——跨字段硬规则、页面实际行为、为什么这样写。[references/example-question-set.json](references/example-question-set.json) 是一份可直接复制改字段的完整起手模板（单选 / 多选 / 自由文本各一题，带推荐答案和上下文字段）。答案的结构见 [references/answerset.schema.json](references/answerset.schema.json)。
3. 创建 QuestionSet JSON 文件。每次 `ask` 都是一次独立提问，id 由 CLI 自动生成。旧版字段（`sessionId` / `roundNumber` / `basedOnRound` / `sessionTitle` / `sessionSummary` / `sessionBackground`）已全部移除，写了会报错指路。
   每道题必写两个字段：`type`（`single` / `multiple` / `text`，**没有默认值，漏写报错**）和 `text`（问题正文，问题本身和描述都写在这里）。选项一律是 JSON 对象 `{"text":"…","description":"…","recommended":true,"reason":"…"}`，**不接受字符串**；`reason` 只能写在 `recommended: true` 的选项上。
   允许留空的题必须显式写 `"required": false`——`required` 默认 `true`，漏写就是必填，页面挂「必填」徽标、留空挡提交。**别在 `text` 里写「（可留空）」代替这个字段**：文案和徽标对不上，用户只能被迫编一句。「还有别的补充吗」「其他备注」「可选参数」这类题一律 `type: "text"` + `"required": false`。
   一并写上上下文字段，让用户不看对话就能判断在问什么：`projectName` / `title` / `summary` / `background`（左栏「本次背景」，右上角有独立按钮可放大）/ `purpose`，需要单独交代前情的题写题级 `background`。
   选择题没有「其他」选项。预设选项之外的答案由每题的补充说明承载，所以选项只列真正互斥的几种，不要凑「其他」。选择题至少要 2 个选项，脚本会直接报错 `第 X 题是选择题，至少要有两个选项` 并退出——只有一个候选的确认题改成 `type: "text"`，或者干脆在对话里问。
   有依赖关系的问题写成同一次提问里的**条件题**：`"showWhen": {"questionId":"q1","optionIds":["a"]}` 让这题只在 `q1` 选了 `a` 时才出现，用户选完当场出现或消失。`showWhen` 只能指向排在前面的题，分支树靠链式依赖搭；文本题作触发源时用 `answered` / `contains` / `matches`。隐藏题不校验必填、也不进 `answers.json`（id 列在 `hiddenQuestionIds`）。完整规则见 [references/schema.md](references/schema.md) 的「条件题（分支）」——`showWhen` 的三条跨字段硬规则（指向前面的题、匹配方式配得上题型、选项 id 真实存在）schema 拦不住，只有运行时会报错。
   `background`、题目的 `text` 和 `background` 支持 **Markdown（GFM：标题、粗体、行内代码、代码块、列表、链接、引用、表格）+ Mermaid**；选项的 `description` 只支持 Markdown。流程、时序、架构这类讲不清的东西写成 ` ```mermaid ` 代码块，会渲染成跟随主题的图。表格、图表和代码块在页面上都能点击放大、缩放拖拽。代码块在围栏上标语言（` ```ts `、` ```sql `）就会按语言高亮。**嵌套规则**：要展示一段本身含 ``` 围栏的 markdown（或代码里含 ```）时，外层围栏必须用四反引号 ` ```` `——三反引号会被内层第一个 ``` 提前闭合，后面的内容漏成正文，页面上出现裸 ``` 字符。
4. **在后台运行命令，只把 stdout 重定向到文件，stderr 留在控制台**：

   ```text
   node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs ask --input <questions.json> > <run>.stdout.json
   ```

   用 harness 的后台机制启动（Claude Code 里是 Bash 工具的 `run_in_background: true`）。stdout 是结果 JSON，必须落文件；stderr 是给人看的进度行（URL、`ask-ui-id`），留在控制台用户当场就能看到。

5. **把页面 URL 复述到回复第一行。**stderr 启动时立刻打出 `Ask UI ready at <url>` 和 `ask-ui-id: <id>`，用 `TaskOutput` 读一次后台任务输出取这两行写进回复。浏览器是脚本自动打开的，但它可能没弹出来（无 GUI、默认浏览器没配、窗口被挡），URL 摆出来用户就能自己打开。读一次就够，读不到就照常结束本轮，不要 `sleep` 重试。
6. 🛑 **STOP：输出 URL 后立刻结束本轮，什么都不用等。**`ask` 没有超时，会一直阻塞到用户提交；用户提交后进程退出，harness 主动把任务完成通知推给你，那就是唤醒信号。
7. 收到完成通知后，直接读 `<run>.stdout.json`——它是一整行 JSON，解析后继续原工作流。
8. 若还需要更多独立问题，再创建一份 QuestionSet JSON 并再次调用 `ask`——每次 `ask` 天然独立，互不干扰。

### 为什么 stdout 必须重定向

不重定向时，后台任务的 stdout 和 stderr 会混进 harness 的同一个任务输出，混在一起的内容 `JSON.parse` 必然失败——这是过去要人工 `resume` 兜底的唯一原因。把 stdout 单独 `>` 到文件后，结果 JSON 就是纯净的一行，任务输出里只剩 stderr 的进度行（URL、`ask-ui-id`、`ask-ui-submitted`），既能给人看又不会污染解析。

### 服务与浏览器生命周期

- 每次 `ask` 都会打开浏览器：页面在提交后自行关闭，所以下一次提问必须重新打开。常驻服务和端口在多次提问间复用。
- 常驻服务不需要手动清理，它自己管进退——四条退出规则见「故障速查」表里「表单挂了很久没人答」那一行。
- 仅当浏览器打开由外部单独管理时才用 `--no-open`。仅当必须固定 localhost 端口时才用 `--port <number>`。

## 故障速查：出什么事，做什么

```text
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs resume --id <askId>
```

🔴 只有下表列出的情况才需要 `resume`。正常路径永远是读 `<run>.stdout.json`，不要把 `resume` 当常规动作。

`askId` 从任务输出（stderr）里的 `ask-ui-id: <id>` 标记取；那里还有一行 `ask-ui-submitted: <id>`，是提交完成的备用信号。`resume` 返回 `status: "submitted"` 时其中就是完整答案。

| 触发条件 | 一线修复 | 仍失败的兜底 |
|---|---|---|
| 任务秒退，任务输出里连 `ask-ui-id` 标记都没有 | QuestionSet JSON 非法，提问根本没建起来：读任务输出里的报错（一次列出全部问题）改 JSON 重跑 `ask` | 🔴 不要跑 `resume`——没有数据可恢复，不带 `--id` 的 `resume` 只会捞出别的任务的旧提问 |
| 后台任务被杀、崩溃，或退出码非 0（任务输出有 `ask-ui-id`） | 取 `askId` 后跑 `resume` | 跑不带 `--id` 的 `resume`，按 `title` / `summary` 筛出讲当前任务的候选，取 `submittedAt` 最新的一条 |
| `<run>.stdout.json` 为空或不是合法 JSON | 同上，用 `resume` 重取结果 | 数据确实不存在时据实说明答案已丢，用同一批问题重新 `ask` |
| 换了新的 Agent 会话，拿不到原来的后台任务 | 从对话里最近的 `ask-ui-id` 标记取 id 后 `resume` | 标记也丢了就跑不带 `--id` 的 `resume` 列候选 |
| `resume` 返回 `{"status":"waiting"}` | 用户还没提交：什么都不做，当场结束本轮，继续等 harness 的完成通知 | 🔴 不重开表单、不重发问题、不催用户、不 `sleep` |
| 表单挂了很久没人答，担心服务一直占着 | 什么都不做：有提问等着答服务就该一直跑，全部答完后 30 分钟无访问自行退出，数据目录被删立即退出，`complete` / `cancel` 结束最后一个提问时当场停掉 | 🔴 不要手动 kill 进程；空闲时长要改就用 `ASK_UI_IDLE_TIMEOUT_MINUTES` |
| 本地浏览器连不上临时服务 | 走 `create` 分离式流程（见「手动回退与恢复」） | 仍连不上才退到 `AskUserQuestion` |
| harness 没有 Bash 或等价的执行工具 | 用 `ToolSearch` 确认工具确实不存在，退到 `AskUserQuestion` | `AskUserQuestion` 也拿不到时才用对话里的编号文本问题 |
| 唤醒适配器失败 | 保住答案，回到手动「已提交」流程 | 答案已落盘，用 `resume` 重取 |

## 手动回退与恢复

🔴 **CHECKPOINT：这是最后手段，只在 `ask` 确实用不了时才走**——它是唯一需要用户回复「已提交」的路径。`ask` 在后台运行**不算**用不了，那是标准路径，按上面等通知即可。

出现以下情况时走分离式（detached）流程：前台工具调用无法保持活跃、本地浏览器连不上临时服务、或需要恢复一个被中断的直连提问：

```text
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs create --input <questions.json>
```

解析返回的 JSON。在对话中同时给出它的 URL 和一个可见标记：

   ```text
   ask-ui-id: <askId>
   ```

告诉用户提交表单后只回复「已提交」。`create` 命令会启动或复用一个分离式 localhost 服务并立即返回。

当用户说「已提交」「提交好了」「答完了」时：

1. 从对话中最近一个 `ask-ui-id` 标记恢复 `askId`。
2. 运行：

   ```text
   node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs resume --id <askId>
   ```

3. 若结果为 `submitted`，用其中的问题和答案继续原工作流。
4. 若还需要更多独立问题，优先回到前台 `ask` 命令（新的 JSON、新的提问）。只有在仍然无法直连等待时才再次使用 `create`。
5. 若没有更多问题，运行：

   ```text
   node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs complete --id <askId>
   ```

若对话中拿不到该标记，运行不带 `--id` 的 `resume`。多个候选时它返回 `status: "ambiguous"` 和一份 `candidates` 列表（含 `askId` / `title` / `summary` / `workspace` / `submittedAt`）。数据目录默认就是当前工作目录下的 `.ask-ui`，所以候选都来自本工作区。按这个顺序筛：

1. 先按 `title` 和 `summary` 筛，只保留讲的是当前任务的候选。
2. 只剩一条就用它；剩多条时取 `submittedAt` 最新的那条。
3. 一条都对不上当前任务时，把各候选的 `askId`、`title`、`submittedAt` 列出来让用户选，不要挑一个最近的凑合用。

🔴 重复的「已提交」消息不得重复创建提问。只有在成功读到一个 `submitted` 的答案集之后，才可以发起新提问。

## 可选的主动唤醒

Ask UI 为 Claude Code 和 Codex App Server 支持可选的唤醒元数据。把它当增强项，不是必需项。

- 只有在用户同意后才启用自动唤醒。
- Claude Code 需要一个已记录的 session id。
- Codex 需要宿主提供的 thread id。绝不猜测 Codex thread id。
- 适配器失败时，保住答案并回到手动「已提交」流程。
- 直连 `ask` 模式永远不触发唤醒适配器，因为等待中的进程本身就是返回通道。

## 反模式：这些事一次都不要做

每次准备发命令或回话之前，对照一遍。

| 🔴 不要做 | 为什么 | 改成 |
|---|---|---|
| 用 `nohup ... &` 之类手写后台 | harness 收不到退出事件，整条链路退回人工追问 | 用 harness 自己的后台机制 |
| stdout 不重定向，直接从任务输出解析结果 | 两股输出混在一起，`JSON.parse` 必然失败 | `> <run>.stdout.json`，stderr 留在控制台 |
| 把 stderr 也重定向进文件 | URL 和 `ask-ui-id` 被埋进文件，用户看不到，页面没弹出来就没法自己打开 | 只重定向 stdout |
| `sleep` 轮询、催用户、让用户回复「已提交」 | 后台任务的完成通知就是唤醒信号，等它即可 | 启动后立刻结束本轮 |
| 从任务输出里找答案，或手拼 `.ask-ui/` 路径 | 任务输出只有 stderr 的进度行，答案不在那里 | 答案读 `<run>.stdout.json` 或跑 `resume`；任务输出只用来取 URL、`ask-ui-id` 和报错 |
| 给选择题加「其他」选项 | 预设外的答案由每题的补充说明承载 | 选项只列真正互斥的几种 |
| 写只有一个选项的选择题 | 脚本硬拒收，整批问题连会话都建不起来 | 补足第二个真实互斥的选项，或改成 `type: "text"` |
| 在 `text` 里写「（可留空）」却不写 `"required": false` | `required` 默认 `true`，页面照挂「必填」徽标、留空挡提交，文案和校验对不上 | 选填题显式写 `"required": false` |
| 漏写 `type`，或把选项写成字符串 | 两者都硬拒收，整批问题连会话都建不起来 | `type` 三选一必写；选项一律写成带 `text` 的 JSON 对象 |
| 用题级 `recommendedOptionIds` 标推荐 | 已经不认这个字段，脚本会报错 | 推荐写在选项里：`"recommended": true` 配 `"reason"` |
| 让 `showWhen` 指向排在后面的题 | 顺序即依赖序，向后引用会被硬拒收 | 把触发题排到前面 |
| 因为「问题有依赖」就拆成多次 ask | 每次都要重开浏览器、Agent 也要多醒一次 | 同一次提问里用 `showWhen` 做分支 |
| 覆盖已提交的问题或答案 | `answers.json` 提交后不可变 | 更正和补充再发起一次 `ask` |
| 把「没有 Bash 工具」说成「服务起不来」 | 归因错了，用户会去修一个不存在的环境问题 | 说清是工具缺失还是服务故障 |
| 猜 Codex thread id | 猜错会把唤醒发给别的会话 | thread id 只能由宿主提供，拿不到就走手动流程 |

## 常用命令

```text
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs ask --input <questions.json>    # 标准路径
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs create --input <questions.json> # 手动回退
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs resume --id <askId>             # 故障恢复
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs status --id <askId>             # 查提问状态
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs serve                           # 常驻服务（ask/create 自动管理，一般不单跑）
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs complete --id <askId>           # 正常结束提问
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs cancel --id <askId>             # 作废提问（问题问错了、任务取消）
node <ASK_UI_SKILL_DIR>/scripts/self-test.mjs                              # 自检，改完 skill 或排查环境时跑
```

