dsh-plugin-mobile-gateway 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/PROTOCOL.md +125 -7
  2. package/README.md +23 -1
  3. package/docs/dsh-rc2-mobile-integration.md +9 -1
  4. package/docs/file-cards-mobile-integration.md +111 -0
  5. package/docs/voice-input-hold-thresholds.md +35 -0
  6. package/docs/voice-input-implementation.md +275 -0
  7. package/docs/voice-input-ipa-analysis.md +150 -0
  8. package/docs/voice-input-ipa-evidence/deepseek-asr-constructor.asm +338 -0
  9. package/docs/voice-input-ipa-evidence/deepseek-extraction.json +5154 -0
  10. package/docs/voice-input-ipa-evidence/deepseek-focused-context.txt +210 -0
  11. package/docs/voice-input-ipa-evidence/deepseek-localization.json +29 -0
  12. package/docs/voice-input-ipa-evidence/deepseek-voice-strings.txt +206 -0
  13. package/docs/voice-input-ipa-evidence/doubao/address-map.txt +11 -0
  14. package/docs/voice-input-ipa-evidence/doubao/code-string-refs.txt +22 -0
  15. package/docs/voice-input-ipa-evidence/doubao/end-longpress-disasm.txt +149 -0
  16. package/docs/voice-input-ipa-evidence/doubao/focused-context.txt +271 -0
  17. package/docs/voice-input-ipa-evidence/doubao/locate_string_refs.py +42 -0
  18. package/docs/voice-input-ipa-evidence/doubao/network-candidates.txt +39 -0
  19. package/docs/voice-input-ipa-evidence/doubao/objc-msgsend-stub.txt +1 -0
  20. package/docs/voice-input-ipa-evidence/doubao/ranges.txt +858 -0
  21. package/docs/voice-input-ipa-evidence/doubao/recognizer-recorder.txt +134 -0
  22. package/docs/voice-input-ipa-evidence/doubao/result-disasm.txt +189 -0
  23. package/docs/voice-input-ipa-evidence/doubao/settext-selector.txt +2 -0
  24. package/docs/voice-input-ipa-evidence/doubao/start-stop-disasm.txt +358 -0
  25. package/docs/voice-input-ipa-evidence/doubao/strings.txt +3536 -0
  26. package/docs/voice-input-ipa-evidence/doubao-extraction.json +7418 -0
  27. package/docs/voice-input-ipa-evidence/doubao-findings.md +52 -0
  28. package/docs/voice-input-ipa-evidence/hold-threshold-rea-evidence.json +1 -0
  29. package/docs/voice-input-ipa-evidence/hold-thresholds/deepseek-hold-callback-refs.txt +1 -0
  30. package/docs/voice-input-ipa-evidence/hold-thresholds/deepseek-input-area-metadata.txt +31 -0
  31. package/docs/voice-input-ipa-evidence/hold-thresholds/deepseek-input-gesture-init.asm +267 -0
  32. package/docs/voice-input-ipa-evidence/hold-thresholds/deepseek-static-summary.json +42 -0
  33. package/docs/voice-input-ipa-evidence/hold-thresholds/deepseek-voice-gesture-constructor.asm +371 -0
  34. package/docs/voice-input-ipa-evidence/hold-thresholds/doubao-findings.md +31 -0
  35. package/docs/voice-input-ipa-evidence/hold-thresholds/doubao-text-mask-objc-metadata.txt +119 -0
  36. package/docs/voice-input-ipa-evidence/hold-thresholds/doubao-text-mask-threshold-evidence.asm +156 -0
  37. package/docs/voice-input-ipa-evidence/hold-thresholds/doubao-text-mask-voice-view-getter.asm +14 -0
  38. package/docs/voice-input-ipa-evidence/rea-evidence.json +1 -0
  39. package/docs/voice-input-ipa-evidence/startup-20261014/README.md +33 -0
  40. package/docs/voice-input-ipa-evidence/startup-20261014/deepseek-objc.txt +222957 -0
  41. package/docs/voice-input-ipa-evidence/startup-20261014/deepseek-preconfigure-annotated.asm +90 -0
  42. package/docs/voice-input-ipa-evidence/startup-20261014/deepseek-preconfigure.asm +90 -0
  43. package/docs/voice-input-ipa-evidence/startup-20261014/deepseek-session-configure-annotated.asm +131 -0
  44. package/docs/voice-input-ipa-evidence/startup-20261014/deepseek-session-configure.asm +131 -0
  45. package/docs/voice-input-ipa-evidence/startup-20261014/deepseek-string-reference-candidates.json +20 -0
  46. package/docs/voice-input-ipa-evidence/startup-20261014/deepseek-touch-began.asm +133 -0
  47. package/docs/voice-input-ipa-evidence/startup-20261014/rea-macho-summary.json +2435 -0
  48. package/lib/client.js +6 -1
  49. package/lib/dsh-host-adapter.mjs +10 -0
  50. package/lib/index.mjs +90 -14
  51. package/lib/session-event-details.mjs +53 -0
  52. package/lib/speech-input.mjs +192 -0
  53. package/package.json +3 -3
package/PROTOCOL.md CHANGED
@@ -5,7 +5,7 @@
5
5
  - **本机端点**:`ws://127.0.0.1:3080/ws/mobile`(与 dsh web GUI 同端口)
6
6
  - **局域网端点**:`ws://<电脑的私有局域网 IP>:3081/ws/mobile`(插件独立监听,只提供经过鉴权的 WebSocket)
7
7
  - **公网端点**:必须由 TLS 反向代理提供 `wss://<域名>/ws/mobile`
8
- - **帧格式**:全部为 JSON 文本帧(UTF-8);图片字节使用标准 Base64
8
+ - **帧格式**:全部为 JSON 文本帧(UTF-8);图片字节及语音 WAV 使用标准 Base64
9
9
  - **连接即推送**:连上后服务端立刻发送一条 `hello`,之后 agent 输出以 `event` 帧实时推送
10
10
 
11
11
  ---
@@ -651,6 +651,7 @@ let image = [
651
651
  | `history` | `sessionId`, `beforeSeq?`, `historyFormatVersion?`, `maxMessages?`, `maxBytes?`, `view?` | 历史事件页(见下) |
652
652
  | `attachment` | `sessionId`, `attachmentId` | 读取历史中属于该会话的图片字节 |
653
653
  | `file-list` | `sessionId`, `path?`, `requestId?` | 列出会话工作目录内的一层文件与文件夹 |
654
+ | `file-resolve` | `sessionId`, `path`, `requestId` | 将文件引用解析为工作目录相对路径及预览元数据;需 `file-references-v1` |
654
655
  | `file-download-open` | `sessionId`, `path`, `requestId` | 打开一个工作目录内的普通文件下载 |
655
656
  | `file-download-read` | `transferId`, `offset` | 拉取下载的下一块字节 |
656
657
  | `file-download-cancel` | `transferId` | 取消并关闭下载 |
@@ -702,7 +703,7 @@ WebUI、App 或其他客户端造成的变化通过以下帧主动推送:
702
703
  - 返回**原始 SessionEvent**(`{type, seq, time, data}`,方案A),可选裁剪
703
704
  - 图片不会内联进历史页。`user/message.data.content[]` 中的图片块为 `{ "type":"image", "attachment": ImageAttachmentRef }`;iOS 使用其中的 `attachmentId` 请求图片数据
704
705
  - `maxBytes`:单帧字节预算,默认 **4 MiB**;超预算保留最新部分并给出 `nextBeforeSeq` 续页(客户端 16 MiB 上限的安全余量)
705
- - `view: "conversation"`:**对话裁剪模式**——隐藏系统消息与 request/header、request/context,移除 Assistant 事件的内嵌 `data.stream`,`tool/result` 文本截断到 2000 字符
706
+ - `view: "conversation"`:**对话裁剪模式**——隐藏系统消息与 request/header、request/context,移除 Assistant 事件的内嵌 `data.stream`,同时保留 `data.finish`(结束类型和结构化失败原因);`tool/result` 原始文本块截断到 2000 字符,另用 `data.output/outputTruncated` 保留最多约 16000 字符的首尾输出
706
707
  - 分页:`hasMore` 为真时用 `beforeSeq: nextBeforeSeq, historyFormatVersion: 4` 请求更早一页
707
708
 
708
709
  ```json
@@ -754,6 +755,43 @@ iOS 发现尚未缓存的 `attachmentId` 后发送:
754
755
 
755
756
  iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建议以 `attachmentId` 为缓存键。不要把 Base64 长期保存在对话模型对象里。并发同步历史时可限制为 2~4 个附件请求,优先加载当前可见消息。
756
757
 
758
+ ### 文件卡片与正文文件引用
759
+
760
+ `hello.capabilities` 中的 `file-references-v1` 表示支持文件交付事件的完整实时字段以及 `file-resolve`。这是协议 3 的可选增量,不改变 Session V4 格式;关闭 `fileDownloadsEnabled` 后不公布此能力,解析请求返回 `file-download-disabled`。旧客户端可忽略新增能力与事件。
761
+
762
+ DSH `present` 工具成功后持久化 `deliverables/presented`,移动端根据这个事件渲染卡片,而非从回复文字猜测附件:
763
+
764
+ ```json
765
+ {
766
+ "kind": "event", "sessionId": "session-abc", "seq": 42, "time": 1786937352,
767
+ "event": {
768
+ "type": "deliverables/presented", "turn": 3, "callId": "present-1",
769
+ "files": [{ "path": "build/三亚国庆旅游攻略.pdf", "description": "A4 共 8 页" }]
770
+ }
771
+ }
772
+ ```
773
+
774
+ `history` / `session-snapshot` 中同一事件仍使用原始 `{type, seq, time, data: {turn, callId, files}}` 格式。按 `sessionId + seq` 合并历史和实时事件,再按 `turn` 将卡片放在本轮回复下面。文件引用保存的是路径,读取的是当前文件内容,并未复制成永久附件;文件以后可能被修改或删除。`description` 是模型提供的描述,页数并非结构化字段。
775
+
776
+ 点击卡片或正文中的本地文件链接后,通过**控制连接**解析:
777
+
778
+ ```json
779
+ { "type": "file-resolve", "requestId": "resolve-1", "sessionId": "session-abc", "path": "build/三亚国庆旅游攻略.pdf" }
780
+ → {
781
+ "kind": "file-resolved", "requestId": "resolve-1", "sessionId": "session-abc",
782
+ "path": "build/三亚国庆旅游攻略.pdf", "name": "三亚国庆旅游攻略.pdf",
783
+ "mediaType": "application/pdf", "size": 330752, "modifiedAt": 1786937352000
784
+ }
785
+ ```
786
+
787
+ - 输入为未做 URL 编码的文件系统路径,可以是相对路径或主机原生绝对路径;仅允许该会话工作目录内的普通文件,包括指向目录内文件的符号链接。会话 `cwd` 的已知路径别名会映射到真实工作目录。
788
+ - 输出 `path` 为规范化的工作目录相对路径,`size` 单位为字节,`modifiedAt` 为 Unix 毫秒。随后把输出 `path` 交给现有 `file-download-open`,完成分块下载及 SHA-256 校验后在 App 内预览。
789
+ - 解析不会创建 `transferId`,下载仍会重新验证路径与文件状态。卡片事件本身不会扩大目录读取权限;工作目录外的文件即使被 `present` 引用也返回 `file-not-allowed`。
790
+ - `..` 路径段、NUL、空路径返回 `bad-request`;缺失、目录、越界符号链接、超限文件分别返回 `file-not-found`、`file-not-regular`、`file-not-allowed`、`file-too-large`。响应和失败均回显字符串 `requestId`,失败另带 `requestType: "file-resolve"`。
791
+ - Markdown 链接由 App 解析,先提取路径并分离 `#L24` 等行号片段,再发送路径;不要把整个 Markdown、HTTP URL 或 `file://` URL 当作路径传入。行内代码只应将本轮已知文件路径及唯一匹配的文件名转换为可点击引用。
792
+
793
+ 两端具体接入点及验收清单见 [文件卡片与预览接入说明](docs/file-cards-mobile-integration.md)。
794
+
757
795
  ### 文件下载(图片、文档、IPA、APK 及其他普通文件)
758
796
 
759
797
  文件下载是独立于历史图片 `attachment` 的二进制传输通道。它不按扩展名做授权白名单:图片、PDF/Office 文档、`.ipa`、`.apk` 和其他**普通文件**均可下载;服务端仅根据扩展名给出 `mediaType`,以便移动端决定打开方式。
@@ -1083,12 +1121,19 @@ Control/旧单连接接收全局模式状态增量,不受所订阅会话过滤
1083
1121
  `event.type` 覆盖(精炼字段):
1084
1122
  - `user/message` → `{text, source, images?: ImageAttachmentRef[]}`
1085
1123
  - 实时 token 使用独立 `assistant-stream`,不是带持久 seq 的 `assistant/chunk`;详见 [rc.2 接入说明](docs/dsh-rc2-mobile-integration.md)。
1086
- - `assistant/attempt` → `{turn, step, stream[]}`
1087
- - `assistant/message` → `{turn, step, text, reasoning, toolCalls[], interrupted?, usage?}`
1124
+ - `assistant/attempt` → `{turn, step, stream[], finish?: {kind, failure?}}`
1125
+ - `assistant/message` → `{turn, step, text, reasoning, toolCalls[], interrupted?, usage?, finish?: {kind, failure?}}`
1088
1126
  - `session/title` → `{title, source?}`
1089
1127
  - `tool/call` → `{turn, step, callId, name, arguments}`
1090
- - `tool/result` → `{turn, step, callId, isError, preview(≤400字符)}`
1091
- - `turn/start|end` / `step/start|end` → `{turn, step, reason?}`
1128
+ - `deliverables/presented` → `{turn, callId, files: [{path, description?}]}`,用于本轮回复下方的文件卡片。
1129
+ - `tool/result` → `{turn, step, callId, isError, preview(≤400字符), output, outputTruncated, error?}`
1130
+ - `turn/start|end` / `step/start|end` → `{turn, step, reason?, reasonDetails?}`,保留原有 `reason` 字符串;`reasonDetails` 提供结构化终态错误或取消来源。
1131
+ - `llm/retry` → `{turn, step, retryId, retry, maxRetries?, delayMs, provider, mode, failure}`。
1132
+ - `llm/retry-started` → `{turn, step, retryId, retry}`。
1133
+
1134
+ `failure` 保留上游提供的 `message/code/status/providerRetryAfterMs/requestId` 等诊断字段。工具 `output` 同时包含标准输出、`[stderr]`、沙箱提示和退出状态标记;超过 16000 字符时保留开头和末尾并设置 `outputTruncated: true`。需要未裁剪的完整内容时,按对应 seq 请求不带 `view: "conversation"` 的历史。
1135
+
1136
+ 移动端必须区分工具失败、模型请求失败、重试和整轮终止:`assistant/attempt` 不代表整轮中断;重试用 `retryId` 合并状态,`llm/retry-started` 更新为已重试并保留失败详情;最终错误以 `turn/end.reason: "error"` 为准,`aborted` 显示取消,`interrupted` 显示中断。Shell 的非零退出码可能仅在输出末尾 `[exit code: N]` 中,Host 的 `isError` 仍为 false;展示时可以标记该工具失败,但不能据此终止整轮。
1092
1137
 
1093
1138
  ### 运行中计时提示
1094
1139
 
@@ -1130,7 +1175,9 @@ DSH Web 的“深度求索中,用时 22 秒 ···”是客户端文案,不
1130
1175
 
1131
1176
  | 版本 | 新增 |
1132
1177
  |---|---|
1133
- | v0.10.0(当前源码) | 响应式居中控制面板与主题同步;自定义下拉菜单;独立配对模态窗口与缩放动画;按运行实例关闭重启提醒;在线设备折叠列表与移动端设备名称上报;Tunnel 配置交互优化 |
1178
+ | v0.10.2(当前源码) | 补齐 `deliverables/presented` 文件卡片实时字段;新增 `file-references-v1` 能力与 `file-resolve` 安全文件引用解析;复用现有分块下载支持移动端预览接入 |
1179
+ | v0.10.1 | 新增官方 speech Remote 语音输入桥接;模型准备与状态同步;移动端音频转文字与取消转写;连接关闭时清理语音任务 |
1180
+ | v0.10.0 | 响应式居中控制面板与主题同步;自定义下拉菜单;独立配对模态窗口与缩放动画;按运行实例关闭重启提醒;在线设备折叠列表与移动端设备名称上报;Tunnel 配置交互优化 |
1134
1181
  | v0.9.0 | DSH 0.2.0-rc.2 插件管理;PC 端 Cloudflare Quick / 命名 Tunnel 及独立重启;插件私有 `cloudflared` 缓存及校验;配对入口选择指引;网关重启与 DSH Web 停止工具 |
1135
1182
  | v0.7.2 | 独立对话/控制连接;空 Session 创建;停止生成与稍后继续;排队消息同步及编辑/删除/Steer;App 归档/重命名 Session;WebUI 归档集合和名称变化实时同步到 App |
1136
1183
  | v0.1.5 | workspace-create / directories / host |
@@ -1239,3 +1286,74 @@ history. Older gateways without the capability continue using one connection.
1239
1286
  ### 移动端设备名称
1240
1287
 
1241
1288
  移动端在 WebSocket 握手中通过 `X-DSH-Device-Name` 上报 UTF-8 设备名称的标准 Base64 编码;配对和凭据重连时均发送。网关不使用控制面板指定的名称,未上报时使用“未命名设备”。可信设备接口保留原始 `name`,并提供 `displayName`;同名设备显示编号后缀,例如“我的手机 (1)”“我的手机 (2)”。
1289
+
1290
+
1291
+ ## 官方语音输入(`speech-input-v1`)
1292
+
1293
+ 网关代理 DSH 0.2.0-rc.2 的官方实验性 `speech` Remote。先在 DSH 插件管理中启用 `@deepseek-ai/dsh-experimental-voice-input-bundle`(“语音输入”),并在详情中下载、准备模型;也可由客户端通过下面的准备接口操作。识别模型运行在 DSH 主机上,不进入移动安装包。
1294
+
1295
+ 所有语音请求必须来自已配对设备,并使用 **control 连接**(兼容 legacy 单连接)。即使关闭本机调试鉴权,未配对连接也不能调用语音接口。配对连接的 `hello.capabilities` 包含 `speech-input-v1`,表示网关协议支持;**是否已启用官方插件、模型是否就绪,需要查询目录**。
1296
+
1297
+ 每个请求都必须提供非空 `requestId`(最多 128 字符),活动请求和订阅期间不要复用。转写结果不创建 Session、不启动 Agent、不写历史,也不自动发送;客户端根据请求 ID 确认录音仍属于当前草稿后插入文字。
1298
+
1299
+ | 请求 `type` | 字段(除 `requestId` 外) | 响应 `kind` |
1300
+ |---|---|---|
1301
+ | `speech-catalog` | 无 | `speech-catalog` |
1302
+ | `speech-subscribe` | 无 | `speech-subscribed`,随后推送 `speech-state` |
1303
+ | `speech-unsubscribe` | 无 | `speech-unsubscribed` |
1304
+ | `speech-configure` | `providerId?`, `language?`,至少一个 | `speech-configured` |
1305
+ | `speech-prepare` | `providerId`, `downloadSource?` | `speech-preparing` |
1306
+ | `speech-cancel-preparation` | `providerId` | `speech-preparation-cancelled` |
1307
+ | `speech-transcribe` | `audioBase64`, `providerId?`, `language?` | `speech-transcript` |
1308
+ | `speech-cancel` | `targetRequestId`,同连接的活动转写 ID | `speech-cancelled` |
1309
+
1310
+ ### 目录、安装和进度
1311
+
1312
+ ```json
1313
+ { "type": "speech-catalog", "requestId": "speech-info-1" }
1314
+ ```
1315
+
1316
+ 启用后响应 `available:true`,并透传官方 `providers`, `selection`, `maxDurationSeconds`。`maxAudioBytes` 为官方限制和网关限制的较小者:网关最多接受 4 MiB 解码后 WAV,并按 `maxPayloadBytes` 为 JSON 信封预留空间。官方默认时长为 120 秒,实际值以目录为准。
1317
+
1318
+ `providers[]` 包含 `id`, `name`, `location`, `languages`, `preparation`,以及可选 `setupEstimate` 和 `downloadSources`。`preparation.phase` 为 `ready` 或 `standby` 时可转写;`unprepared` 需要准备。不要假设语言代码,使用所选 Provider 的 `languages`。`selection` 是主机保存的默认识别器和语言。
1319
+
1320
+ 插件未启用/Remote 已撤销时,目录返回:
1321
+
1322
+ ```json
1323
+ { "kind": "speech-catalog", "requestId": "speech-info-1", "available": false,
1324
+ "providers": [], "code": "speech/unavailable",
1325
+ "message": "Enable the official voice input bundle in DSH plugin management" }
1326
+ ```
1327
+
1328
+ ```json
1329
+ { "type": "speech-subscribe", "requestId": "speech-watch-1" }
1330
+ { "type": "speech-prepare", "requestId": "speech-setup-1", "providerId": "sensevoice-local" }
1331
+ ```
1332
+
1333
+ 每条 `speech-state` 带订阅的 `requestId`,是完整目录快照。准备过程会提供阶段、步骤和真实下载字节。`speech-preparing` 只表示准备请求已提交,不能当作模型已就绪。`downloadSource` 必须来自目录公布的源,官方 Provider 会验证它。断开观察连接或取消订阅不取消 Host 拥有的准备任务;只有 `speech-cancel-preparation` 显式取消。订阅因服务禁用或正常结束而终止时会返回错误,重新启用后需要重新订阅。
1334
+
1335
+ ### 松手后提交录音、回填文字
1336
+
1337
+ 客户端长按时录音,松手停止并转换成 **16 kHz、单声道、PCM16 WAV**,将包含 WAV 头的完整文件编码为规范 Base64(不是 PCM 裸流、AAC/M4A 或 Opus)。
1338
+
1339
+ ```json
1340
+ { "type": "speech-transcribe", "requestId": "record-1",
1341
+ "providerId": "sensevoice-local", "language": "zh", "audioBase64": "<完整 WAV 的 Base64>" }
1342
+ ```
1343
+
1344
+ ```json
1345
+ { "kind": "speech-transcript", "requestId": "record-1", "text": "帮我检查这段代码",
1346
+ "audioSeconds": 3.2, "inferenceSeconds": 0.4 }
1347
+ ```
1348
+
1349
+ `text` 允许为空,表示未识别到讲话。录音开始时应从目录捕获识别器和语言并随请求发送,避免主机偏好在录音中改变。这里只支持官方插件的**完整录音转写**,没有实时增量文字、语音通话或 TTS。
1350
+
1351
+ 每连接最多一个活动转写,网关总共最多四个,每连接最多八个待完成普通语音请求。转写包括目录查询在内有 180 秒超时;官方 Provider 还具有自己的准备/推理限制。
1352
+
1353
+ ```json
1354
+ { "type": "speech-cancel", "requestId": "cancel-1", "targetRequestId": "record-1" }
1355
+ ```
1356
+
1357
+ 取消回执只表示已发出取消信号;原转写随后返回 `speech/cancelled`,资源清理完成前仍占并发名额。断线、网关停止会中止该连接的活动转写和状态订阅。其他连接不能取消这个转写,即使属于同一设备。客户端上滑取消、切换页面或替换草稿后必须忽略旧请求的迟到结果。
1358
+
1359
+ 错误统一为 `kind:error`,包含 `requestType`, `requestId`, `code`, `message`。网关错误包括 `authentication-required`, `wrong-channel`, `bad-request`, `speech/unavailable`, `speech/busy`, `speech/request-in-progress`, `speech/request-not-found`, `speech/invalid-audio`, `speech/cancelled`, `speech/timeout`, `speech/invalid-response`, `speech/stream-ended`;官方插件的 `speech/transcription-failed` 等错误代码保留。
package/README.md CHANGED
@@ -10,7 +10,11 @@ DeepSeek Harness 的设备鉴权移动网关,支持会话与实时事件、排
10
10
  >
11
11
  > 实时流已改为独立 `assistant-stream` 帧,移动端需要按 [rc.2 接入说明](docs/dsh-rc2-mobile-integration.md) 更新订阅、缓存与分页处理。当前修改尚未发布。
12
12
  >
13
- > v0.10.0(当前源码):移动设备面板改为响应式居中设置窗口,跟随主应用深浅色主题;统一自定义下拉菜单与连接状态高亮;配对二维码使用独立模态窗口并支持缩放进出动画;安装重启提醒可按网关运行实例关闭;可信设备默认仅显示在线设备,支持展开离线设备、移动端名称上报与同名编号;Tunnel 切换自动应用,命名配置编辑后保存。
13
+ > v0.10.2(当前源码):新增文件卡片事件透传与安全文件引用解析,复用现有分块下载,为移动端文件卡片和正文文件链接预览提供协议支持。App 接入方式见 [文件卡片与预览接入说明](docs/file-cards-mobile-integration.md)。
14
+ >
15
+ > v0.10.1:新增官方语音输入接入,支持语音模型准备与状态同步、移动端音频转文字、取消转写及连接关闭后的任务清理。通过官方 speech Remote 处理识别,移动网关无需打包 ASR 模型。
16
+ >
17
+ > v0.10.0:移动设备面板改为响应式居中设置窗口,跟随主应用深浅色主题;统一自定义下拉菜单与连接状态高亮;配对二维码使用独立模态窗口并支持缩放进出动画;安装重启提醒可按网关运行实例关闭;可信设备默认仅显示在线设备,支持展开离线设备、移动端名称上报与同名编号;Tunnel 切换自动应用,命名配置编辑后保存。
14
18
  >
15
19
  > v0.9.0:新增 DSH 0.2.0-rc.2 插件管理能力;PC 端可在面板开启、重启 Cloudflare Quick / 命名 Tunnel,使用插件私有缓存中的 `cloudflared` 并校验文件;配对面板增加局域网与外网入口选择指引;工具菜单可重启网关或停止 DSH Web,重启时不再打开新标签页。
16
20
  >
@@ -299,3 +303,21 @@ npm test
299
303
  ```
300
304
 
301
305
  面板右上角“工具”菜单提供“重启当前网关”和“停止 DSH Web”。重启只在通过 `dsh web` 启动时可用,会结束当前 Web 进程并沿用原启动参数重新运行;重启时自动加入 `--no-open`,不会再打开新的浏览器标签页,当前标签页会等待恢复并刷新。重启后的进程在后台运行,不再由原 Terminal 前台任务管理;需要关闭时可在同一菜单点击“停止 DSH Web”,或在 Terminal 找到监听 Web 端口的 DSH 进程并发送 `SIGTERM`。重启期间连接会短暂中断,Quick Tunnel 的外网地址可能随之改变。源码修改后需重新安装插件并重启 `dsh web`。
306
+
307
+
308
+ ## 官方语音输入
309
+
310
+ 网关已接入 DSH 0.2.0-rc.2 官方实验性语音输入插件。移动端可以查询识别器、订阅模型准备进度、准备模型、设置识别偏好,以及提交录音取得文字。取消和断线会中止转写,结果不会自动发送给 Agent。
311
+
312
+ 1. 在 DSH 插件管理中启用“语音输入”(`@deepseek-ai/dsh-experimental-voice-input-bundle`),下载并准备模型。
313
+ 2. 客户端通过已配对的 control WebSocket 查询 `speech-catalog`,确认插件可用及模型 `ready`/`standby`。
314
+ 3. 长按录音、松手后,将完整音频转换为 16 kHz 单声道 PCM16 WAV,通过 `speech-transcribe` 上传 Base64。
315
+ 4. 收到 `speech-transcript` 后将文字填入输入框,用户确认后使用现有 `message` 发送。
316
+
317
+ 模型运行在 DSH 电脑上,手机无需携带识别权重。当前只提供完整录音转写;客户端的录音按钮、权限申请和音频转换仍须接入。接口、准备流程和错误处理见 [协议说明](PROTOCOL.md#官方语音输入speech-input-v1)。
318
+
319
+ 可用已安装的官方插件核对真实 WAV 校验(识别 Provider 使用测试替身,不下载模型):
320
+
321
+ ```bash
322
+ DSH_INSTALL_ROOT=/absolute/path/to/@deepseek-ai/dsh node test/speech-official.test.mjs
323
+ ```
@@ -105,7 +105,7 @@
105
105
 
106
106
  `revision` 属于一次 Agent 生命周期,`index` 属于一次 attempt。不要把其中任何一个写入持久事件 seq。网关检查连续性;App 仍应按当前 streamId/attemptId 去重和隔离展示。
107
107
 
108
- 持久帧仍使用 `kind: "event"`,由同一个上游 follow 有序转发,带真实 seq 以及 subscriptionId/streamId。不会再与全局 `session/event` 重复转发。消息附带中断标识 `interrupted` 和可选 usage;失败 attempt 保留 turn/step 与紧凑 stream。`surfaceOp` 和 `sourceEventSeqs` 在存在时位于持久帧根节点。
108
+ 持久帧仍使用 `kind: "event"`,由同一个上游 follow 有序转发,带真实 seq 以及 subscriptionId/streamId。不会再与全局 `session/event` 重复转发。消息附带中断标识 `interrupted` 和可选 usage;失败 attempt 保留 turn/step 与紧凑 stream。实时与历史对话裁剪均另行保留 `finish: {kind, failure?}`,因此重连后仍可还原模型错误。`surfaceOp` 和 `sourceEventSeqs` 在存在时位于持久帧根节点。
109
109
 
110
110
  ## 4. 重连、切换与取消
111
111
 
@@ -153,3 +153,11 @@
153
153
  ## 验证范围
154
154
 
155
155
  仓库测试覆盖 rc.2 的参数封装、两种独立序号、缺口恢复、取消清理、真实 WebSocket 的原子基线/最终结算、重连前缀、历史裁剪、游标版本校验、工具失败与控制投影。真实 rc.2 Host 与 App 联调仍需在客户端接入后执行。普通文件上传、jobs 和 PTC 专用 UI 属于后续能力扩展。
156
+
157
+
158
+ ## 错误详情与重试展示
159
+
160
+ - 实时 `tool/result` 新增 `output/outputTruncated`,历史 conversation 模式在 `data` 内返回相同字段。优先展示 `output`,回退到 `preview`,无正文时展示 `error`。输出较长时保留首尾,退出状态和沙箱拒绝提示不会被仅取开头的摘要截掉。
161
+ - `assistant/attempt.finish` 表示单次请求结束原因,不能一律显示“生成已中断”。`llm/retry` 携带 `retryId/retry/maxRetries?/delayMs/failure`,按重试链更新状态;`llm/retry-started` 表示等待结束、开始重试,不表示请求成功。
162
+ - `turn/end` 保留原来的字符串 `reason`,新增 `reasonDetails`:`error` 展示生成失败及 `reasonDetails.error`,`aborted` 展示取消,`interrupted` 展示异常中断。工具执行失败后模型仍可继续运行。
163
+ - 这些字段均为向后兼容的可选扩展,Session format 和订阅时钟不变。原始历史仍保留完整 token 流,裁剪历史只保留结束原因摘要。
@@ -0,0 +1,111 @@
1
+ # 移动端文件卡片、正文引用与预览接入
2
+
3
+ 基于本机 DSH `0.2.0-rc.2` 和当前 `dsh-mobile` 代码检查,2026-10-15。
4
+
5
+ ## 数据来源与网关调整
6
+
7
+ 截图中的两个入口来源不同,但应共用一个移动端文件打开流程:
8
+
9
+ | 入口 | 数据来源 | 移动端行为 |
10
+ | --- | --- | --- |
11
+ | 回复底部文件卡片 | `present` 成功后产生的持久事件 `deliverables/presented` | 按本轮交付的文件列表渲染图标、文件名和描述 |
12
+ | 正文蓝色文件名 | Markdown 文件链接 | 拦截文件链接,交给文件打开流程 |
13
+ | 正文行内代码文件路径 | 桌面端用本轮产生或交付的文件集合匹配行内代码 | 仅对已知路径或唯一匹配文件名提供点击预览 |
14
+
15
+ 上游事件结构已经存在,不需要增加 `ContentBlock.type = file`,也不应拿只支持图片的 `attachment` 接口读取 PDF:
16
+
17
+ ```json
18
+ {
19
+ "type": "deliverables/presented",
20
+ "seq": 42,
21
+ "time": 1786937352,
22
+ "data": {
23
+ "turn": 3,
24
+ "callId": "present-1",
25
+ "files": [{ "path": "build/三亚国庆旅游攻略.pdf", "description": "A4 共 8 页" }]
26
+ }
27
+ }
28
+ ```
29
+
30
+ 上游 `@deepseek-ai/dsh-tool-present/lib/types/types.d.ts` 定义上述数据;工具实现只在成功后追加事件。`@deepseek-ai/dsh-client-ui-deliverables` 将交付结果归到对应 `turn` 的回复末尾,并匹配正文文件引用。它的 `/api/present.open` 是让**主机桌面**打开文件的接口,不能当作移动端预览接口。
31
+
32
+ 本次网关实现:
33
+
34
+ 1. `lib/index.mjs` 的 `buildWireEvent` 保留实时事件的 `turn/callId/files`。原实现落入默认分支,只返回事件类型。历史和 opening snapshot 原本就保留该事件,无须转换历史格式。
35
+ 2. 新增能力 `file-references-v1`,保持 WebSocket `protocol: 3`、`historyFormatVersion: 4`。
36
+ 3. 新增控制请求 `file-resolve`,将工作目录内的相对路径或主机原生绝对路径转换为现有下载协议需要的相对路径,并返回文件元数据。
37
+ 4. 文件读取继续使用 `file-download-open/read/cancel`,保留分块、取消、连接归属及最终 SHA-256 校验。
38
+
39
+ 卡片只保证声明时文件存在,文件内容没有被快照保存。路径越界、文件删除或文件过大时应显示可恢复的错误,不能用任意绝对路径下载绕过工作目录限制。
40
+
41
+ ## App 共享层改动
42
+
43
+ 下面路径均相对于 `dsh-mobile` 仓库。本次未修改 App 代码。
44
+
45
+ | 位置 | 改动 |
46
+ | --- | --- |
47
+ | `shared/.../protocol/GatewayDtos.kt` | 增加 `PresentedFile(path, description?)`;`GatewayEvent` 增加 `files`;`RawSessionEvent.normalizedEvent()` 显式处理 `deliverables/presented`,保留 `turn/callId/files` |
48
+ | `shared/.../protocol/GatewayWireDecoder.kt` | 确认新事件和 `file-resolved` 可解码;文件元数据复用已有 frame 字段,补齐缺失字段 |
49
+ | `shared/.../projection/ConversationProjection.kt` | 按会话与 `turn` 收集交付文件,产出本轮回复下方的卡片数据;同时更新原生桥接使用的快照模型 |
50
+ | `shared/.../gateway/GatewayProtocol.kt` | 增加 `fileResolve(sessionId, path, requestId)` 请求,走控制连接;读取 `hello.capabilities` |
51
+ | `shared/.../facade/SharedWorkspaceFileStore.kt` | 增加解析阶段及其请求关联;收到 `file-resolved` 后调用已有 `download`,purpose 使用 `preview` |
52
+
53
+ 实时事件读取 `frame.event.files`,历史读取 `raw.data.files`,两者最终必须进入同一数据模型。只修改实时 DTO 会导致重新进入会话后卡片消失;只修改历史映射会导致文件生成后不能立即显示。
54
+
55
+ 事件按 `gatewayId + sessionId + seq` 幂等合并。同一事件内卡片以 `seq + index` 标识;同一轮重复交付同一原始路径时可以更新描述,保持首次出现顺序。对历史分页中暂时缺少对应 assistant 的交付事件先保留,后续合并时挂载,不能丢弃。不要改变持久化事件 seq 或把卡片存为新的 assistant 文本消息。
56
+
57
+ 统一打开流程应显式携带 `gatewayId/sessionId`,不能在异步回调中重新读取当前选中会话。现有 Android `openWorkspaceFile` 从 `snapshot.selectedSessionId` 取会话,需要改为接受来源会话,并在来源网关失效或会话切换时正确取消/丢弃回调。
58
+
59
+ ## 卡片与链接统一打开流程
60
+
61
+ ```text
62
+ 卡片 / Markdown 文件链接 / 已知行内文件引用
63
+ → openConversationFile(gatewayId, sessionId, sourcePath)
64
+ → control: file-resolve(requestId, sessionId, path)
65
+ ← file-resolved(path, name, mediaType, size, modifiedAt)
66
+ → 原有 file-download-open/read
67
+ → 临时文件落盘、长度与 SHA-256 校验、原子提交
68
+ → 展示应用内预览
69
+ ```
70
+
71
+ `file-resolve` 的完整 JSON 示例与错误码见仓库根目录 `PROTOCOL.md` 的“文件卡片与正文文件引用”。解析不需要先遍历目录,因此文件位于 `build/` 下时也可以直接打开。
72
+
73
+ 正文链接处理规则:
74
+
75
+ - 使用 Markdown 解析器返回的链接目标,不扫描整段文本;解析 `<...>`、中文和空格,URL 编码仅解码一次,保留真正文件名中的特殊字符。
76
+ - 相对文件路径和主机绝对路径走解析接口。明确的 `file://` 链接仅提取合法本地路径后交给网关,绝不能在手机本地直接打开主机路径。Windows 主机的盘符/UNC 路径不能误判为普通 URI scheme。
77
+ - `https://` / `http://` 继续作为网页处理。其他 scheme 按 App 已有白名单处理。`dsh-resource://` 属于上游另一套资源地址,当前新增接口不接收;如需支持,须先实现明确的地址语法和会话范围校验,不能当作普通路径。
78
+ - 只分离已识别的源码行号片段,例如 `#L24`、`#L24-L30`;它不是文件名的一部分。不要无条件剥离文件名中的 `#`。
79
+ - 行内代码优先精确匹配本轮已知路径。仅当 basename 在本轮唯一时才允许缩写匹配;歧义或未知文本维持普通代码样式。不要使每个包含 `.pdf` 的代码字符串都变成文件卡片。
80
+
81
+ 缓存应隔离 `gatewayId + sessionId + canonicalPath`,结合解析返回的 `modifiedAt/size` 判断是否需要重取;下载的最终 SHA-256 用于确认传输完整性。元数据不能保证跨时间内容不变,不能只凭旧路径缓存永远打开旧文件。长文件下载进度与取消由现有下载状态机提供,Base64 不进入聊天消息模型。
82
+
83
+ ## Android 接入
84
+
85
+ `androidApp/.../ui/DshMarkdownText.kt` 当前用 Markwon 渲染,尚无传入聊天文件打开器的入口。增加文件链接回调,并在 Markwon 的链接处理层区分网页和文件目标。需要将当前 `gatewayId/sessionId` 安全地传到回调;列表复用时更新回调,避免使用旧消息的会话。
86
+
87
+ 聊天消息/本轮回复组件增加文件卡片,文件名来自路径 basename,描述使用 `description`。大小可在解析后显示;“共 8 页”目前只是描述文字,没有独立 `pageCount` 协议字段,不应伪造。点击卡片与正文链接走同一个打开器。
88
+
89
+ `androidApp/.../ui/WorkspaceFilesUi.kt` 已有下载完成监听:代码类文件应用内预览,PDF 等其他文件通过 `ACTION_VIEW` 外部打开。应把下载完成处理和预览承载从文件列表弹层提取到可供聊天页调用的位置,避免聊天页下载成功却没有监听器展示预览,也避免重复监听造成重复弹窗。
90
+
91
+ 要满足截图 PDF 的“直接弹出预览”,增加应用内 PDF 弹层或页面,可基于系统 [PdfRenderer](https://developer.android.com/reference/android/graphics/pdf/PdfRenderer) 对本地文件分页渲染。渲染放在后台执行,限制页面位图缓存,离开页面释放页、renderer 和文件描述符;大文档不要一次渲染全部页面。保留“另存为/用其他应用打开”作为辅助操作。其他不支持的文件显示下载/外部打开选项;聊天文件预览不应自动触发 APK 安装流程。
92
+
93
+ ## iOS 接入
94
+
95
+ `DeepSeekHarnessMobile/Views/ConversationView.swift` 的 `MarkdownContent` 使用 MarkdownUI。通过链接打开处理拦截本地文件引用并交给统一打开器;已知行内代码引用需要在解析/渲染层匹配,普通 `openURL` 处理不会自动让代码片段变成链接。
96
+
97
+ `DeepSeekHarnessMobile/Views/RootView.swift` 已有 `WorkspaceQuickLookPreview`、下载结果和预览绑定。将其提取为共享预览组件,并在聊天页可以触达的层级展示 sheet;无需重写已有 [QLPreviewController](https://developer.apple.com/documentation/quicklook/qlpreviewcontroller) 桥接。共享层新增卡片模型后,同步更新 Swift 原生桥接和会话渲染快照,否则 Kotlin DTO 有数据而 Swift UI 仍然看不到卡片。
98
+
99
+ ## 兼容与验收
100
+
101
+ 不支持 `file-references-v1` 的旧网关,可对已确认安全的相对路径复用原有文件下载;对于无法解析的绝对路径给出升级提示。上游未调用 `present` 时,不自动把所有 Markdown 链接生成底部卡片,正文文件链接仍可以单独预览。
102
+
103
+ 网关自动化覆盖历史事件保留、实时字段、解析元数据、中文/空格、相对/绝对路径、目录、缺失、越界、符号链接和原有分块 SHA-256 下载。App 实现后需要补充:
104
+
105
+ 1. 实时产生 PDF 后立即出现卡片,历史重入、重连、分页和多网关同名文件不会丢失或重复。
106
+ 2. 卡片、正文链接及已知行内代码路径均可打开同一 PDF,手机没有外部 PDF 阅读器时仍可预览。
107
+ 3. 多个文件、同 basename 不同目录、多个回合时归属正确。
108
+ 4. 下载中切换会话/网关、断线或关闭预览时无跨会话弹窗、残留临时文件及重复监听。
109
+ 5. 文件修改/删除、超限、越界、校验失败时显示真实错误,允许重试或取消。
110
+
111
+ 本次自动化使用模拟 rc.2 Host 和真实本地 WebSocket;尚未完成真实桌面 DSH 到移动 App 的端到端联调。
@@ -0,0 +1,35 @@
1
+ # 按住说话的长按识别阈值
2
+
3
+ 分析日期:2026-10-12。仅分析用户提供的豆包 12.2.1 和 DeepSeek 2.5.1 iOS IPA;未安装或运行第三方 App,也未读取服务端配置。阈值是手势判定时间,不是麦克风就绪或 ASR 开始的端到端延时。
4
+
5
+ ## 豆包:输入框文本蒙层为 150ms
6
+
7
+ `FlowInputBizUI.MessagingInputView.textMaskLongPressGesture` 的初始化在 `0x1078f0964..0x1078f0974` 构造 double `0x3fc3333333333333`(0.15 秒),并传给 `setMinimumPressDuration:`。同一构造路径随后安装独立的 `handleTextMaskViewTapWithGesture:`,因此该数字属于输入框 tap/longpress 区分。
8
+
9
+ 长按 action 为 `handleTextMaskViewLongPressWithGesture:`,回调链取得 `basicView.voiceInputView` 后转交当前手势。该关联排除了把文本选择或语音通话手势误当成语音输入。快捷输入按钮还有 100ms 路径,专用语音按钮还有 0ms 路径,它们不是该文本蒙层的阈值。
10
+
11
+ REA 安全提取 Evidence:`ev_adf3fc5bce309b0fcea619cdaf3d36b5766996f265fa8a7046639bf25a6319b8`。主程序 SHA-256:`8f81ead52957545efb22c2be3c03717d7fef4803b090d25a2daa0db613fbc47e`。本轮指令及调用链为本地 LLVM/结构解析补充证据,未虚构新的 REA ID。
12
+
13
+ 证据:[详细调查](voice-input-ipa-evidence/hold-thresholds/doubao-findings.md)、[阈值与入口汇编](voice-input-ipa-evidence/hold-thresholds/doubao-text-mask-threshold-evidence.asm)、[语音视图 getter](voice-input-ipa-evidence/hold-thresholds/doubao-text-mask-voice-view-getter.asm)。完整后续 ASR 间接分发尚未全部恢复。
14
+
15
+ ## DeepSeek:100/150ms 配置回退,另有 1ms 提前识别分支
16
+
17
+ 解码的 `InputConfig` 字段包含 `voiceGestureDurationMs`、`voiceMaskAppearDurationMs`,编码键分别为 `voice_gesture_duration_ms`、`voice_mask_appear_duration_ms`。输入区配置回调 `0x1004c8b34` 无配置时在 `0x1004c8c5c..60` 装入 150 和 100,随后按 inputMode 的计算结果选择其中一项,转换成秒并设置长按识别器。该路径使用 `InputAreaView` 的 `_inputViewModel` 字段(实例 offset 40,offset 变量 `0x101349d48`)。
18
+
19
+ 在 `0x1004c8d30..70`,选出的毫秒值除以 1000:其中一只手势根据布尔分支选择配置时长或 double 0.001 秒,另一只使用配置时长。该 literal 的链接地址 `0x101053c48` 已直接读取确认。说明早期手势识别和可见输入状态判定有不同路径,不能将 1ms、100ms 或 150ms 任意一个数字描述成所有模式的固定录音延时。
20
+
21
+ 本轮没有执行 App、恢复远端配置或完整确定 inputMode/布尔分支的业务含义;因此结论限于包内回退值和静态设置路径,不声称实机当前一定采用这些值。
22
+
23
+ REA `inspect_macho` Evidence:`ev_c96cdf0dac4ab8f520e94b93dbee01a57ce337a76934895ca925e4a8cddc9f5c`。主程序 SHA-256:`027b12d8d3c3becb5c6e203b09d2d3387f57747f9f2a299ca90f194cab9e8c20`。已关闭本轮原生目标。
24
+
25
+ 证据:[REA 完整记录](voice-input-ipa-evidence/hold-threshold-rea-evidence.json)、[结构与数值摘要](voice-input-ipa-evidence/hold-thresholds/deepseek-static-summary.json)、[配置回调汇编](voice-input-ipa-evidence/hold-thresholds/deepseek-input-gesture-init.asm)、[输入区字段](voice-input-ipa-evidence/hold-thresholds/deepseek-input-area-metadata.txt)。ADRP 候选扫描仅用于定位,结论使用对应指令核验,不把候选扫描视为完整调用图。
26
+
27
+ ## 本客户端的调整与验证
28
+
29
+ 旧 iOS 与 Android 长按门槛均为 350ms,蒙层淡入为 240ms。现在统一由 KMM `VoiceInputInteraction` 提供 100ms 长按门槛和 120ms 淡入时长;识别成功即发送公共层录音 intent,不再单独等待蒙层。淡出时长保留原值。
30
+
31
+ 100ms 是本客户端的工程选择,参考上述静态路径并满足用户要求更快地区分 tap/longpress,不冒称两个 App 都使用 100ms。首次授权、音频会话和硬件启动仍由系统处理,不能将长按门槛称为真实录音就绪的总耗时。
32
+
33
+ Android 模拟器 7 项界面回归通过:真实 70ms 按下/松手正常进入编辑,160ms 按下/松手已进入语音长按的前置检查;还验证了草稿保留、回填后编辑、椭圆边缘和更快淡入。测试使用原始 down/up 事件,避免 Compose `longClick` 帮助方法强制至少 400ms 的限制。iOS 10 项语音/输入框回归通过;公共层 259 项测试通过,Kotlin 改动通过 ktlint 和 `git diff --check`。
34
+
35
+ 当前用户本地 `DeepSeekHarnessMobile` scheme 覆盖了共享测试配置;本轮使用现有 `WorkspaceDrawerUITests` scheme,仅选择单元测试运行,未修改用户 scheme。验证日志:`/tmp/dsh-fast-voice-device.log`、`/tmp/dsh-fast-voice-ios.log`。测试不连接远端服务,不代表第三方 App 或远端真实 ASR 的运行观测。