1. 从 session.prompt 的图片数组到语音输入:DSH Mobile 的下一步
DSH Mobile 下一步要补的不是一个孤立的话筒按钮,而是把语音输入接进session.prompt的内容链路。电脑端 Harness 继续负责 Agent 循环、工具执行和插件运行,模型 Key 统一去 TaoToken 官网(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=voice_intro)获取,Base URL 填https://taotoken.net/api。手机端要做的是:拿到语音、转成文本、可选附带图片引用、调用session.prompt、处理断线恢复,并把模型请求日志留在电脑侧可查。
session.prompt已经支持图片内容数组,这件事对移动端交互开发者很有参考价值:prompt 的 content 并不是一段死字符串,而是可以承载多模态片段的结构。语音最稳妥的接入方式不是把音频硬塞进模型,而是先在客户端或电脑侧完成 ASR,把识别文本作为text片段发送;如果用户同时拍了一张报错截图,再把图片引用作为image片段追加进去。这样既不破坏现有 RPC 结构,也能让 Harness 继续用同一套模型供应商配置。
本文的可复现产出有两份:一份语音输入链路草案,一份 Harness 模型请求日志字段规范。前者决定 Android、iOS、鸿蒙三端怎么共享录音与识别状态,后者决定当语音发送失败、模型调用超时或请求重复时,你能不能快速定位是客户端、隧道、Harness 还是 TaoToken 侧的问题。
2. 语音输入链路草案:从按住说话到 session.prompt 的可复现路径
移动端语音输入最怕做成“录完再想”。用户按一下、说半句、发现识别错了,又得重来。对 DSH Mobile 这种远程控制面板来说,语音输入应该围绕短指令和补充信息设计,例如“继续跑刚才的任务”“允许这次审批”“把错误日志里的 502 改成 504 再试一次”。链路可以拆成七步:
- 入口交互:按住说话、上滑取消、松手进入识别预览。不要松手立刻发送,先给用户一次确认或修改机会。
- 权限申请:Android 需要
RECORD_AUDIO,iOS 需要NSMicrophoneUsageDescription,鸿蒙侧也要在模块配置中声明麦克风权限。权限被拒绝时给出可操作提示,而不是只弹一个“失败”。 - 录音参数:统一为 16 kHz、单声道、16 bit PCM,按 100 ms 左右分片。三端底层 API 不同,但写入共享层的 chunk 结构必须一致。
- 端点检测:用 RMS 或 VAD 判断用户是否说完。短指令场景下,静音超过 800 ms 可以自动停止;长语音则保留手动停止。
- ASR 转换:优先调用系统识别能力,拿到 partial text 和 final text。系统识别不可用时,把音频文件写入本地缓存,通过已经建立的 SSH 或 Relay 隧道交给电脑侧自建 ASR 适配器。没有适配器时只保留录音草稿,不上传到不明服务。
- 文本确认:把 final text 回填到输入框,允许用户手动改错别字。如果带图片,把图片引用加到 content 数组里。
- 调用
session.prompt:用 HTTP RPC 发送,同时生成clientMsgId做幂等。重连后只恢复观察和控制,不重新执行已经发过的 Prompt。
共享层接口可以长得像下面这样,三端分别实现,页面只依赖接口:
// commonMain interface DshVoiceInputModule { suspend fun ensurePermission(): Boolean fun start(): kotlinx.coroutines.flow.Flow<VoiceChunk> suspend fun stop(): VoiceDraft suspend fun cancel() } data class VoiceChunk( val sequence: Long, val pcm: ByteArray, val rms: Int, val timestampMs: Long ) data class VoiceDraft( val audioPath: String, val durationMs: Long, val partialText: String, val finalText: String? )发送时不要为语音另造一套 RPC。session.prompt本来就是发消息入口,语音只是多了一步“先转文本”。下面是一个 Kotlin 侧发送示例,clientMsgId推荐由客户端生成并持久化到当前草稿中:
suspend fun sendVoicePrompt( sessionId: String, text: String, imageRefs: List<String> = emptyList() ) { val clientMsgId = generateClientMsgId() val content = buildList { add(mapOf("type" to "text", "text" to text)) imageRefs.forEach { ref -> add(mapOf("type" to "image", "ref" to ref)) } } dshHostProtocol.rpc( method = "session.prompt", body = mapOf( "sessionId" to sessionId, "clientMsgId" to clientMsgId, "content" to content ) ) }这里有两个细节值得移动端开发者注意。第一,clientMsgId不能每次重试都变,否则断线重发会产生两条用户消息。第二,语音识别结果如果在录音结束后才返回,页面状态要区分“录音中”“识别中”“待确认”“已发送”,否则用户会以为按钮没反应。
3. 在 Kuikly commonMain 里下沉语音 Module:三端差异只留在桥接层
DSH Mobile 用 Kuikly 做跨端原生,核心思路是共享业务语义,把平台差异压到最底层。语音输入也应该沿用这套拆法:在commonMain定义DshVoiceInputModule,Android 用AudioRecord,iOS 用AVAudioEngine,鸿蒙用AudioCapturer,但上层页面只看到start()、stop()、cancel()和统一的数据结构。
这样做的收益在协议变化时会放大。今天语音只转文本,明天如果session.prompt的内容数组新增了音频附件类型,或者你要把语音草稿同步到电脑侧做二次识别,只需要改共享层的状态机和适配接口,不需要在三个宿主工程里各写一遍。Android、iOS、鸿蒙宿主代码仍然会有三四千行量级,但那些代码主要处理权限、前后台、音频焦点、蓝牙耳机切换、系统中断等平台能力,不承载业务状态机。
语音模块还要和已有的连接层配合。DSH Mobile 直接连接电脑侧 Harness 暴露的 Host 协议,手机不重新实现 Agent 循环。连接方式可能是 SSH 隧道,也可能是扫码 Relay。语音识别如果走电脑侧适配器,音频上传必须复用现有隧道,不能再开一条公网通道。录音文件放在本地缓存,发送成功后按策略清理;发送失败则保留草稿,等重连后再让用户决定是否重发。
页面层可以按 16 ms 合帧更新识别文本。系统 ASR 的 partial result 可能来得很密,如果每个字都触发一次重组,长语音会明显掉帧。做法是:录音波形按 chunk 更新,识别文本按固定时间窗口合并,最终文本只在finalText到达时写入输入框。
4. 电脑端 Harness 改走 TaoToken:Base URL、API Key 与模型请求日志
手机端语音链路能不能跑通,最终还要看电脑端 Harness 的模型调用是否稳定。在电脑侧配置模型供应商时,Key 去 TaoToken 官网(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=harness_provider)创建或获取,Base URL 填https://taotoken.net/api。注意 Base URL 是给工具和 SDK 用的,不要在后面拼接 UTM 参数;UTM 只用于官网访问统计。
如果 Harness 通过环境变量读取模型配置,可以先用下面这组方式验证通道。Key 用占位符YOUR_API_KEY,实际值不要提交到仓库。
# 电脑端 Harness 运行时环境 export TAOTOKEN_API_KEY="YOUR_API_KEY" export OPENAI_BASE_URL="https://taotoken.net/api" export OPENAI_API_KEY="$TAOTOKEN_API_KEY" # 如果当前适配器走 Anthropic 兼容通道 export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="$TAOTOKEN_API_KEY"配置完成后,不要只依赖界面上的“成功”提示,要在 Harness 侧打一份模型请求日志。建议用 JSONL,每行一个请求,字段至少包含:
{"ts":"2026-01-15T10:00:00.123Z","request_id":"req_01J...","session_id":"sess_abc","source":"mobile_voice","provider":"taotoken","base_url":"https://taotoken.net/api","model":"claude-sonnet-4-20250514","prompt_chars":42,"image_count":0,"latency_ms":812,"status":200,"input_tokens":128,"output_tokens":56,"event_seq":9012}这份日志能回答几个高频问题:请求有没有真正发出、走的是不是 TaoToken、Base URL 有没有写错、模型名是否被供应商拒绝、语音转文本后的 prompt 长度是否异常、响应延迟是否集中在 ASR 之后。日志里不要记录完整 Authorization Header,也不要记录用户原始音频。需要排查时,用request_id和session_id关联客户端事件流即可。
如果日志里完全没有请求,先检查 Harness 进程是否加载了新环境变量;如果status是 401 或 403,检查 Key 是否复制完整、是否在正确项目下创建;如果status是 404,优先检查 Base URL 是否误写成了带路径或带查询参数的形式。调试入口可以放在 TaoToken 官网(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=request_log)的控制台侧,但请求日志主体仍然留在电脑本地,避免把敏感上下文传到无关位置。
5. 用 Claude Code、Codex 与 CC Switch 验证 TaoToken 通道
在正式把 Harness 切到语音工作流之前,可以先用 Claude Code 和 Codex 验证 TaoToken 的模型通道。这样做的好处是把“客户端语音问题”和“模型供应商配置问题”分开。Claude Code 用settings.json,变量是ANTHROPIC_*;Codex 用config.toml,不要把ANTHROPIC_*套到 Codex 上。
Claude Code 的settings.json可以这样写:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514", "ANTHROPIC_SMALL_FAST_MODEL": "claude-3-5-haiku-20241022" } }Codex 的config.toml单独配置,不要混用 Anthropic 变量:
model = "gpt-5-codex" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat" # 若控制台模型要求 Responses API,再改为 responses对应环境变量:
export TAOTOKEN_API_KEY="YOUR_API_KEY"如果你使用 CC Switch 管理多套配置,可以把它当成“三件套”来理解:Claude Code、Codex、OpenAI 兼容客户端各维护一套 Profile,切换时只换 Base URL 和 Key,不交叉污染变量名。
| Profile | 用途 | Base URL | Key 变量 | 注意 |
|---|---|---|---|---|
| Claude Code | 验证 Anthropic 兼容通道 | https://taotoken.net/api | ANTHROPIC_AUTH_TOKEN | 与settings.json保持一致 |
| Codex | 验证 OpenAI 兼容通道 | https://taotoken.net/api | TAOTOKEN_API_KEY | 不要写ANTHROPIC_* |
| OpenAI 兼容客户端 | 调试 Chat Completions | https://taotoken.net/api | TAOTOKEN_API_KEY | 按客户端字段填写 |
验证时让 Claude Code 或 Codex 跑一个最小请求,然后回到 Harness 请求日志,确认provider是taotoken、base_url是https://taotoken.net/api、status是 200。如果这里都正常,再回到手机端语音链路排查录音、ASR 和session.prompt。
6. 锁屏、切后台、重连与重复发送:移动端语音交互排障清单
语音输入比纯文本输入多了一层系统能力,排障时不要只盯着模型。下面这份清单按移动端真实场景排列,适合作为 DSH Mobile 语音功能的自测表。
第一,权限与音频焦点。Android 切到后台后可能被回收录音权限或音频焦点,iOS 来电、闹钟、其他 App 抢占麦克风都会导致录音中断。处理方式是监听中断事件,保存已经录到的音频,并把状态改为“待确认”或“草稿”,不要让页面卡在“录音中”。
第二,VAD 阈值。短指令场景下,用户说“允许”可能只有几百毫秒,VAD 阈值过高会直接截掉。可以先固定一个保守阈值,再把录音前 300 ms 和后 500 ms 保留下来,避免首尾字丢失。
第三,ASR 空结果。先检查采样率、声道数和编码格式是否符合识别引擎要求。系统识别不接受 16 kHz 单声道时,需要在平台层重采样。识别失败时不要把空字符串发给session.prompt,否则用户会看到一条没有内容的 Prompt。
第四,session.prompt超时。移动网络切换、Relay 断线、电脑休眠都会导致 RPC 超时。客户端要用clientMsgId标记这条语音 Prompt,超时后进入待重试状态。重连成功后先补session/event,再拉session.history对齐聊天记录,最后用最新快照覆盖 queue 和 jobs。重连是恢复观察和控制,不是重新执行任务。
第五,重复发送。用户看到超时提示后可能手动再点一次发送。如果客户端没有幂等键,Harness 会收到两条相同内容。建议在草稿创建时生成clientMsgId,重试时复用同一个值,服务端按clientMsgId去重。
第六,图片与语音混合发送。session.prompt支持图片内容数组,但语音转文本后应该以text为主,图片作为可选补充。不要把大段音频直接塞进 content 数组,除非 Harness 协议已经明确支持音频类型。长音频和长文本更适合拆成多次短 Prompt,移动端只做决策入口。
第七,日志关联。客户端日志里记录clientMsgId、录音时长、ASR 耗时、RPC 耗时和最终request_id。Harness 日志里记录模型请求字段。两端通过session_id和clientMsgId关联。这样当用户说“我刚才那条语音发出去没反应”时,你能在几分钟内判断是没录上、没识别、没发出,还是模型调用失败。
7. 落地顺序与 CTA:模型对话 → Coding Plan → 创建 Key → Claude Code 文档
语音输入补进 DSH Mobile 的落地顺序可以很务实:先在 KuiklycommonMain定义语音接口和状态机,再补 Android、iOS、鸿蒙三端录音实现;接着把 ASR 结果回填到输入框,用clientMsgId调session.prompt;最后在电脑端 Harness 配置 TaoToken 模型通道,并打开请求日志验证。整个过程不需要把 Agent 循环搬到手机,手机仍然是短而高频交互的决策节点。
如果你还没有配置模型 Key,可以按下面路径走一遍:先打开模型对话看可用模型与返回格式,再根据使用量选择 Coding Plan,然后创建 API Key 并填入 Harness 或本地工具,最后用 Claude Code 文档核对settings.json和ANTHROPIC_*配置。Base URL 始终填https://taotoken.net/api,Key 用YOUR_API_KEY占位,实际值不要提交到 Git。
- 模型对话:https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=cta_chat
- Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=cta_coding_plan
- 创建 API Key:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=cta_api_keys
- Claude Code 文档:https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=cta_claude_code_doc
官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=final_step
当语音输入、session.prompt、Harness 模型请求日志和 TaoToken 通道都跑通后,DSH Mobile 能接住的不只是文字追问,还包括通勤路上的一句“继续跑”、会议间隙的一次审批、排队时对 Agent 追问的即时补充。移动端开发者要盯住的仍然是那条链路:录音是否可靠、识别是否可改、发送是否幂等、重连是否补事件、模型请求是否有日志。把这些做扎实,语音输入才不是演示功能,而是远程 Agent 工作流里真正可用的一环。