a4phone 通过内置的 dsh-hook Cordis 插件让 DeepSeek Harness(DSH)同样具备完整的手机远程交互能力:任务完成通知、AI 提问手机作答、权限请求手机审批、以及直接注入桌面会话的远程续聊。a4p setup 检测到 DSH 环境时自动把插件挂载到 web profile,手机端无需额外订阅。本文章介绍 DSH 支持的事件覆盖、插件挂载机制、续聊实现与心跳检测。
1. DSH 支持概览
| Hook | DSH 事件 | 手机交互(外出模式) |
|---|---|---|
| 任务完成 | turn/end 且 reason.kind === 'completed' |
系统通知 + 手机推送(含 AI 最后输出) |
| AI 提问 | ask_user_question 工具调用(tools/execute 拦截) |
手机点选选项 / 文字自由作答 |
| 权限请求 | approval/request |
手机 Approve / Deny |
| 远程续聊 | 文件队列 ~/.a4phone/dsh-jobs/ |
手机发文字 → 注入桌面会话 → 回复推回手机 |
1.1 与 Claude Code / Codex 的差异
| 维度 | Claude Code / Codex | DSH |
|---|---|---|
| 接入方式 | Hook 命令(a4p hook) |
Cordis 插件(dsh-hook) |
| 事件来源 | 外部进程 Hook 协议 | DSH 内部会话事件流 |
| 远程续聊 | 另起 headless 进程 | 进程内 agent.followup 注入当前会话 |
| 会话锁 | Claude 独占锁 / Codex 写锁 | 无锁冲突 |
| 挂载方式 | 写 settings.json / config.toml | 写 cordis.patch.yml(热生效) |
| 支持版本 | 全部版本 | 插件挂载自 1.1.6,续聊自 1.2.0 |
2. 插件挂载机制
2.1 自动挂载
a4p setup 检测到 DSH 环境(~/.dsh/profiles/web 目录存在)时,把本包 dsh/ 目录下的插件以 insert 形式写入 web profile 的 cordis.patch.yml:
# ===== a4phone dsh-hook (auto-generated) =====
- insert:
- id: dsh-hook
name: "C:/ProgramMine/a4phone/dsh/lib/index.js"
# ===== end a4phone dsh-hook =====
- 幂等可重复执行:已挂载相同路径时自动跳过
- 若检测到旧版手动挂载(指向
C:\ProgramMine\dsh-hook的id: dsh-hook),自动替换为本包路径 cordis.patch.yml被 DSH 热监视(watchUserPatches),挂载即时生效,无需重启- 插件代码更新后建议重启
dsh web使新代码生效
2.2 卸载
a4p uninstall 会同时移除该挂载(marker 包裹的插件块 + 旧版手动挂载块)。
3. 插件架构
dsh-hook 插件监听 DSH 的 Cordis 事件,与 a4phone 复用同一套配置与推送模块:
cordis 插件] B --> C[session/event
会话事件流] B --> D[tools/execute
工具执行拦截] B --> E[approval/request
审批请求拦截] B --> F[续聊服务
resume-service] C --> G[任务完成通知
+ AI 最后输出] D --> H[提问推送手机] E --> I[审批推送手机] F --> J[文件队列
req / resp] G --> K[ntfy.sh] H --> K I --> K J --> K K --> L[手机] style A fill:#e8f4f8,stroke:#1a6b8a,stroke-width:2px style B fill:#ebdef0,stroke:#8e44ad,stroke-width:2px style C fill:#d6eaf8,stroke:#1a6b8a,stroke-width:2px style D fill:#d6eaf8,stroke:#1a6b8a,stroke-width:2px style E fill:#d6eaf8,stroke:#1a6b8a,stroke-width:2px style F fill:#fdebd0,stroke:#b7950b,stroke-width:2px style G fill:#d5f5e3,stroke:#27ae60,stroke-width:2px style H fill:#d5f5e3,stroke:#27ae60,stroke-width:2px style I fill:#d5f5e3,stroke:#27ae60,stroke-width:2px style J fill:#fadbd8,stroke:#c0392b,stroke-width:2px style K fill:#ffecd6,stroke:#e67e22,stroke-width:2px style L fill:#e8f4f8,stroke:#1a6b8a,stroke-width:2px
3.1 事件日志
插件把每个 hook 事件写入 ~/.a4phone/dsh-logs/,按事件分类:
| 日志文件 | 记录内容 |
|---|---|
task-complete.jsonl |
任务完成事件(会话、轮次、reasonKind) |
question-asked.jsonl |
提问工具调用(问题与选项) |
permission-request.jsonl |
审批请求与决策(approval/asked、approval/decided) |
4. DSH 远程续聊
4.1 与 Claude Code / Codex 续聊的本质区别
DSH 续聊不另起进程,而是让 dsh web 进程内的插件直接把手机消息注入桌面上正在运行的同一个会话——手机消息与 AI 回复都会实时出现在桌面端会话里,手机与桌面看到同一段对话,且不存在会话锁冲突。
4.2 文件队列协议
a4p listen] --> B[原子写入
req-<id>.json] B --> C[dsh web 进程
插件每秒扫描] C --> D[解析目标 agent] D --> E[agent.followup
注入用户消息] E --> F[await whenIdle
等待轮次结束] F --> G[提取最后
assistant 文本] G --> H[写 resp-<id>.json] H --> I[a4p 轮询取回] I --> J[推送回复到手机] C --> K[刷新心跳
dsh-heartbeat.json] style A fill:#fdebd0,stroke:#b7950b,stroke-width:2px style B fill:#fadbd8,stroke:#c0392b,stroke-width:2px style C fill:#ebdef0,stroke:#8e44ad,stroke-width:2px style D fill:#d6eaf8,stroke:#1a6b8a,stroke-width:2px style E fill:#d5f5e3,stroke:#27ae60,stroke-width:2px style F fill:#d6eaf8,stroke:#1a6b8a,stroke-width:2px style G fill:#d5f5e3,stroke:#27ae60,stroke-width:2px style H fill:#fadbd8,stroke:#c0392b,stroke-width:2px style I fill:#fdebd0,stroke:#b7950b,stroke-width:2px style J fill:#e8f4f8,stroke:#1a6b8a,stroke-width:2px style K fill:#fdebd0,stroke:#b7950b,stroke-width:2px
协议要点:
- 记录最近会话:插件在每次顶层会话
turn/end(completed)时写入~/.a4phone/last.json(agent: "DSH"),与 Claude Code / Codex 的 Stop Hook 记录一致 - 请求:a4p 把手机文字原子写入
req-<id>.json(含目标 sessionId 与文本),插件每秒扫描 - 目标解析:优先使用请求指定的 sessionId(仍 live 的会话),否则兜底最近一个顶层会话
- 注入:
agent.followup()把文字作为普通user/message写入桌面正在运行的会话,await agent.whenIdle()等待轮次结束 - 回复提取:从会话事件日志中提取 followup 之后最后一条非空 assistant 文本作为回复
- 响应:写
resp-<id>.json,a4p 轮询取回并推回手机 - at-most-once:请求文件先删除再处理,进程崩溃不会导致同一条手机消息重复注入
4.3 心跳检测
插件每次轮询刷新 ~/.a4phone/dsh-heartbeat.json({ alive: true, ts })。a4p 据此快速判断 dsh web 是否在运行:
- 心跳超过 10 秒视为插件未运行,续聊请求快速失败并给出提示,而不是干等 30 分钟超时
- 续聊轮询期间周期复查心跳,插件中途死亡立即中断并提示
4.4 续聊前提
dsh web正在运行,且已挂载新版 dsh-hook 插件(a4p setup自动挂载,插件代码更新后需重启dsh web)- 未检测到
dsh web时续聊快速失败并给出提示
5. 任务完成推送去重
续聊轮次的"任务完成"推送已自动去重:插件检测到会话正在被续聊服务驱动(inflight),跳过重复通知,回复统一由 a4p 推回手机。
6. 常见问题
问:DSH 插件挂载后需要重启 DSH 吗?
答:不需要。cordis.patch.yml 被 DSH 热监视,挂载即时生效;但插件代码更新后建议重启 dsh web 使新代码生效。
问:DSH 模式切换(a4p out / a4p home)需要重启 DSH 吗?
答:不需要。插件在每次 tools/execute、approval/request 事件时实时读取 ~/.a4phone/mode.json 判断当前模式,与 Claude Code / Codex 的 Hook 机制一致,切换即时生效。
问:手机端需要额外订阅 DSH 的话题吗?
答:不需要。DSH 插件复用与 Claude Code / Codex 相同的 a4phone 话题与模式,手机端同一订阅即可收到 DSH 的推送。
问:DSH 续聊会另起进程吗?
答:不会。DSH 续聊直接发生在桌面正在运行的会话上,手机与桌面看到同一段对话,且无会话锁冲突。
举手提问