a4phone 在 AI 助手调用提问工具时,把问题推送到手机:外出模式下手机端显示选项按钮可直接点选,也可以向响应话题发送文字自由作答,答案经 ntfy 回传后注入会话让 AI 继续任务。本文章介绍提问交互的触发、选项展示规则、答案注入机制与回退策略。
1. 功能概述
| 维度 | 说明 |
|---|---|
| 触发工具 | Claude Code:AskUserQuestion;Codex:request_user_input;DSH:ask_user_question |
| 拦截方式 | Claude Code / Codex:PreToolUse Hook;DSH:tools/execute 事件拦截 |
| 交互方式 | 手机按钮点选(选项 ≤ 3)/ 编号文字作答(选项 > 3)/ 自由文本 |
| 生效模式 | 仅外出模式(a4p out),终端优先模式直接走终端 |
| 超时回退 | 手机超时未回复自动回退终端原生提问 |
1.1 事件流程
AskUserQuestion] --> B{当前模式} B -->|终端优先| C[终端原生提问
不干预] B -->|外出模式| D[推送提问到手机] D --> E{选项数量} E -->|不超过 3 个| F[按钮点选] E -->|超过 3 个| G[编号列表
回复数字] F --> H[响应话题回传
-response] G --> H D --> I[自由文本作答] I --> H H --> J[答案注入会话] J --> K[AI 采用答案继续] H -->|超时| C style A fill:#ffecd6,stroke:#e67e22,stroke-width:2px style B fill:#ebdef0,stroke:#8e44ad,stroke-width:2px style C fill:#fdebd0,stroke:#b7950b,stroke-width:2px style D fill:#e8f4f8,stroke:#1a6b8a,stroke-width:2px style E fill:#ebdef0,stroke:#8e44ad,stroke-width:2px style F fill:#d5f5e3,stroke:#27ae60,stroke-width:2px style G fill:#fdebd0,stroke:#b7950b,stroke-width:2px style H fill:#fadbd8,stroke:#c0392b,stroke-width:2px style I fill:#e8f4f8,stroke:#1a6b8a,stroke-width:2px style J fill:#d5f5e3,stroke:#27ae60,stroke-width:2px style K fill:#ffecd6,stroke:#e67e22,stroke-width:2px
2. 提问推送内容
推送标题为 {Agent}: {问题标题}(无标题时显示 Question),正文包含:
- 问题全文
- 选项编号列表(
1. 选项A、2. 选项B...) - 作答提示(按钮模式提示可发送文字,编号模式提示回复数字)
- 浏览器兜底链接(未订阅响应话题时可网页直接回复)
Claude Code: 部署方案选择
你希望如何部署这个服务?
1. 使用 Docker 部署
2. 使用 systemd 直接运行
3. 仅本地测试不部署
(也可直接向话题 a4p-xxxx-response 发送文字作答)
(如未订阅响应话题,可浏览器打开 https://ntfy.sh/a4p-xxxx-response 直接回复)
3. 选项展示与作答规则
3.1 按钮模式(选项 ≤ 3)
ntfy 单条推送最多支持 3 个 action 按钮(超过会返回 HTTP 400 导致推送失败),因此选项不超过 3 个时每个选项渲染为一个按钮,点击即回传对应选项 label。
3.2 编号模式(选项 > 3)
选项超过 3 个时自动降级为文本编号列表,手机回复对应编号(如「3」)即可选中选项;回复的纯数字会被映射回选项 label 注入会话。
编号回复依赖 a4phone 1.4.1 及以上版本——1.4.1 修复了纯数字自由文本(如「4」)被
JSON.parse误判为 JSON 对象而丢失的问题。
3.3 自由文本作答
除点选外,还可以直接向响应话题(主话题加 -response 后缀,如 a4p-xxxx-response)发送任意文字作为答案,不受选项限制。按钮模式与编号模式均支持自由文本。
4. 答案注入机制
手机答案经响应话题回传后,a4phone 按 Agent 不同构造 Hook 输出注入会话:
| Agent | 注入方式 | 说明 |
|---|---|---|
| Claude Code | updatedInput.answers 改写工具输入 |
工具自带跳过用户交互的实现,模型直接读取注入的答案 |
| Codex | permissionDecision: "deny" + permissionDecisionReason |
阻断 request_user_input 工具调用,把答案写进阻断原因,让模型看到"用户已作答"后直接采用答案继续 |
| DSH | 返回 { answers: [{ id, selected, custom? }] } 工具输出契约 |
替换原生提问,模型直接拿到手机答案 |
Codex 的提问工具
request_user_input的 handler 会忽略输入里的 answers,无法像 Claude Code 那样用updatedInput注入;a4phone 因此采用"阻断工具调用 + 把答案写进阻断原因"的方式,这是两种事件 schema 差异导致的必要适配。
5. 超时与失败回退
| 场景 | 处理方式 |
|---|---|
| 手机超时未回复 | 等待 timeout 秒(默认 60,见配置说明)后回退终端原生提问 |
| 推送失败(网络异常等) | 直接回退终端原生提问,不阻塞 AI |
| 无选项的提问 | 无法手机作答,跳过推送,回退终端 |
| 终端优先模式 | 不拦截,直接走终端提问 |
举手提问