dsh-plugin-weapp-gateway 0.8.14 → 0.8.23

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.
package/PROTOCOL.md CHANGED
@@ -1081,7 +1081,7 @@ Control/旧单连接接收全局模式状态增量,不受所订阅会话过滤
1081
1081
  "event": { "type": "assistant/message", "turn": 1, "step": 0, "text": "已完成", "reasoning": "", "toolCalls": [] } }
1082
1082
  ```
1083
1083
  `event.type` 覆盖(精炼字段):
1084
- - `user/message` → `{text, source, images?: ImageAttachmentRef[]}`
1084
+ - `user/message` → `{text, source, images?: ImageAttachmentRef[], attachments?: MessageAttachment[]}`
1085
1085
  - 实时 token 使用独立 `assistant-stream`,不是带持久 seq 的 `assistant/chunk`;详见 [rc.2 接入说明](docs/dsh-rc2-mobile-integration.md)。
1086
1086
  - `assistant/attempt` → `{turn, step, stream[]}`
1087
1087
  - `assistant/message` → `{turn, step, text, reasoning, toolCalls[], interrupted?, usage?}`
@@ -1090,6 +1090,8 @@ Control/旧单连接接收全局模式状态增量,不受所订阅会话过滤
1090
1090
  - `tool/result` → `{turn, step, callId, isError, preview(≤400字符)}`
1091
1091
  - `turn/start|end` / `step/start|end` → `{turn, step, reason?}`
1092
1092
 
1093
+ `MessageAttachment` 的普通文件元素形如 `{kind:"file", name, receiptId?, attachmentId?, bytes?, mediaType?}`,工作区目录元素形如 `{kind:"directory", name, path}`。发送 `message` 时也可带 `{kind:"file"|"directory", path, name?}`,其中 `path` 必须是工作区相对路径;网关拒绝绝对路径、`..` 路径和超过 6 项的请求。当前 DSH Remote 仅提供 `session/prompt`,工作区文件/目录按受校验的上下文引用发送并保留在历史附件元数据中,不伪造 `receiptId`,也不会未经用户操作递归上传目录;原生文件 receipt 上传需宿主提供 `fileUploads.uploadStream` 后再启用。
1094
+
1093
1095
  ### 运行中计时提示
1094
1096
 
1095
1097
  DSH Web 的“深度求索中,用时 22 秒 ···”是客户端文案,不作为独立 Host 事件或网关帧发送。移动端可用 `sessions.items[].running` 判断会话是否运行;订阅中的 `event` 帧在根节点带 `time`,`event.type: turn/start` 标记本轮开始,`turn/end` 标记结束。收到 `turn/start` 后以 `Date.now() - time` 在本地更新秒数,收到 `turn/end` 或会话 `running: false` 后停止。计时文本、单位和动画由移动端本地化渲染。
@@ -1317,3 +1319,168 @@ history. Older gateways without the capability continue using one connection.
1317
1319
  取消回执只表示已发出取消信号;原转写随后返回 `speech/cancelled`,资源清理完成前仍占并发名额。断线、网关停止会中止该连接的活动转写和状态订阅。其他连接不能取消这个转写,即使属于同一设备。客户端上滑取消、切换页面或替换草稿后必须忽略旧请求的迟到结果。
1318
1320
 
1319
1321
  错误统一为 `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` 等错误代码保留。
1322
+
1323
+ ## DeepSeek 账号(`account-v1`)
1324
+
1325
+ Host 提供 `accountController` 服务时,配对连接的 `hello.capabilities` 包含 `account-v1`,客户端可通过控制连接读取当前 DeepSeek 账号的登录状态、资料和余额;Host 未提供该服务时能力不出现,请求返回 `account-unavailable`,旧客户端和旧 Host 均不受影响。账号请求与其他控制帧一样必须来自已配对设备。
1326
+
1327
+ ```json
1328
+ { "type": "account", "requestId": "account-1",
1329
+ "client": { "version": "1.2.3", "locale": "zh", "timezoneOffsetSeconds": 28800 } }
1330
+ ```
1331
+
1332
+ `requestId` 必填,最多 128 字符。`client` 可选:提供时必须带非空 `version`(最多 64 字符);`locale` 省略时按 `en` 处理,`timezoneOffsetSeconds` 非法时按 0 处理。网关把 `client` 传给 Host 用于本地化的资料查询,不会持久化。
1333
+
1334
+ 成功响应回显 `requestId`,`state` 为 Host 的账号投影(登录状态等);提供 `client` 时还会返回本地化后的 `profile` 和 `balance`,单字段失败时为 `null`,不影响整个响应:
1335
+
1336
+ ```json
1337
+ { "kind": "account", "requestId": "account-1", "state": { "signedIn": true },
1338
+ "profile": { "name": "…" }, "balance": { "currency": "CNY", "total": 0 } }
1339
+ ```
1340
+
1341
+ 错误统一为 `kind:error`:`bad-request`(缺少 `requestId`)、`account-unavailable`(Host 未提供账号服务,或读取异常)。客户端应把 `account-unavailable` 当作当前 Host 不支持账号信息并隐藏相关入口,而不是提示网络错误。
1342
+
1343
+ ## 额度卡片(`cost-meter-v1`)
1344
+
1345
+ 网关内置额度卡片数据,不依赖独立的 `dsh-cost-meter` 插件;`hello.capabilities` 固定声明 `cost-meter-v1`。请求必须来自已配对设备,其他连接返回 `authentication-required`。
1346
+
1347
+ ```json
1348
+ { "type": "cost-meter", "requestId": "cost-1", "action": "summary" }
1349
+ ```
1350
+
1351
+ `requestId` 必填,最多 128 字符;`action` 省略时按 `summary` 处理。
1352
+
1353
+ | action | 额外字段 | 说明 |
1354
+ | --- | --- | --- |
1355
+ | `summary` | — | 折叠会话用量(30 秒缓存)并返回 `summary` 与原生 `dashboard` |
1356
+ | `config` | — | 只读显示设置,不折叠会话用量 |
1357
+ | `update` | `patch` | 校验并持久化显示设置(币种、汇率、Go 额度窗口、峰谷时段) |
1358
+ | `refresh` | — | 清空用量/余额缓存后重新读取 |
1359
+ | `set-go-key` / `clear-go-key` | `value` | 写入/清除 `OPENCODE_GO_API_KEY`(DSH 凭据库,不回传移动端) |
1360
+ | `set-balance-key` / `clear-balance-key` | `value` | 写入/清除 `DSH_WEAPP_DEEPSEEK_BALANCE_API_KEY` |
1361
+
1362
+ | `refresh-source` | `source` | 刷新单个来源:`plan:<id>` / `gateway:<id>` / `custom:<index>` |
1363
+ | `source-config` | — | 返回所有可配置来源的精简描述(含是否已配置凭据),不含密钥 |
1364
+ | `set-source-key` / `clear-source-key` | `source`, `value`, 可选 `secret` | 写入/清除厂商凭据到 DSH 凭据库 |
1365
+
1366
+ `patch` 支持显示设置(`currency`、`symbol`、`decimals`、`exchangeRate`、`display.sidebarStyle` = `summary` / `simple` / `standard` / `compact`、`display.sidebarVisible`、`display.peakStyle`)、`balance`、`goQuota`、`budget`(显示位置)、`peak`、`codingPlans`、`customBalances`(≤8)与 `gatewayQuotas.sources`(≤4)。每个来源都有 `display`(`both` / `sidebar` / `settings` / `off`)与 `refreshMinutes`。
1367
+
1368
+ `dashboard` 在原有 `balance` / `goQuota` / `peak` 基础上新增:
1369
+
1370
+ ```json
1371
+ { "currency": "CNY", "symbol": "¥", "decimals": 3,
1372
+ "display": { "sidebarStyle": "standard", "sidebarVisible": true, "peakStyle": "progress" },
1373
+ "plans": [{ "id": "siliconflow", "label": "SiliconFlow 硅基流动", "status": "ok",
1374
+ "windows": { "balance": { "text": "余额 ¥12.50", "resetsAt": "" } } }],
1375
+ "gatewayQuotas": [{ "id": "gw1", "status": "ok", "accounts": [
1376
+ { "provider": "claude", "label": "…", "status": "ok",
1377
+ "windows": [{ "label": "5h", "percent": 21, "resetsAt": "…" }] }] }],
1378
+ "customBalances": [{ "index": 0, "status": "ok", "remaining": 42.5,
1379
+ "maxBudget": 100, "spend": 57.5, "percent": 42.5, "unit": "USD" }],
1380
+ "settings": { "codingPlans": { }, "customBalances": [], "gatewayQuotas": { } } }
1381
+ ```
1382
+
1383
+ 厂商适配器、网关额度适配器与自定义余额解析复用 `dsh-cost-meter` 的 MIT 代码(`lib/vendor/cost-meter/`)。SCNet 与千问的 `planCredits` / `planStart` 配置用于本地逐模型用量估算。
1384
+
1385
+ 成功响应回显 `requestId` 与 `action`:
1386
+
1387
+ ```json
1388
+ { "kind": "cost-meter", "requestId": "cost-1", "action": "summary",
1389
+ "summary": { "…": "今日/本月/累计用量汇总" },
1390
+ "config": { "currency": "CNY", "exchangeRate": 7.2, "decimals": 3,
1391
+ "goQuota": { "enabled": false, "main": "rolling", "barDirection": "used" },
1392
+ "peak": { "enabled": true, "windows": [{ "start": 1, "end": 4 }] } },
1393
+ "dashboard": {
1394
+ "balance": { "enabled": true, "status": "ok", "currency": "CNY", "totalBalance": 0 },
1395
+ "customBalances": [],
1396
+ "goQuota": { "enabled": false, "configured": false, "main": "rolling", "barDirection": "used",
1397
+ "rolling": null, "weekly": null, "monthly": null },
1398
+ "peak": { "enabled": true, "effectiveAt": "",
1399
+ "windows": [{ "start": 1, "end": 4 }, { "start": 6, "end": 10 }], "holidays": [] },
1400
+ "settings": { "goQuota": { "…": "…" }, "peak": { "…": "…" },
1401
+ "goKeyConfigured": false, "balanceKeyConfigured": false } } }
1402
+ ```
1403
+
1404
+ - 官方账号余额优先使用 Host `accountController.getBalance()`;未登录时可用 `DSH_WEAPP_DEEPSEEK_BALANCE_API_KEY` 或 `DEEPSEEK_API_KEY` 读取 DeepSeek 开放平台余额。
1405
+ - OpenCode Go 额度读取 `https://opencode.ai/zen/go/v1/usage`,Key 依次取自 DSH 凭据库、环境变量与 OpenCode 本地 auth 文件。
1406
+ - 显示与峰谷设置写入 `$DSH_HOME/storages/weapp-cost-dashboard.json`(可用 `DSH_WEAPP_COST_DASHBOARD_FILE` 覆盖,文件权限 0600);余额与 Go 额度各缓存 60 秒。
1407
+ - 错误为 `kind:error`、`requestType: "cost-meter"`:`bad-request`、`cost-meter-unavailable`、`cost-meter-dashboard-error`、`cost-meter-credential-write-failed`。
1408
+
1409
+ ## 上下文仪表盘与 Agent 编队(`context-dashboard-v1` / `context-fleet-v1` / `context-content-v1`)
1410
+
1411
+ 网关在 Host 提供 `sessionProjections` 服务时注册纯函数投影 `mobileContext`(key 为 `mobileContext`,`stateVersion` 为 4;旧缓存会整段 refold),把会话事件流折叠为上下文仪表盘数据;`hello.capabilities` 固定声明 `context-dashboard-v1`、`context-fleet-v1` 与 `context-content-v1`。这些请求必须来自已配对设备,其他连接返回 `authentication-required`;Host 未提供所需服务或投影未注册时返回 `context-unavailable`。
1412
+
1413
+ ```json
1414
+ { "type": "context-dashboard", "sessionId": "…" }
1415
+ ```
1416
+
1417
+ 成功响应:
1418
+
1419
+ ```json
1420
+ { "kind": "context-dashboard", "sessionId": "…", "asOfSeq": 123,
1421
+ "value": {
1422
+ "v": 1, "model": "deepseek-chat", "provider": "deepseek", "contextWindow": 128000,
1423
+ "current": { "system": 1, "tools": 2, "user": 3, "inject": 4, "skill": 5, "assistant": 6, "tool": 7, "total": 28 },
1424
+ "counts": { "humanInputs": 1, "answers": 1, "toolCalls": 1, "skillLoads": 0, "injects": 0, "compactions": 0, "prunes": 0 },
1425
+ "tools": { "total": 1, "names": [{ "name": "read_file", "count": 1 }] },
1426
+ "timing": { "wallMs": 1200 },
1427
+ "requests": [{ "seq": 42, "time": 1, "turn": 1, "step": 0, "system": 1, "tools": 2, "user": 3, "total": 6 }],
1428
+ "events": [{ "seq": 43, "time": 1, "kind": "inject", "form": "context" }],
1429
+ "truncated": { "requests": false, "events": false, "surface": false },
1430
+ "breakdown": { "…": "Host contextBreakdown 或 null" },
1431
+ "usage": { "…": "Host tokenUsage 或 null" },
1432
+ "stats": { "…": "Host sessionStats 或 null" }
1433
+ } }
1434
+ ```
1435
+
1436
+ `current` 的 token 估算采用与 DSH token meter 相同的固定密度启发式(每 4 字符 1 token,加块/角色开销);`breakdown` / `usage` / `stats` 是 Host 的 `contextBreakdown` / `tokenUsage` / `sessionStats` 投影,Host 未提供时为 `null`。`events` 只保留最近 120 条,`requests` 最近 200 条,surface 节点最多 4000 个、替换/压缩档案最多 400 个、请求头 epoch 最多 100 个、文件操作最多 400 条、`spans` 最多 600 段;档案与文件操作裁剪水位分别通过 `archiveFloor` / `fileOpsFloor` 表示。surface/header 列表只带轻量索引,不携带正文或工具 schema,以免周期刷新传输大段内容;`context-content` 按需读取选中条目的原文。`fileOps` 记录已知文件工具配对 call/result 推导出的读/写/搜索及行变化估算。`timing.wallMs` 剔除提问与审批等待时间。
1437
+
1438
+ 单条正文请求(同样需要已配对设备):
1439
+
1440
+ ```json
1441
+ { "type": "context-content", "sessionId": "…", "seq": 42 }
1442
+ ```
1443
+
1444
+ 成功响应只返回选中 seq 的正文摘要;文本与工具参数最多 24,000 字符,单个工具 schema 最多 5,000 字符;任何内容触及长度上限时,`value.truncated` 为 `true`。图片仅返回附件占位,不传图像 payload。正文类型限于 system/developer/user/assistant/tool 消息及 request/header、request/context;找不到该 seq 时返回 `context-unavailable`。
1445
+
1446
+ ```json
1447
+ { "kind": "context-content", "sessionId": "…", "seq": 42,
1448
+ "value": { "seq": 42, "time": 1, "type": "user/message", "text": "…" } }
1449
+ ```
1450
+
1451
+ v2 additive 字段(旧客户端可忽略):
1452
+
1453
+ ```json
1454
+ { "activity": { "userInputs": 16, "reads": 36, "writes": 38, "searches": 26, "images": 4 },
1455
+ "timing": { "wallMs": 4980000, "ttftMs": 1598000, "reasoningMs": 1825000, "textMs": 99000,
1456
+ "toolArgMs": 617000, "toolsMs": 840000, "otherMs": 1500,
1457
+ "waitCount": 313, "reasoningCount": 201, "textCount": 16, "toolArgCount": 313, "toolCount": 318,
1458
+ "speedTokens": 204000, "speedMs": 2550000 },
1459
+ "spans": [[0, 1791806835279, 1791807000000]],
1460
+ "lastAnswer": "最近回答预览(≤240 字)",
1461
+ "requests": [{ "seq": 53, "items": { "system": 1, "tools": 79, "user": 3 }, "prompt": 11200, "output": 191 }],
1462
+ "team": { "members": [], "tasks": [] } }
1463
+ ```
1464
+
1465
+ `spans` 的 kind:0=等待、1=思考、2=回复、3=工具参数、4=工具执行、5=其他。
1466
+ `context-fleet` 的 `agents[]` 增加 `lastAnswer` / `phase`,`value.team` 为 `agentTeam` 投影快照(members/tasks 各截 20)。
1467
+
1468
+ ```json
1469
+ { "type": "context-fleet", "sessionId": "…" }
1470
+ ```
1471
+
1472
+ ```json
1473
+ { "kind": "context-fleet", "sessionId": "…",
1474
+ "value": {
1475
+ "rootSessionId": "…", "currentSessionId": "…",
1476
+ "agents": [{ "sessionId": "…", "title": "…", "depth": 0, "isCurrent": true, "isRoot": true,
1477
+ "running": false, "createdAt": 1, "composition": { "system": 1, "tools": 2, "user": 3, "inject": 4, "skill": 5, "assistant": 6, "tool": 7, "total": 28 },
1478
+ "parentSessionId": "…", "mode": "one-shot", "label": "…", "model": "…", "provider": "…",
1479
+ "contextWindow": 128000, "pressureTokens": 900, "tokensTotal": 20, "turns": 2, "steps": 3, "activeMs": 500 }],
1480
+ "counts": { "agents": 1, "running": 0, "subagents": 0 }
1481
+ } }
1482
+ ```
1483
+
1484
+ `context-fleet` 需要 Host 的 `sessions` + `sessionProjections` 服务;只把 `origin: "subagent"` 的会话当作子节点,fork 只是共享谱系的独立会话,只在当前会话的祖先链中出现。家族最多 40 个成员,当前会话始终可见。
1485
+
1486
+ 错误统一为 `kind:error`:`authentication-required`、`bad-request`(缺少 `sessionId`)、`context-unavailable`(Host 未提供所需服务或投影未注册)。客户端应把 `context-unavailable` 当作当前 Host 不支持该项并隐藏入口。
package/README.md CHANGED
@@ -2,7 +2,56 @@
2
2
  <img src="docs/assets/whale-girl-ios-app-promo-16x9.png" alt="DeepSeek Harness Mobile 与移动网关" width="100%">
3
3
  </p>
4
4
 
5
- # dsh-plugin-weapp-gateway
5
+ # 鲸尾移动网关
6
+
7
+ `dsh-plugin-weapp-gateway` · DeepSeek Harness 移动客户端连接插件
8
+
9
+ ### v0.8.23 · 额度设置读取提速
10
+
11
+ - 修复小程序额度卡片页偶发「读取失败 / 未连接」:`config` 动作改为**只读缓存**,不再在打开设置页时同步请求官方余额/OpenCode Go;慢速账户接口被 8 秒上限兜底,避免整个配置请求超时。
12
+ - 首次打开额度页仍会自动触发一次 `refresh` 拉取实时数据;之后由各来源的刷新间隔控制。
13
+ - 小修复复:从设置页返回首页、连接状态变化时,小程序会立即重读网关的侧栏显示开关,不再显示过期卡片。
14
+
15
+ ### v0.8.22 · 额度卡片对齐插件额度页
16
+
17
+ - 网关内置 `dsh-cost-meter` 插件额度页的额度来源:官方余额、OpenCode Go、15 家 Coding Plan(OpenDesign / Factory / Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow / CommandCode / Cline Pass / SCNet / 火山方舟 / 千问 / 小米 MiMo / WorkBuddy)、CLIProxyAPI 网关额度与自定义 Provider 余额(最多 8 条)。
18
+ - `cost-meter` 新增 `refresh-source`、`source-config`、`set-source-key` / `clear-source-key` 等动作;`update` 支持 `display`(四种左侧栏样式与显示位置)、预算显示、Coding Plan 与网关来源配置。小程序不再需要安装 `dsh-cost-meter`。
19
+ - 厂商适配复用 `dsh-cost-meter` 的 MIT 代码(`lib/vendor/cost-meter/`,见 `THIRD-PARTY-NOTICES/dsh-cost-meter.txt`);SCNet / 千问按本地逐模型会话用量估算,其余厂商走官方额度端点。
20
+ - 所有凭据只写入 DSH 凭据库;额度设置写入 `$DSH_HOME/storages/weapp-cost-dashboard.json`(0600)。自定义余额端点只允许 https(本机回环可用 http),拒绝重定向并在外发凭据前校验主机白名单。
21
+ - 协议细节见 [PROTOCOL.md](PROTOCOL.md#额度卡片cost-meter-v1)。
22
+
23
+ ### v0.8.21 · 额度卡片原生数据
24
+
25
+ - `cost-meter` 在原 `summary` / `config` / `update` / `refresh` 基础上直接返回 `dashboard`:官方账号余额、OpenCode Go 滚动/周/月额度、峰谷时段与自定义余额;首页左侧栏额度卡片与「设置 → 侧栏 → 额度卡片」共用这一路数据,不依赖 `dsh-cost-meter` 插件。
26
+ - 新增 `set-go-key` / `clear-go-key` / `set-balance-key` / `clear-balance-key`:密钥仅写入 DSH 凭据库,不回传移动端;显示与峰谷设置写入 `$DSH_HOME/storages/weapp-cost-dashboard.json`(权限 0600),余额与 Go 额度各缓存 60 秒,`refresh` 强制刷新。
27
+ - `hello.capabilities` 声明 `cost-meter-v1`;协议细节见 [PROTOCOL.md](PROTOCOL.md#额度卡片cost-meter-v1)。
28
+
29
+ ### v0.8.18 · 上下文仪表盘 v2
30
+
31
+ - `mobileContext` 投影升级到 `stateVersion: 2`(旧缓存自动 refold),新增 additive 字段:
32
+ `activity`(用户输入/读取/写入/搜索/图片)、`timing` 六段拆分(等待/思考/回复/工具参数/工具执行/其他)+ 输出速度、`spans` 时间轴(≤600 段)、`requests[].items/prompt/output`(请求明细,上限 200)、`lastAnswer`(最近回答预览)。
33
+ - `context-fleet` 增加 `team`(Host `agentTeam` 投影的成员/任务,各 20 条)与每个 Agent 的 `lastAnswer/phase`;微信小程序上下文/编队 v2 卡片与运行中动态刷新依赖这些字段,旧字段语义不变。
34
+ - 协议细节见 PROTOCOL.md「上下文仪表盘与 Agent 编队」。
35
+
36
+ ### v0.8.17 · 修复上下文仪表盘读取
37
+
38
+ - 修复 `context-dashboard`:Host adapter 的 `sessions.history` 返回原始 `session/follow` 快照(没有 `kind` 字段),旧实现误判为读取失败并返回 `context-unavailable`;现在同时接受快照形状与网关 `proxyQuery` 的 `kind: 'history'` 形状,并补充回归测试。
39
+ - `mobileContext` / `context-fleet` 数据与协议不变,无需客户端或配对变更。
40
+
41
+ ### v0.8.16 · 上下文仪表盘与 Agent 编队
42
+
43
+ - 新增纯函数会话投影 `mobileContext`:把会话事件流折叠为上下文构成(system / tools / user / inject / skill / assistant / tool)、累积 token、请求趋势、工具调用、压缩/剪枝事件与真实工作时长(剔除提问与审批等待)。token 估算与 DSH token meter 的固定密度启发式一致,折叠状态有界并可被投影缓存检查点。
44
+ - 配对设备可通过 `context-dashboard` 读取当前会话的仪表盘数据,并附带 Host 的 `contextBreakdown` / `tokenUsage` / `sessionStats` 官方投影;Host 未注册投影时返回 `context-unavailable`。
45
+ - `context-fleet` 返回当前 Agent 的会话家族(祖先链 + subagent 子树,最多 40 个成员),每个成员带上下文构成、token、轮次/步骤、运行状态与耗时;fork 不作为 subagent 子节点,只在祖先链中显示。
46
+ - `hello.capabilities` 新增 `context-dashboard-v1` 与 `context-fleet-v1`;请求必须来自已配对设备,旧 Host 或未提供服务的 Host 返回 `context-unavailable`,网关其余功能不受影响。
47
+ - 协议细节见 [PROTOCOL.md](PROTOCOL.md#上下文仪表盘与-agent-编队context-dashboard-v1--context-fleet-v1)。
48
+
49
+ ### v0.8.15 · 工作区附件与 DeepSeek 账号
50
+
51
+ - 移动端发送消息时可附带工作区文件或目录引用:最多 6 项、只接受工作区相对路径,网关拒绝绝对路径、`..` 和空项;引用会作为受校验的上下文发送给 Agent,并不伪造文件 receipt。
52
+ - 实时与历史 `user/message`、`assistant/message` 事件新增 `attachments` 元数据;历史消息中的 `[工作区文件: …]` / `[工作区目录: …]` 文本也会被解析为同结构,客户端不必自行解析正文。
53
+ - 配对设备可通过 `account-v1` 能力读取当前 DeepSeek 账号的登录状态、资料与余额;Host 未提供账号服务时能力不出现,请求返回 `account-unavailable`,旧 Host 自动降级。
54
+ - 协议细节见 [PROTOCOL.md](PROTOCOL.md#deepseek-账号account-v1)。
6
55
 
7
56
  ### v0.8.13 · 网关内置微信 ClawBot
8
57
 
@@ -23,7 +72,7 @@
23
72
 
24
73
  ### v0.8.9 · 插件在线更新
25
74
 
26
- - 打开「设置 → 移动设备」时自动检查 npm 最新版本,并可在面板中一键安装更新。
75
+ - 打开「设置 → 鲸尾移动网关」时自动检查 npm 最新版本,并可在面板中一键安装更新。
27
76
  - DSH 宿主支持插件管理时直接升级;旧版宿主会给出 Terminal 安装命令。构建脚本审批不会自动批准,要求重启时需手动重启 DSH。
28
77
  - 接入小程序订阅通知:通过一次性通知票据,在对应会话轮次结束后回调通知服务,不发送任务正文。
29
78
 
@@ -35,13 +84,13 @@
35
84
 
36
85
  协议与客户端处理约定见 [文件变更接入说明](docs/workspace-changes-mobile-integration.md)。
37
86
 
38
- DeepSeek Harness 的设备鉴权移动网关,支持会话与实时事件、排队消息同步及编辑/删除/Steer、Session 归档和重命名的双向同步、停止当前生成并稍后继续、任务列表和当前 Goal 同步及管理、服务端驱动的命令和技能菜单、Human-in-the-loop、图片及文件传输。本 fork 不占用侧边栏:全部管理能力集中在 Harness WebUI 的「设置 → 移动设备」页面,可直接开启网关、生成配对二维码和管理可信设备;离开该页面即停止轮询,不再有任何悬浮层。
87
+ DeepSeek Harness 的设备鉴权移动连接插件,支持会话与实时事件、排队消息同步及编辑/删除/Steer、Session 归档和重命名的双向同步、停止当前生成并稍后继续、任务列表和当前 Goal 同步及管理、服务端驱动的命令和技能菜单、Human-in-the-loop、图片及文件传输。本 fork 不占用侧边栏:全部管理能力集中在 Harness WebUI 的「设置 → 鲸尾移动网关」页面,可直接开启连接服务、生成配对二维码和管理可信设备;离开该页面即停止轮询,不再有任何悬浮层。
39
88
 
40
89
  > 当前源码以 **DSH 0.2.0-rc.2** 为适配基线,使用 Session format 4;旧版历史游标需重新取得基线。
41
90
  >
42
91
  > 实时流已改为独立 `assistant-stream` 帧,移动端需要按 [rc.2 接入说明](docs/dsh-rc2-mobile-integration.md) 更新订阅、缓存与分页处理。当前修改尚未发布。
43
92
  >
44
- > v0.7.7:同步上游 v0.8.0(DSH 0.1.7-rc.2 / Session format 4、inbox 排队消息同步、Schedule 与 Permission Presets、审批 `displayReason`),管理界面仅保留在「设置 → 移动设备」,不再提供侧边栏入口。
93
+ > v0.7.7:同步上游 v0.8.0(DSH 0.1.7-rc.2 / Session format 4、inbox 排队消息同步、Schedule 与 Permission Presets、审批 `displayReason`),管理界面仅保留在「设置 → 鲸尾移动网关」,不再提供侧边栏入口。
45
94
  >
46
95
  > v0.8.6(续):修复 DSH 0.2.1 宿主上所有 Remote 请求崩溃——`Cannot read properties of undefined (reading 'signal')`。0.2.1-alpha 起 `InvokeRemoteRequest.peer` 改为必填(`assertPeerLive` 与 `methodSignal` 都会读 `peer.signal`,流式方法还会并入它),而适配器此前只传 `{namespace, method, args, signal}`;工作区/会话/历史全部不可用,只剩 `default-model`(不走 Remote Gateway)正常。现在适配器以自己的 owner Peer 发言(随插件 dispose 中止),0.1.7/0.2.0 宿主忽略该字段,因此新旧宿主通吃。附 0.2.1 宿主形状的回归测试。
47
96
  >
@@ -72,7 +121,7 @@ DeepSeek Harness 的设备鉴权移动网关,支持会话与实时事件、排
72
121
  > v0.8.4:修复中继模式下打开面板地址栏仍显示 Cloudflare 地址
73
122
  >
74
123
  > v0.8.5:扫码成功后二维码弹窗自动关闭——服务端暴露待用配对码 ID,面板轮询发现自己的码被消费即关闭,不用等 5 分钟过期或手动关。——refresh() 原先只看本地路由 ref(初始 auto),现在先采纳服务端的 pairingMode 再推导地址,打开即显示中继地址。——原先用在捕获阶段 stopPropagation 阻止设置页插件折叠导航,但 React 的 onClick 委托在 document 根,捕获阶段拦截会连自己的 handler 一起掐死。改为 MutationObserver 监听并即时还原折叠,不碰事件。——弹窗导航按钮固定 40px 高度(不再塌成文字高度);点导航不再被设置页插件折叠成汉堡按钮(捕获阶段拦截 + 自我恢复);中继设备自动命名(连接器每次连接都上报手机型号 header,网关据此更新设备名)。——换用上游 client.js 时把 id: 'dsh-plugin-mobile-gateway' 一并带入,加载器按包名找不到注册项,整个插件条目被剔除(设置页消失)。现改回 dsh-plugin-weapp-gateway 并加防回归守卫。
75
- > v0.7.8:同步上游 v0.8.1(PC 端可开启 Cloudflare Quick / 命名 Tunnel、配对面板增加局域网与外网入口选择指引、工具菜单可重启网关或停止 DSH Web),管理界面仅保留在「设置 → 移动设备」。
124
+ > v0.7.8:同步上游 v0.8.1(PC 端可开启 Cloudflare Quick / 命名 Tunnel、配对面板增加局域网与外网入口选择指引、工具菜单可重启网关或停止 DSH Web),管理界面仅保留在「设置 → 鲸尾移动网关」。
76
125
  >
77
126
  > v0.7.9:Cloudflare Tunnel 扩展到 Linux 服务器(本 fork 定制):上游仅支持 Windows/macOS 自动下载校验 `cloudflared`,本 fork 允许在 Linux 上使用 Cloudflare 官方 apt 源安装的 `cloudflared`(或配置 `cloudflaredPath` 指向任意可执行文件),隧道机制不变。
78
127
  >
@@ -107,7 +156,7 @@ DeepSeek Harness 的设备鉴权移动网关,支持会话与实时事件、排
107
156
 
108
157
  插件提供稳定 `gatewayId`、可配置 `gatewayName`、配对候选地址列表,以及可持久化的“关闭 / 临时开启 / 常驻开启”运行模式。一个 App 可以分别配对不同机器上的网关;客户端多网关管理仍需按 [App 对接说明](docs/multi-gateway-app-integration.md) 实现。本次源码尚未发布新的 npm 版本。
109
158
 
110
- 在“移动设备”面板选择“常驻开启”,网关会持续接受已授权设备连接,重启后保持。常驻需要 DSH 进程运行、机器未休眠且网络可达,不提供自动发现或网络中转。
159
+ 在“鲸尾移动网关”面板选择“常驻开启”,网关会持续接受已授权设备连接,重启后保持。常驻需要 DSH 进程运行、机器未休眠且网络可达,不提供自动发现或网络中转。
111
160
 
112
161
  部署配置示例(对应 mobile-gateway 插件的 `config`):
113
162
 
@@ -178,13 +227,13 @@ npx --yes dsh-plugin-weapp-gateway@latest init
178
227
  dsh web
179
228
  ```
180
229
 
181
- 打开 WebUI,进入「设置」确认出现「移动设备」页面(侧边栏不会新增入口)。
230
+ 打开 WebUI,进入「设置」确认出现「鲸尾移动网关」页面(侧边栏不会新增入口)。
182
231
 
183
232
  ## 局域网配对
184
233
 
185
234
  适用于 DSH 电脑和 iPhone 位于同一个可互访的局域网。
186
235
 
187
- 1. 在 WebUI 的「设置」中打开“移动设备”。
236
+ 1. 在 WebUI 的「设置」中打开“鲸尾移动网关”。
188
237
  2. 将“网关运行模式”设为“常驻开启”(短期配对也可选“临时开启”)。
189
238
  3. 保持“设备鉴权”开启。
190
239
  4. 在“配对连接方式”选择“局域网直连”,确认 WebSocket 地址为 `ws://<电脑局域网 IP>:3081/ws/mobile`。即使 Cloudflare Tunnel 已开启,也无需关闭它。选择框旁的问号会按当前网关环境解释各入口并给出场景建议,不会自动更改选择。
@@ -242,7 +291,7 @@ http://127.0.0.1:<本地端口>
242
291
 
243
292
  ### 4. 在 UI 配置公网入口
244
293
 
245
- 在 WebUI「设置 → 移动设备」中打开面板,在“公网接入”填写云厂商控制台提供的公网 IPv4,然后点击“配置公网接入”或“更新公网配置”。Helper 会自动读取当前 `dsh web` 端口并配置 Nginx、TLS 证书和 `wss://<公网 IP>/ws/mobile`。
294
+ 在 WebUI「设置 → 鲸尾移动网关」中打开面板,在“公网接入”填写云厂商控制台提供的公网 IPv4,然后点击“配置公网接入”或“更新公网配置”。Helper 会自动读取当前 `dsh web` 端口并配置 Nginx、TLS 证书和 `wss://<公网 IP>/ws/mobile`。
246
295
 
247
296
  <p align="center">
248
297
  <img src="docs/assets/public-access-ui.png" alt="在移动设备面板配置公网接入" width="420">
@@ -265,7 +314,7 @@ http://127.0.0.1:<本地端口>
265
314
 
266
315
  ### Cloudflare Quick Tunnel(一键临时接入)
267
316
 
268
- 在「移动设备 → Cloudflare Tunnel」选择「Quick Tunnel · 无需域名」,点击「一键开启」。面板会在首次运行时下载 `cloudflared`,连接后显示随机的 `wss://<名称>.trycloudflare.com/ws/mobile`,并自动将其填为配对地址。需要在局域网内直连时,将下方“配对连接方式”改为“局域网直连”;Tunnel 可以继续开启。生成二维码,用手机 App 扫码即可。关闭开关会停止隧道;DSH 再次启动时会恢复已开启的 Quick 模式并取得**新的随机地址**。
317
+ 在「鲸尾移动网关 → Cloudflare Tunnel」选择「Quick Tunnel · 无需域名」,点击「一键开启」。面板会在首次运行时下载 `cloudflared`,连接后显示随机的 `wss://<名称>.trycloudflare.com/ws/mobile`,并自动将其填为配对地址。需要在局域网内直连时,将下方“配对连接方式”改为“局域网直连”;Tunnel 可以继续开启。生成二维码,用手机 App 扫码即可。关闭开关会停止隧道;DSH 再次启动时会恢复已开启的 Quick 模式并取得**新的随机地址**。
269
318
 
270
319
  Quick Tunnel 无需 Cloudflare 账户、Token 或自己的公网域名,但地址在 DSH 重启或隧道重连后可能变化。手机 App 只信任已确认的地址,因此地址变化后需在面板重新生成二维码并扫码更新配对;**仅凭 PC 在线不能保证旧地址继续可用**。Cloudflare 将 Quick Tunnel 定位于测试和开发,当前有并发请求上限且不提供可用性保证。参见 [Cloudflare Quick Tunnel 文档](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/)。
271
320
 
@@ -273,7 +322,7 @@ Quick Tunnel 无需 Cloudflare 账户、Token 或自己的公网域名,但地
273
322
 
274
323
  ### Cloudflare 命名 Tunnel(固定地址)
275
324
 
276
- 面板(「移动设备 → Cloudflare Tunnel」选「命名 Tunnel」)里内置了两个跳转链接:
325
+ 面板(「鲸尾移动网关 → Cloudflare Tunnel」选「命名 Tunnel」)里内置了两个跳转链接:
277
326
  [Cloudflare 控制台 → 隧道](https://dash.cloudflare.com/?to=/:account/tunnels) 和
278
327
  [官方创建教程](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel/)。
279
328
 
@@ -299,7 +348,7 @@ echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudf
299
348
  apt-get update && apt-get install -y cloudflared
300
349
  ```
301
350
 
302
- 安装后「移动设备 → Cloudflare Tunnel」分组即出现在面板(与 Windows/macOS 相同)。插件按以下顺序定位程序:profile 配置里的 `cloudflaredPath`(绝对路径优先),否则在 PATH 中查找 `cloudflared`;都不存在时面板会提示上面的安装命令。其余行为(Quick / 命名模式、配对地址自动填入、Token 保存、`/ws/mobile` 之外一律 404)与 PC 完全一致;Linux 同样不会监听 DSH WebUI 或 `/mgw` 的对外路径。
351
+ 安装后「鲸尾移动网关 → Cloudflare Tunnel」分组即出现在面板(与 Windows/macOS 相同)。插件按以下顺序定位程序:profile 配置里的 `cloudflaredPath`(绝对路径优先),否则在 PATH 中查找 `cloudflared`;都不存在时面板会提示上面的安装命令。其余行为(Quick / 命名模式、配对地址自动填入、Token 保存、`/ws/mobile` 之外一律 404)与 PC 完全一致;Linux 同样不会监听 DSH WebUI 或 `/mgw` 的对外路径。
303
352
 
304
353
  #### 公网入口检测与状态含义
305
354
 
@@ -375,11 +424,11 @@ sudo env "PATH=$PATH" npx --yes dsh-plugin-weapp-gateway@latest remove-helper
375
424
 
376
425
  | 现象 | 处理方式 |
377
426
  |---|---|
378
- | 「设置」里没有“移动设备” | 确认安装在 `web` profile,并完整重启 `dsh web` |
427
+ | 「设置」里没有“鲸尾移动网关” | 确认安装在 `web` profile,并完整重启 `dsh web` |
379
428
  | iOS 收到 `503` | 回到 WebUI 开启“允许移动设备连接” |
380
429
  | iOS 收到 `401` | 在 WebUI 重新生成二维码并配对 |
381
430
  | Linux 服务器公网连接超时 | 检查云安全组、服务器防火墙和 TCP `80/443` |
382
- | Linux 服务器公网地址没有显示 | 在“移动设备 → 公网接入”填写公网 IPv4 并点击更新 |
431
+ | Linux 服务器公网地址没有显示 | 在“鲸尾移动网关 → 公网接入”填写公网 IPv4 并点击更新 |
383
432
  | Cloudflare 下载失败 | 检查 PC 到 GitHub Releases 的连接;也可自行安装 cloudflared,并把 `cloudflaredPath` 设为可执行文件的绝对路径 |
384
433
  | Quick Tunnel 重启后 App 无法连接 | Quick 地址可能已变化,在面板重新生成二维码并让 App 扫码更新配对;需要固定地址时改用命名 Tunnel |
385
434
  | 面板提示未找到 cloudflared(Linux) | 按上文 apt 命令安装 Cloudflare 官方 `cloudflared`,或把 `cloudflaredPath` 指向已有可执行文件后重启 |
@@ -0,0 +1,31 @@
1
+ dsh-cost-meter quota adapters (vendored under lib/vendor/cost-meter/)
2
+ https://github.com/Han-1413141/dsh-cost-meter
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2026 dsh-cost-meter contributors
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+
26
+ The vendored modules are: net.mjs, factory-quota.mjs, opendesign.mjs,
27
+ workbuddy.mjs, workbuddy-accounts.mjs, workbuddy-transport.mjs,
28
+ coding-plans.mjs, gateway-quotas.mjs, gateway-quota-adapters.mjs,
29
+ custom-balance.mjs, aliyun-balance.mjs, tokenrhythm-balance.mjs. Local shims
30
+ replace the two DSH package imports so the gateway plugin keeps a small
31
+ dependency surface.
package/bin/setup-ip.mjs CHANGED
File without changes
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Account bridge: proxy the Host's DeepSeek account projection to the mobile
3
+ * client. Injected only when the Host provides `accountController`; older hosts
4
+ * keep answering `account-unavailable`, which the client degrades gracefully.
5
+ *
6
+ * Wire shape (mobile -> host): { type: 'account', requestId, client }
7
+ * Wire shape (host -> mobile): { kind: 'account', requestId, state, profile, balance }
8
+ */
9
+ export const ACCOUNT_REQUEST_TYPES = new Set(['account'])
10
+
11
+ const MAX_REQUEST_ID = 128
12
+
13
+ function normalizeClient(value) {
14
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
15
+ const version = typeof value.version === 'string' ? value.version.trim().slice(0, 64) : ''
16
+ if (!version) return undefined
17
+ const rawLocale = typeof value.locale === 'string' ? value.locale.trim().slice(0, 32) : ''
18
+ const offset = Number(value.timezoneOffsetSeconds)
19
+ return {
20
+ version,
21
+ locale: rawLocale || 'en',
22
+ timezoneOffsetSeconds: Number.isFinite(offset) ? Math.trunc(offset) : 0,
23
+ }
24
+ }
25
+
26
+ function settle(promise) {
27
+ try { return Promise.resolve(promise).catch(() => null) } catch (_) { return Promise.resolve(null) }
28
+ }
29
+
30
+ export async function handleAccount(api, msg) {
31
+ const requestId = typeof msg?.requestId === 'string' ? msg.requestId.slice(0, MAX_REQUEST_ID) : ''
32
+ const fail = (code, message) => ({
33
+ kind: 'error', requestType: 'account', ...(requestId ? { requestId } : {}), code, message,
34
+ })
35
+ if (!requestId) return fail('bad-request', 'account requires a requestId')
36
+ const account = api?.account
37
+ if (!account || typeof account.getState !== 'function') {
38
+ return fail('account-unavailable', 'Account service is unavailable on this host')
39
+ }
40
+ const client = normalizeClient(msg.client)
41
+ try {
42
+ const state = (await settle(account.getState())) ?? null
43
+ let profile = null
44
+ let balance = null
45
+ if (client) {
46
+ const [profileOutcome, balanceOutcome] = await Promise.all([
47
+ typeof account.getProfile === 'function' ? settle(account.getProfile(client)) : null,
48
+ typeof account.getBalance === 'function' ? settle(account.getBalance(client)) : null,
49
+ ])
50
+ profile = profileOutcome ?? null
51
+ balance = balanceOutcome ?? null
52
+ }
53
+ return { kind: 'account', requestId, state, profile, balance }
54
+ } catch (_) {
55
+ return fail('account-unavailable', 'Account information is unavailable')
56
+ }
57
+ }
package/lib/client.js CHANGED
@@ -252,7 +252,7 @@ window.__ModuleLoader__.load({
252
252
  // so a throwing glyph leaves the shell's own icon in place rather than
253
253
  // blanking the cell.
254
254
  // --------------------------------------------------------------------------
255
- const NAV_LABEL = '移动设备'
255
+ const NAV_LABEL = '鲸尾移动网关'
256
256
  const NAV_ICON_MARK = 'data-mgw-nav-icon'
257
257
 
258
258
  function navGlyph() {
@@ -988,10 +988,10 @@ window.__ModuleLoader__.load({
988
988
  pairingTokenCopied ? React.createElement('div', { style: { ...styles.muted, color: colors.success, marginBottom: 8 } }, '已复制到剪贴板') : null,
989
989
  copyNotice ? React.createElement('div', { role: 'alert', style: { ...styles.muted, color: colors.danger, marginBottom: 8 } }, copyNotice) : null,
990
990
  React.createElement('div', { style: styles.muted }, `有效期至 ${new Date(qr.pairing.expiresAt).toLocaleTimeString()};扫码成功后此二维码立即失效。`)))) : null,
991
- React.createElement('aside', { className: 'mobile-gateway-panel', inert: qr ? true : undefined, 'aria-hidden': qr ? true : undefined, style: styles.panel, onMouseDown: (event) => { event.stopPropagation(); setToolsOpen(false) }, role: 'dialog', 'aria-modal': true, 'aria-label': '移动设备管理' },
991
+ React.createElement('aside', { className: 'mobile-gateway-panel', inert: qr ? true : undefined, 'aria-hidden': qr ? true : undefined, style: styles.panel, onMouseDown: (event) => { event.stopPropagation(); setToolsOpen(false) }, role: 'dialog', 'aria-modal': true, 'aria-label': '鲸尾移动网关管理' },
992
992
  React.createElement('div', { style: styles.header },
993
993
  React.createElement('div', null,
994
- React.createElement('h2', { style: styles.title }, '移动设备'),
994
+ React.createElement('h2', { style: styles.title }, '鲸尾移动网关'),
995
995
  React.createElement('div', { style: styles.muted }, !status ? '正在加载网关状态…' : status.gatewayEnabled ? `移动网关已开启 · ${status.requireAuth ? '设备鉴权已开启' : '本机设备鉴权已关闭'}` : '移动网关已关闭 · 普通 WebUI 模式')),
996
996
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
997
997
  React.createElement('div', { style: { position: 'relative' }, onMouseDown: (event) => event.stopPropagation() },
@@ -1020,7 +1020,7 @@ window.__ModuleLoader__.load({
1020
1020
  React.createElement('svg', { width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.2, strokeLinecap: 'round', 'aria-hidden': true },
1021
1021
  React.createElement('path', { d: 'M6 6l12 12M18 6L6 18' }))))),
1022
1022
  React.createElement('div', { className: 'mgw-body' },
1023
- React.createElement('nav', { ref: navRef, className: 'mgw-nav', 'aria-label': '移动网关设置分组' },
1023
+ React.createElement('nav', { ref: navRef, className: 'mgw-nav', 'aria-label': '鲸尾移动网关设置分组' },
1024
1024
  [['general', '通用设置'], ['pairing', '连接与配对'], ['devices', '可信设备']].map(([id, label]) =>
1025
1025
  React.createElement('button', { key: id, type: 'button', 'aria-current': activeSection === id ? 'page' : undefined,
1026
1026
  onClick: () => { setActiveSection(id); setConnectionHelpOpen(false); setToolsOpen(false) } }, label))),
@@ -1345,7 +1345,7 @@ window.__ModuleLoader__.load({
1345
1345
  // name): it is a slot key that pinned-section state and the settings
1346
1346
  // plugin-hub map against, so renaming it would drop existing pins.
1347
1347
  ctx.slots.inject('settings.section', () => ctx.slots.register(
1348
- { name: 'settings.section', id: 'mobile-gateway', order: 35, label: () => '移动设备' },
1348
+ { name: 'settings.section', id: 'mobile-gateway', order: 35, label: () => NAV_LABEL },
1349
1349
  () => React.createElement(SettingsEntry, null),
1350
1350
  ))
1351
1351
  }