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 事件流程

graph LR A[AI 调用提问工具
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. 选项A2. 选项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
无选项的提问 无法手机作答,跳过推送,回退终端
终端优先模式 不拦截,直接走终端提问