@marktrue/dsh-mobile-remote 3.2.1-rc.1 → 3.2.2-alpha.1
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/CHANGELOG.md +405 -17
- package/docs/00-/345/274/200/345/217/221/346/200/273/347/272/262.md +4 -1
- package/docs/01-PRD.md +1 -1
- package/docs/02-architecture.md +2 -2
- package/docs/03-api.md +146 -32
- package/docs/04-security.md +9 -5
- package/docs/05-test-cases.md +58 -18
- package/docs/06-install-run.md +64 -0
- package/docs/07-user-manual.md +2 -1
- package/docs/09-compatibility.md +60 -32
- package/docs/10-release.md +3 -1
- package/docs/spec-app-auto-update.md +143 -0
- package/lib/index.js +271 -142
- package/package.json +2 -2
package/docs/03-api.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 03 API 接口设计文档 — dsh-mobile-remote
|
|
2
2
|
|
|
3
|
-
> 版本:v3.
|
|
3
|
+
> 版本:v3.1.5 · 状态:已实现;Git API 小节为尚未实现的设计草案 · 配套:00-开发总纲.md、02-architecture.md、04-security.md、09-compatibility.md
|
|
4
4
|
> 前缀:`/m`(可配置项 `path`,默认 `/m`)。以下所有路径均以前缀开头。
|
|
5
5
|
> 服务端 API 同时服务 Flutter App(dsh-mobile-app)与桌面设置页客户端模块;不含网页版页面(v2.1 起移除)。
|
|
6
6
|
## 1. 通用约定
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
| GET | `/m/api/usage` | 会话 token 用量 | 是 |
|
|
46
46
|
| GET | `/m/api/workspaces` | 已注册工作区 | 是 |
|
|
47
47
|
| GET/POST | `/m/api/directories` | 目录浏览/新建文件夹 | 是 |
|
|
48
|
-
| GET | `/m/api/diagnostics` |
|
|
48
|
+
| GET | `/m/api/diagnostics` | 环境诊断(插件运行时、services、checks) | 是 |
|
|
49
|
+
| — | `/m/api/git/*`(规划中) | 设计草案;v3.1.5 未注册这些路由 | 不适用 |
|
|
49
50
|
| GET | `/m/api/balance` | DeepSeek 官方余额 | 是 |
|
|
50
51
|
| GET | `/m/api/account-usage` | 用量与额度(DeepSeek / Codex / OpenCode Go) | 是 |
|
|
51
52
|
| GET | `/m/api/qr-config` | 桌面二维码数据(loopback only) | 否(loopback) |
|
|
@@ -62,7 +63,7 @@
|
|
|
62
63
|
|
|
63
64
|
### 2.1 App 自动更新(主机源,v3.x)
|
|
64
65
|
|
|
65
|
-
**概念**:插件配置 `updateDir`(如 `~/.dsh/mobile-remote/update
|
|
66
|
+
**概念**:插件配置 `updateDir`(如 `~/.dsh/mobile-remote/update/`,见 docs/06 §8.5)里放发布脚本产出的 APK 与 `manifest.json`。**manifest 是唯一权威**——App 只读 manifest,不枚举目录。
|
|
66
67
|
|
|
67
68
|
**`manifest.json` 契约**(由 `tools/gen-manifest.js` 统一生成,双端发布脚本共用):
|
|
68
69
|
|
|
@@ -118,7 +119,7 @@
|
|
|
118
119
|
}
|
|
119
120
|
```
|
|
120
121
|
|
|
121
|
-
- `urls
|
|
122
|
+
- `urls`:按优先级排列——默认「局域网私有地址优先」(`10./192.168.x` → `172.16-31` → Tailscale CGNAT `100.64/10` → 其他),首个非 internal IPv4 在前,loopback 最后;**仅当机器上没有任何局域网私有地址时**,才把 Tailscale CGNAT 提到最前(issue #49,避免校园网/热点等场景选出不可达地址);`port` 来自 `ctx.webServer.port`。v3.0.0:`lanBridge` 监听成功时首选地址为桥地址(`http://<IP>:<lanBridge.port>`,端口默认 3080),回环 webserver 地址仅作本机自连兜底。
|
|
122
123
|
- `agents[].status`:`"running" | "idle"`(映射自 agent 状态与最近事件推断)。
|
|
123
124
|
- `capabilities.turnOutline`:轮次大纲能力位(轮次导航)。宿主**真的注册了** `turnOutline` 投影单元、且投影感知观察面(`sessionQuery.observeSession`)可用时,返回 `{ "version": 1, "supported": true, "unloaded": true, "truncated": true }`——`unloaded` 表示未加载轮次也可跳转、`truncated` 表示超过体积上限会显式截断;否则返回 `{ "version": 1, "supported": false }`。**不按宿主版本号推断**:旧宿主/裁剪宿主显式声明不支持,App 据此退回「只用已加载轮次」的降级行为。SSE hello 的同名字段与该处**同判据、同形状**(见 §3.6 与 §6.18)。
|
|
124
125
|
- 未认证:`401`(见通用约定)。
|
|
@@ -162,7 +163,7 @@
|
|
|
162
163
|
**响应**:`200` 原始字节(`Content-Type` 按 `mediaType`,`Cache-Control: private, max-age=3600`,`x-attachment-meta` 带 width/height/bytes/name);`400` 缺参;`404 attachment-not-found`;其他错误透传。鉴权与其他端点一致(`x-mobile-token`/cookie)。
|
|
163
164
|
### 3.3 GET /m/api/sessions
|
|
164
165
|
|
|
165
|
-
|
|
166
|
+
**规划中的扩展响应示例**(当前 v3.1.5 仅返回原有字段,仍按 `lastActivity ?? createdAt` 排序;以下新增字段与排序规则待实现)
|
|
166
167
|
|
|
167
168
|
```json
|
|
168
169
|
{
|
|
@@ -175,21 +176,27 @@
|
|
|
175
176
|
"live": true,
|
|
176
177
|
"title": "会话标题(活动会话)",
|
|
177
178
|
"archived": false,
|
|
178
|
-
"lastActivity": 1750000123456
|
|
179
|
+
"lastActivity": 1750000123456,
|
|
180
|
+
"lastMessageAt": 1750000111000,
|
|
181
|
+
"origin": "subagent",
|
|
182
|
+
"parentSession": "session-parent"
|
|
179
183
|
}
|
|
180
184
|
]
|
|
181
185
|
}
|
|
182
186
|
```
|
|
183
187
|
|
|
184
|
-
-
|
|
188
|
+
- 排序(规划中):优先按 `lastMessageAt` 倒序,缺失时回退 `lastActivity`,再回退 `createdAt`;等值时以 `id` 为次级键保证顺序稳定。数据源:优先 `sessionQuery.listSessions()`(含休眠会话),回退 `ctx.sessions.list()`(仅活动会话)。
|
|
185
189
|
- `archived`:是否已归档(见 3.4 归档接口)。
|
|
186
|
-
- `lastActivity`:最近活跃时间(ms)。任意会话事件(SSE)与移动端"打开会话"(3.4 touch)都会更新,持久化于 `~/.dsh/mobile-remote/session-activity.json
|
|
190
|
+
- `lastActivity`:最近活跃时间(ms)。任意会话事件(SSE)与移动端"打开会话"(3.4 touch)都会更新,持久化于 `~/.dsh/mobile-remote/session-activity.json`。保留给旧版 App,且在消息时间缺失时仍作为排序回退值;旧 App 调用 `touch` 或内部事件发生时,这类会话仍可能重排。
|
|
191
|
+
- `lastMessageAt`(规划中,可选):最近一条用户/助手可见对话消息的时间(ms);无记录时为 `null`。打开会话不更新,工具活动与生命周期事件也不更新。字段缺失或为 `null` 时,客户端回退 `lastActivity`、再回退 `createdAt`,不提示降级;这项兼容回退不保证打开/内部事件后顺序始终不变。
|
|
192
|
+
- `origin`(规划中,可选):会话来源,子代理会话为 `"subagent"`;其余会话不返回。客户端据此隐藏子代理会话;字段缺失即不过滤。
|
|
193
|
+
- `parentSession`(规划中,可选):派生来源会话 id,为将来"折叠到父会话"预留。**不得**用它判定子代理会话——用户主动 fork 的会话只有该字段而没有 `origin`。
|
|
187
194
|
|
|
188
195
|
### 3.4 归档 / 活跃时间接口
|
|
189
196
|
|
|
190
197
|
| 方法 | 路径 | 说明 |
|
|
191
198
|
|---|---|---|
|
|
192
|
-
| POST | `/m/api/sessions/touch` | 标记会话被打开(更新 lastActivity
|
|
199
|
+
| POST | `/m/api/sessions/touch` | 标记会话被打开(更新 lastActivity);新版 App 将不再调用,端点保留给旧版 App |
|
|
193
200
|
| POST | `/m/api/sessions/archive` | 归档会话(映射内核 `workspace.archiveSession`,与 PC 端同一份状态) |
|
|
194
201
|
| POST | `/m/api/sessions/unarchive` | 恢复(取消归档) |
|
|
195
202
|
| POST | `/m/api/sessions/stop` | 停止(取消)会话当前运行(映射核心 RPC `session.cancel`) |
|
|
@@ -210,17 +217,9 @@
|
|
|
210
217
|
- `before`(可选):**上翻分页**——只返回 `seq < before` 的最近 `limit` 条事件(对话内滚动到顶部加载更早)。
|
|
211
218
|
- 三种模式优先级:`after` > `before` > 初始加载(缺省时返回最近 `limit` 条,即尾部)。
|
|
212
219
|
- `limit`(可选,默认 500,上限 1000):最多返回条数
|
|
213
|
-
**过滤规则**:返回可进入 Conversation timeline 的事件;`assistant/chunk` / `assistant/live-chunk` 只用于实时草稿,`agent/inbox/spliced` 由队列投影承载,`request/header`(含 system prompt/tool schema)、`request/context`、`session/
|
|
214
|
-
|
|
215
|
-
未知 type(即使带 `ignorable: true`)**保留 `seq`/`type` 进入时间线**(事件保真:不能因客户端尚未认识类型而静默丢弃),但**不下发详情指针**——`detail.available` 只对下方 allow-list 内的可见类型为真,其余类型调 3.5b 返回 `404 event-not-found`。这保证「新类型照常可见」与「未命名记录的原始载荷不外泄」同时成立(fail-closed)。
|
|
216
|
-
|
|
217
|
-
**详情可见类型(allow-list)**:`user/message`、`assistant/message`、`tool/call`、`tool/result`、`todo/write`、`turn/start`、`turn/end`。
|
|
218
|
-
|
|
219
|
-
判定依据:这几类正是 `summarizeEventCore` **逐个类型显式审过载荷**并下发给 App 的类型;其余类型走 default 分支只给 `{seq, type}`,App 从不消费其 data。`assistant/chunk` / `assistant/live-chunk` 虽被 App 消费,但属于实时草稿(不落 durable 历史),故不在名单内。
|
|
220
|
+
**过滤规则**:返回可进入 Conversation timeline 的事件;`assistant/chunk` / `assistant/live-chunk` 只用于实时草稿,`agent/inbox/spliced` 由队列投影承载,`request/header`(含 system prompt/tool schema)、`request/context`、`session/end-seed`、`step/start`、`step/end`、`system/message`、`assistant/attempt`(重试/中断的原始 stream 记录)与 `compaction/start` / `compaction/summary` / `compaction/prune` / `compaction/end`(压缩生命周期;summary 正文即替换 shadowed 区间后的上下文快照)属于内部/重建/快照元数据,历史、实时与 3.5b 详情三处都不返回。`compaction/end` 作为不落卡片的实时控制帧通知 App 重新加载 durable snapshot。未知 type(即使带 `ignorable: true`)保留 `seq`/`type`,但不提供详情指针;由于详情端点采用 fail-closed allow-list,未知类型请求详情会返回 `404 event-not-found`。只有经审查并列入 allow-list 的可见类型可通过 3.5b 读取详情。
|
|
220
221
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
> 客户端呈现约定:`session/title`、`model/selection`、`sandbox/mode`、`agent-preset/selected`、`subagent/*`、`team/*`、`feedback/*`、`command/*`、`approval/policy`、`tool/ptc-dispatch*`、`goal/change`、`plan/mode`、`permission/preset`、`schedule/change`、`hook/*`、`llm/retry*`、`deliverables/presented`、`tool-workflow/*`、`session-log-deepseek/delivery-accepted`、`subagent/model-selection-policy` 属协议/运行时元数据——仍下发并保留 `seq`,但普通模式不渲染,调试模式可展开审阅(见 05-test-cases F-34)。`approval/asked` / `approval/decided` 是 durable 审批记录,普通模式以可读标题呈现(历史回放的权威源;问询只有瞬态帧,不保证回放)。
|
|
222
|
+
> 客户端呈现约定:`session/title`、`model/selection`、`sandbox/mode`、`agent-preset/selected`、`subagent/*`、`team/*`、`feedback/*`、`command/*`、`approval/policy`、`tool/ptc-dispatch*`、`goal/change` 属协议/运行时元数据;普通模式不渲染,调试模式按摘要可用字段展示。只有 `user/message`、`assistant/message`、`tool/call`、`tool/result`、`todo/write`、`turn/start`、`turn/end` 在服务端详情 allow-list 中,其他事件没有详情指针(见 3.5b)。`approval/asked` / `approval/decided` 是 durable 审批记录,普通模式以可读标题呈现(历史回放的权威源;问询只有瞬态帧,不保证回放)。
|
|
224
223
|
**响应 200**
|
|
225
224
|
|
|
226
225
|
```json
|
|
@@ -261,9 +260,7 @@
|
|
|
261
260
|
|
|
262
261
|
当服务端只能从 seeded session 的当前 surface 读取时,成功响应额外包含 `"degraded": true, "detailMode": "current-surface"`;客户端必须保留该元数据并提示详情可能不完整。详情不存在或属于内部/敏感类型返回 `404 event-not-found`;单事件详情超过 8 MiB 返回 `413 event-detail-too-large`。
|
|
263
262
|
|
|
264
|
-
稳定错误矩阵:`session-not-found`(会话不存在)、`event-not-found`(seq
|
|
265
|
-
|
|
266
|
-
> 命名说明:本端点的读取失败码是 **`event-read-failed`**(单事件级),与 `/m/api/history` 的 **`session-read-failed`**(整会话回放失败)语义不同,**不要**为"统一"而合并——App 侧对两者有各自的文案与重试路径。
|
|
263
|
+
稳定错误矩阵:`session-not-found`(会话不存在)、`event-not-found`(seq 不存在/不可见)、`session-corrupt`(会话数据损坏)、`event-read-failed`(读取失败)和 `event-detail-too-large`(超过 8 MiB)。错误响应不得泄露主机路径或原始异常;旧服务端未保存详情时客户端显示安全错误并提供重试,不猜测重建。
|
|
267
264
|
|
|
268
265
|
**规范化正文(`text`)**:`assistant/message` 的详情响应额外附 `data.text`,由服务端用**与事件摘要同一个 `blocksToText`** 提取(只拼 `type == "text"` 的块,跳过 `reasoning` 与内部块),因此与摘要下发的 `text` 同源同规则。客户端必须直接采用该字段作为正文,**不得自行递归拼接 `message.content`**——那会把 `reasoning` 块并进正文,使思维链在折叠块之外重复出现(issue #1 需求变更记录)。`tool/result` 等其它类型的详情仍以原始事件载荷为准;原始 `message` 块原样保留。
|
|
269
266
|
|
|
@@ -287,7 +284,7 @@
|
|
|
287
284
|
| `tool/result` | `{ callId, name, isError, text, images? }`;长结果可通过详情端点获取;**不再下发 `files`**(issue #1 需求变更:工具产出文件不上时间线;图片仍带 `images`) |
|
|
288
285
|
| `turn/start` | `{ turn: number }` |
|
|
289
286
|
| `turn/end` | `{ turn: number, reason: object }` |
|
|
290
|
-
|
|
|
287
|
+
| 其他可见事件 | 保留 `type`、`seq`,未知类型使用通用卡保留顺序;不提供详情指针,也不允许通过 3.5b 读取原始载荷 |
|
|
291
288
|
|
|
292
289
|
**控制帧**:连接建立后立即 `data: {"type":"hello","serverTime":...,"capabilities":{"eventTimeline":{...},"turnOutline":{...}}}`;每 25s `: ping` 注释行。bootstrap 同样返回这两项能力;`turnOutline` 与 bootstrap 同判据、同形状(见 §6.18),客户端应按能力协商而不是按版本号猜测。
|
|
293
290
|
**错误语义**:鉴权失败在连接建立阶段以 `401` HTTP 状态返回(EventSource 会触发 error 事件,客户端转登录态)。
|
|
@@ -299,13 +296,14 @@
|
|
|
299
296
|
| type | 说明 |
|
|
300
297
|
|---|---|
|
|
301
298
|
| `session/context` | `{ sessionId, contextWindow }`——模型上下文窗口(`request/context` 事件,PC 端圆环同源) |
|
|
302
|
-
| `agent/status` | `{ agentId, sessionId, status, child }
|
|
299
|
+
| `agent/status` | `{ agentId, sessionId, status, child }`——`status` 只有 `running` / `idle` 两值(内核 `AgentStatus` 即为二元联合);`sessionId` 为去 `session:` 前缀的会话 id,`child` 标记子代理会话(v2.7.2 起携带后两字段)。**不存在 `waiting`**:等待用户审批/作答时 agent 仍为 `running`,该事实由 `mobile/frame` 的挂起问询/审批承载 |
|
|
303
300
|
| `notifications/changed` | 通知记录增删(如移动端删除后),客户端刷新列表与角标 |
|
|
304
301
|
| `mobile/notify` | `{ notification: { id, kind, sessionId, title, detail, time } }`——插件"真结束"判定后推送的通知(completed / failed / needs-answer),悬浮球/App 与通知中心同源渲染(v2.7.2) |
|
|
305
302
|
| `mobile/frame` | 内核瞬态帧(问询/审批)。`frame` 字段为 `question/requested`(含 `rpcId`、`questions[]`)、`question/resolved`(`questionRpcId`)、`approval/requested`(`rpcId`、`approvalId`、`toolName`、`reason?`)、`approval/resolved`(`approvalId`)。**App 断线重连时服务端补发挂起的待答帧**(`pendingFrames` 回放) |
|
|
306
303
|
| `mobile/queue` | `{ sessionId, rows: [{ id, text, placement }] }`——内核队列快照(`agent/inbox/spliced` 即时镜像,v3.0.2):认领/删除/编辑实时反映,App 端 dock 以此为权威源(`placement`: `queued` / `steering` / `context`,与 GET /queue 同款形状);断线重连时 mux 回放当前队列 |
|
|
304
|
+
| `git/changed`(规划中) | 设计目标为 `{ repositoryId }`;当前版本未实现 Git provider,也不会推送此帧 |
|
|
307
305
|
|
|
308
|
-
客户端应按 `type` 分派;未知 type
|
|
306
|
+
客户端应按 `type` 分派;未知 type 使用通用事件卡保留顺序(前向兼容)。当前服务端故意不给未知 type 详情指针,按需详情端点对其返回 `404 event-not-found`,避免未经审查地暴露事件载荷。
|
|
309
307
|
### 3.7 GET /m/qr.png
|
|
310
308
|
|
|
311
309
|
**查询参数**:`text`(必填,二维码内容,URL 编码)。无 `text` → `400`。 **响应**:`200 image/png`(qrcode 包生成,尺寸 512,纠错级别 M)。
|
|
@@ -324,6 +322,10 @@
|
|
|
324
322
|
| 405 | `method-not-allowed` | 方法不支持(GET 端点收到 POST 等) |
|
|
325
323
|
| 503 | `no-live-agent` | 无运行中 agent |
|
|
326
324
|
| 503 | `agents-unavailable` | agents 服务不可用 |
|
|
325
|
+
| 503 | `git-provider-unavailable` | DSH subprocess/git provider 不可用 |
|
|
326
|
+
| 403 | `workspace-not-allowed` | 仓库不在已注册工作区边界内 |
|
|
327
|
+
| 404 | `not-git-repository` | 工作区不是 Git 仓库 |
|
|
328
|
+
| 400 | `git-command-failed` | Git 只读命令失败 |
|
|
327
329
|
|
|
328
330
|
## 5. 版本兼容策略
|
|
329
331
|
|
|
@@ -350,7 +352,7 @@
|
|
|
350
352
|
| POST | `/m/api/directories` | 新建文件夹(v2.1) |
|
|
351
353
|
| GET | `/m/api/diagnostics` | 环境诊断(服务端端点实测,v2.1) |
|
|
352
354
|
| GET | `/m/api/balance` | DeepSeek 官方余额(服务端代查,v2.1) |
|
|
353
|
-
| GET | `/m/api/account-usage` | 用量与额度(服务端代查,v3.
|
|
355
|
+
| GET | `/m/api/account-usage` | 用量与额度(服务端代查,v3.1.5) |
|
|
354
356
|
| GET | `/m/api/turn-outline` | 会话轮次大纲(只读;完整阶梯的轮次摘要,轮次导航) |
|
|
355
357
|
| GET | `/m/api/qr-config` | 桌面二维码数据(loopback only,v2.1) |
|
|
356
358
|
| POST | `/m/api/defaults` | 修改默认 Agent/权限预设(v2.1) |
|
|
@@ -496,7 +498,7 @@
|
|
|
496
498
|
|
|
497
499
|
- `outcome`:`allowed-once` | `rejected`。
|
|
498
500
|
|
|
499
|
-
**取消**(`kind: "cancel"
|
|
501
|
+
**取消**(`kind: "cancel"`):必须携带对应 `sessionId`;问询以 `UserQuestionError`/`ASK_CANCELLED` rejection 结算,审批以 `cancelled` 结算。
|
|
500
502
|
|
|
501
503
|
响应:`200 { "ok": true, "accepted": true }`;`accepted: false` + `reason`(如 `not-pending`,PC 端已先回答)。
|
|
502
504
|
|
|
@@ -673,7 +675,113 @@
|
|
|
673
675
|
- `commands.execute` 外层套 `AbortSignal.timeout(15000)`:15s 是**整个命令**的上限。`/compact` 之类会等一次完整的 LLM 摘要,长会话可能超时 → `504 commands-execute-failed`;客户端此时**不能断言失败**(结果未知)
|
|
674
676
|
- `result` 为内核 settle 对象(`commandId` + `result.{kind,text}`),与 PC 端一致
|
|
675
677
|
|
|
676
|
-
###
|
|
678
|
+
### Git API 设计草案(本版本未实现)
|
|
679
|
+
|
|
680
|
+
> 以下 Git endpoints 是设计目标,不属于 v3.1.5 已实现 API;当前 `lib/index.js` 未注册 `/m/api/git/*` 路由,客户端不得依赖这些端点。设计背景见 `docs/design/mobile-git.md` 与 `docs/adr/` 中相关 ADR。
|
|
681
|
+
|
|
682
|
+
Git 接口计划由项目自有 provider 提供,执行通过 DSH `subprocess`,仓库必须位于
|
|
683
|
+
`workspaceRegistry` 已注册工作区内。`repositoryId` 为 provider 分配的不透明仓库标识,客户端不得自行拼接
|
|
684
|
+
shell 命令。Slice A 是只读基础;B1/B2 提供受保护的 stage/commit 与本地分支写操作;B3 增加明确目标的 fetch/pull/push/sync 及冲突 abort,仍不提供删除分支、stash、tags、force push 或手机端冲突编辑。
|
|
685
|
+
|
|
686
|
+
`GET /m/api/git/capabilities` 始终返回 `{ok, git:{available,read,writes,reason,features}}`,不可用时
|
|
687
|
+
由 `available=false` 明确表达;实际仓库操作在 provider 不可用时返回 `503 git-provider-unavailable`。
|
|
688
|
+
`GET /m/api/git/context?sessionId=…` 返回不透明的稳定 `repositoryId`、仓库名称和能力,不返回主机路径。
|
|
689
|
+
其余接口均要求 `repositoryId`:`status` 返回分支与文件状态,`branches` 返回本地/远端分支。
|
|
690
|
+
客户端不得将主机路径当作 B1 写操作的 `repositoryId`;服务端仅接受由 `context` 返回、并在授权工作区内解析的仓库标识。
|
|
691
|
+
`graph` 支持 `limit/cursor` 和可选 JSON `refs`(最多 5 个 `{name,tipOid}` 引用对;本地与远程引用合计);首次请求返回
|
|
692
|
+
`snapshotId`、绑定的 `tips`、提交页和 `nextCursor`,后续请求必须使用同一快照的不透明游标。
|
|
693
|
+
引用移动、删除、tip 不匹配、仓库变化、游标上下文错误或快照过期返回 `409 graph-stale`,客户端不得
|
|
694
|
+
静默回退到全量图。`commit` 要求 `oid`,`diff` 支持 `kind=working|staged|commit`、`oid/path`。
|
|
695
|
+
越过工作区边界返回 `403 workspace-not-allowed`,非 Git 目录返回 `404 not-git-repository`。
|
|
696
|
+
|
|
697
|
+
#### Slice B0 操作任务
|
|
698
|
+
|
|
699
|
+
B0 在同一 Git 移动契约下提供持久化任务查询基础设施。`GET /m/api/git/operations` 支持
|
|
700
|
+
`repositoryId`、`status`、`limit`(1–100)和不透明 `cursor`;`GET
|
|
701
|
+
/m/api/git/operations/:operationId` 查询单项任务。查询响应包含 `operationId`、`requestId`、`repositoryId`、`kind`、`status`、`revision`、阶段、可取消性、结果/脱敏错误和恢复阻塞事实。
|
|
702
|
+
|
|
703
|
+
`POST /m/api/git/operations/:operationId/cancel` 的 JSON 请求体必须包含新的控制
|
|
704
|
+
`requestId` 和查询时的 `expectedRevision`。它只接受 `queued` 或 `running`;排队任务立即进入
|
|
705
|
+
`cancelled`,运行任务先持久化取消请求再尽力终止子进程,不能宣称已回滚。重复的相同控制
|
|
706
|
+
`requestId` 返回原结果,revision 不匹配返回 `409 state-changed`。
|
|
707
|
+
|
|
708
|
+
任务状态为 `queued`、`running` 或终态 `succeeded`、`failed`、`cancelled`、`conflicted`、
|
|
709
|
+
`unknown-result`。`POST /m/api/git/recovery/acknowledge` 必须携带新的控制
|
|
710
|
+
`requestId`、`operationId`、`repositoryId` 和 `expectedRevision`;它只解除用户已查看事实后的
|
|
711
|
+
恢复阻塞,不重放旧操作。SSE `/m/api/events` 增加 `git/operation` 帧,包含完整操作视图和单调
|
|
712
|
+
`revision`;B1 的 `git/changed` 帧仍至少包含 `repositoryId`,并可带 `changeKinds`(如 `index`、`head`)提示
|
|
713
|
+
客户端刷新对应事实。事件重复或丢失都不改变账本事实,客户端重连后必须重新查询 operationId。
|
|
714
|
+
B0 不定义具体 Git 写操作;B1/B2 写端点复用 B0 任务账本。所有返回 `202` 的执行端点统一返回 accepted DTO:
|
|
715
|
+
`{ok:true,accepted:true,operationId,requestId,status,deduplicated,queryUrl,queryLink,operation}`。
|
|
716
|
+
其中 `queryUrl`/`queryLink` 是可直接 GET 的相对链接 `/git/operations/:operationId`;客户端应按该链接查询,不能把 `operation` 快照当作最终结果。
|
|
717
|
+
`git.capabilities.operations` 用于声明任务账本、幂等、恢复和取消基础设施是否可用。
|
|
718
|
+
|
|
719
|
+
#### Slice B1 暂存与精确提交
|
|
720
|
+
|
|
721
|
+
`POST /m/api/git/change-sets` 请求 `{repositoryId,kind}`,其中 `kind` 为 `working|staged`;响应返回
|
|
722
|
+
短期 `changeSetId`、`stateVersion`、`preconditionToken` 和文件/可选 hunk 清单。客户端只提交
|
|
723
|
+
`fileId` 与 `hunkId`,不得提交 patch;未跟踪、二进制和重命名首版仅支持整文件操作。
|
|
724
|
+
|
|
725
|
+
`POST /m/api/git/stage` 与 `/m/api/git/unstage` 请求 `{repositoryId,requestId,changeSetId,
|
|
726
|
+
preconditionToken,selections}`,`selections` 为 `[{fileId,hunkIds?}]`,立即返回 `202` 的 Git 操作任务。
|
|
727
|
+
服务端在私有临时 index 中执行并通过 Git index lock 原子安装,change-set 事实变化返回
|
|
728
|
+
`409 state-changed` 或 `409 hunk-stale`,不会部分应用。
|
|
729
|
+
|
|
730
|
+
`POST /m/api/git/commit/preflight` 请求 `{repositoryId,message}`,响应返回 staged tree、当前 HEAD、
|
|
731
|
+
本地分支、提交身份和绑定这些事实的 `preconditionToken`。随后 `POST /m/api/git/commit` 请求
|
|
732
|
+
`{repositoryId,requestId,message,preconditionToken,confirm:true}`,立即返回 `202` 任务;提交任务使用
|
|
733
|
+
`commit-tree` 加 expected HEAD 的 `update-ref` CAS,不运行 hooks,HEAD/tree 变化返回
|
|
734
|
+
`409 state-changed`。任务成功结果包含新 commit OID、tree OID 和分支。
|
|
735
|
+
|
|
736
|
+
#### Slice B2 本地分支与受保护切换
|
|
737
|
+
|
|
738
|
+
`POST /m/api/git/branches/preflight` 的 `action` 只能为 `create|rename`(缺失或其他值均为
|
|
739
|
+
`400 invalid-argument`)。预检返回规范化的 `params`;执行请求必须携带**同一个** `params` 对象,另加
|
|
740
|
+
`{repositoryId,requestId,preconditionToken}`。创建参数为 `{name,startOid,remoteRef?}`(默认从当前 HEAD,
|
|
741
|
+
远端起点必须是已验证且仍存在的精确 `refs/remotes/<remote>/<branch>`);rename 参数为
|
|
742
|
+
`{oldName,name,oldOid}`。创建成功不会自动切换。
|
|
743
|
+
|
|
744
|
+
`POST /m/api/git/branch-rename` 使用 `{repositoryId,requestId,params:{oldName,name,oldOid},preconditionToken}`,
|
|
745
|
+
只重命名本地分支,不修改远端引用。非法/重复/不存在分支分别返回稳定的 `invalid-argument`、
|
|
746
|
+
`branch-exists` 或 `branch-not-found`。
|
|
747
|
+
|
|
748
|
+
`POST /m/api/git/branch-switch/preflight` 的 action 固定为 `switch`,使用 `targetBranch` 或已验证的
|
|
749
|
+
`targetRef`。远端引用必须是 provider 精确验证的 `refs/remotes/<remote>/<branch>`;远端切换必须明确
|
|
750
|
+
`localName`,该值同时成为规范 `params.targetBranch`,不会隐式猜测本地名称。返回目标 OID 与影响摘要。
|
|
751
|
+
干净工作区或 Git 可安全携带的改动返回 `safe:true` 和 token;存在覆盖风险时返回 `safe:false` 及
|
|
752
|
+
`allowedActions: ["commit","computer","cancel"]`,不签发强制切换 token。`POST /m/api/git/branch-switch`
|
|
753
|
+
必须提交预检返回的同一 `params`,只执行无 force 的安全切换;所有分支写操作均作为 B0 Git 操作任务返回
|
|
754
|
+
`202` accepted DTO。
|
|
755
|
+
|
|
756
|
+
#### Slice B3 远端同步
|
|
757
|
+
|
|
758
|
+
B3 的写请求必须携带 `X-DSH-Git-Contract: 2.x`(或等价的
|
|
759
|
+
`X-DSH-Git-Mobile-Contract`)版本声明;缺失或主版本不兼容返回
|
|
760
|
+
`409 client-incompatible`。所有执行端点只接受预检返回的规范 `params` 和短期
|
|
761
|
+
`preconditionToken`,不接受远端 URL、任意 refspec、OID 或命令选项。
|
|
762
|
+
|
|
763
|
+
- `GET /m/api/git/remotes?repositoryId=…` 返回已配置 remote、脱敏 URL、remote-tracking 分支和本地 upstream;远端凭据不进入 DTO、账本或错误。
|
|
764
|
+
- `POST /m/api/git/fetch/preflight` 请求 `{repositoryId,remote,branch}`,随后
|
|
765
|
+
`POST /m/api/git/fetch` 执行只更新对应 `refs/remotes/<remote>/<branch>` 的 fetch。
|
|
766
|
+
- `POST /m/api/git/pull/preflight` 请求 `{repositoryId,remote?,branch?,localBranch?,strategy?}`,随后
|
|
767
|
+
`POST /m/api/git/pull` 执行 fetch→固定 OID 的 merge/rebase 两阶段;默认策略为 merge。
|
|
768
|
+
- `POST /m/api/git/push/preflight` 请求 `{repositoryId,remote,branch,localBranch?,setUpstream?}`,随后
|
|
769
|
+
`POST /m/api/git/push` 执行明确的 `refs/heads/<local>:refs/heads/<branch>` 普通 push;目标不存在时允许创建该单一目标 ref。
|
|
770
|
+
force/force-with-lease 永不支持;只有远端确认成功后才写入 upstream 配置。
|
|
771
|
+
- `POST /m/api/git/sync/preflight` 与 `POST /m/api/git/sync` 执行非原子的
|
|
772
|
+
fetch→pull→push;新建的远端目标会跳过没有目标可取的 fetch/integrate 阶段,直接以普通 push 创建单一目标 ref。
|
|
773
|
+
操作查询中的 `stages[]` 保存每阶段状态、跳过原因、前后事实和副作用;任一阶段失败、冲突、取消或不确定都会停止后续阶段并保留部分成功结果。
|
|
774
|
+
- `POST /m/api/git/abort/preflight` 返回当前 merge/rebase 中间态的影响摘要;随后先经
|
|
775
|
+
`POST /m/api/git/confirmations` 签发一次性挑战,再由 `POST /m/api/git/abort` 创建独立 abort 任务;签发记录可跨服务重启幂等复用,挑战消费和任务创建在同一持久化账本事务中完成。
|
|
776
|
+
abort 不由普通 cancel 隐式执行。
|
|
777
|
+
- `POST /m/api/git/operations/:operationId/handoff` 的 `target` 为 `computer|model`,只记录冲突交接意图;移动端不自动 continue、commit 或 push。
|
|
778
|
+
|
|
779
|
+
所有 B3 执行端点返回 `202` accepted DTO。fetch/pull/push/sync/abort 使用不同协调域:
|
|
780
|
+
fetch/push 序列化 common domain,pull/sync/abort 同时序列化 common 与 worktree domain。
|
|
781
|
+
远端认证、网络、远端拒绝、非 fast-forward 分别映射为稳定错误;连接中断或结果无法
|
|
782
|
+
从读事实证明时进入 `unknown-result`,而不是自动重试。
|
|
783
|
+
|
|
784
|
+
### 6.16 GET /m/api/account-usage(用量与额度,v3.1.5)
|
|
677
785
|
|
|
678
786
|
服务端并发查询三个固定来源,凭据只在电脑端解析和使用;手机端只接收成功来源的脱敏投影。默认使用电脑进程内 60 秒快照;详情页顶部刷新使用 `?refresh=1` 绕过快照并发起一轮新的查询(2 秒内重复刷新受保护)。来源固定按 DeepSeek → Codex → OpenCode Go 排列,未配置/未登录来源不返回,已配置但本次查询失败的来源也不返回。
|
|
679
787
|
|
|
@@ -712,21 +820,23 @@
|
|
|
712
820
|
- 查询快照在电脑进程内缓存 60 秒并共享并发请求;手动 `refresh=1` 的连点在 2 秒内复用快照;不写手机或电脑持久化文件。配额窗口不相加,有限 Credits 和个人消费上限分别展示。
|
|
713
821
|
- HTTP 错误只返回通用 `502 account-usage-failed`(单来源失败不会使整体请求失败)。
|
|
714
822
|
|
|
715
|
-
**悬浮球面板消费语义(v3.
|
|
823
|
+
**悬浮球面板消费语义(v3.1.5,ADR 0008)**:原生悬浮球面板把本端点当作「展开时按需读取」的快照——每次展开面板最多触发一次查询(客户端 2 分钟节流,不并入面板打开期间的 5 秒轮询,也不做后台周期轮询);成功快照在进程内保留,失败时沿用旧值并标注相对时间;投影不可用(旧插件 404 / 未配置 / 全部失败)时,面板整体降级为原有的单行 DeepSeek 余额。面板按「一个来源一行」渲染:金额来源保留文字(CNY 优先),配额来源只显示主 bucket 窗口的细条与颜色、不出百分比数字,每个进度条上方居中显示窗口短标签(5h / 每周 / 每月,与详情页窗口语义一致);附加 bucket(服务端命名的 `名称 · 5h`)与账户身份不出现 overlay 面板,详情页仍是完整信息入口(`displayName`/`maskedEmail`、Credits、个人消费上限、重置时间)。
|
|
716
824
|
|
|
717
825
|
### 6.17 文件传输(v3.1.2,B站 csborbbnc 反馈)
|
|
718
826
|
|
|
719
827
|
**GET `/m/api/files?path=…`** — 下载电脑文件
|
|
720
828
|
- 响应:`200` 文件流(`content-type` 按扩展名推断、`content-disposition: attachment` 带 UTF-8 文件名);路径不存在或非文件 → `404 file-not-found`;缺 `path` → `400 bad-request`
|
|
721
|
-
- 与目录选择器(§6.
|
|
829
|
+
- 与目录选择器(§6.2)同信任模型:口令鉴权 + 现有限流,路径由手机显式指定;服务端先打开句柄、`fstat` 复核为普通文件,再按 descriptor 真实路径做工作区包含校验,符号链接/越界路径一律 `404`
|
|
830
|
+
- 平台范围:descriptor-relative 保护需 procfs/devfs,**仅 Linux/macOS 提供**;Windows 返回 `503 files-unavailable`(安全 fail-closed,见 docs/04 §6)
|
|
722
831
|
|
|
723
832
|
**POST `/m/api/files/upload`** — 上传文件到电脑(写会话工作目录)
|
|
724
833
|
```json
|
|
725
834
|
请求: { "sessionId": "…(可选)", "name": "README.md", "data": "<base64>" }
|
|
726
835
|
响应: { "ok": true, "path": "F:\\DSH-Outpost\\README.md", "bytes": 1234 }
|
|
727
836
|
```
|
|
728
|
-
- 目标目录:`sessionId` 对应 agent 的工作目录 → 缺省回退第一个注册工作区根;无法确定 → `
|
|
729
|
-
-
|
|
837
|
+
- 目标目录:`sessionId` 对应 agent 的工作目录 → 缺省回退第一个注册工作区根;无法确定 → `404 workspace-not-found`(不再静默写入其它 workspace)
|
|
838
|
+
- 服务端先打开并校验目录句柄,再通过 descriptor-relative + `O_CREAT|O_EXCL|O_NOFOLLOW` 创建目标,目录替换/符号链接竞态不会越界;当前平台不具备 descriptor fs 时 fail-closed → `503 files-unavailable`
|
|
839
|
+
- `name` 不合法(含 `\ / : * ? " < > |`、`.`/`..`/超 255)→ `400 invalid-name`;同名已存在 → `409 file-exists`;body 上限 64MB → `413 payload-too-large`
|
|
730
840
|
|
|
731
841
|
### 6.18 GET /m/api/turn-outline(轮次大纲,轮次导航)
|
|
732
842
|
|
|
@@ -813,4 +923,8 @@
|
|
|
813
923
|
|
|
814
924
|
|
|
815
925
|
|
|
926
|
+
### 6.18 GET /m/api/diagnostics
|
|
927
|
+
|
|
928
|
+
返回当前插件的运行诊断:`plugin` 标识插件名与版本;`runtime` 描述插件进程、WebServer 监听地址、鉴权、LAN 桥及运行指标;`services` 列出探测到的宿主服务;`checks` 提供功能检查结果;`notes` 提供可读提示。
|
|
816
929
|
|
|
930
|
+
`checks.approvalMode` 表示问询/审批策略,`checks.remoteEvents` 表示 `$events` 双端通道是否就绪。旧宿主的 `apiProxy` / `respondBridge` / `frameBridge` 项仅在相应服务被探测到时出现。当前响应不包含宿主版本、`host.capabilities` 或统一受支持范围;不能根据插件版本推断宿主能力。
|
package/docs/04-security.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 04 安全设计文档 — dsh-mobile-remote
|
|
2
2
|
|
|
3
|
-
> 版本:v3.
|
|
3
|
+
> 版本:v3.1.5 · 状态:已实现 · 配套:02-architecture.md、03-api.md、09-compatibility.md
|
|
4
4
|
|
|
5
5
|
## 1. 威胁模型
|
|
6
6
|
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
**这是新增攻击面,边界在代码中强制(`lib/index.js`「LAN 桥」段):**
|
|
62
62
|
1. **只转发移动 API 面**:`pathname.startsWith(/m/api)` 之外一律 404——桌面 `/api` RPC 网关、`/m/api/qr-config`(含口令)、`/m/qr.png`(仅本机语义)、一切其他 `/m/*` 面均不转发;Host 头重写为 `127.0.0.1:<webServer.port>` 使下游 hostAllowed 自然放行(真实鉴权仍由口令把关)。
|
|
63
63
|
2. **口令强制**:未配置 `authToken`、或口令短于 16 字符 → **拒绝启动桥**(仅警告不断言;schema 不设 min 以免破坏既有用户)并记日志。
|
|
64
|
-
3. **资源防护**:`maxConnections=128`、headersTimeout 15s、requestTimeout
|
|
64
|
+
3. **资源防护**:`maxConnections=128`、headersTimeout 15s、requestTimeout 180s、upstream SSE 60s/普通请求 180s 超时、在网连接随插件卸载销毁。
|
|
65
65
|
4. **暴露条件**:桥默认关闭;开启即等于「0.0.0.0 移动 API 可达」——前置条件:可信 WiFi + ≥16 字符强口令(同 §2.1)。`/m/api/diagnostics` 的 `runtime.lanBridge.listening` 可确认监听状态(绑定失败时 QR/地址自动回退,不会指向死端口)。
|
|
66
66
|
5. **与 §3 表格关系**:桥上的流量走「局域网(可信 WiFi)明文」档位,残余风险与缓解同 §3 末。
|
|
67
67
|
|
|
@@ -78,10 +78,14 @@
|
|
|
78
78
|
- 插件**持久化仅限最小移动端状态**(`~/.dsh/mobile-remote/`:通知已读 id、会话最近活跃时间),不含会话内容;口令只存在于 profile 配置。
|
|
79
79
|
- 会话内容沿用 dsh 现有策略存储;插件仅在请求处理期间持有内存副本。
|
|
80
80
|
- **二维码数据端点 `/m/api/qr-config` 仅允许 loopback**(TCP socket 来源校验,无法伪造)—— 桌面设置页读取后展示二维码;二维码含地址+口令,请勿截屏转发。
|
|
81
|
-
- **问询/审批应答(`/m/api/respond
|
|
81
|
+
- **问询/审批应答(`/m/api/respond`)不绕过内核安全**:RC1 路径先在插件侧按原始问题严格校验答案(每项必须有 `selected: string[]`,`custom` 若存在必须为字符串,选项/数量/重复项均校验),再通过 `$events/result` 交给内核;旧 `apiProxy` 路径仍由内核 schema 校验。rpcId 与 sessionId 必须命中对应 pending 表(先到先得,不可伪造待答)。取消操作同样以 `UserQuestionError`/`ASK_CANCELLED` rejection 走内核语义。
|
|
82
|
+
- **文件传输(`/m/api/files*`)**:与目录选择器同信任模型(§2 口令鉴权 + 手机端显式指定路径),**不做工作区包含校验**。下载按给定路径 `resolve` 后 `statSync` 确认为普通文件再流式读取;上传把文件名限制在目标会话工作目录内并拒绝路径分隔符与 NUL。已认证用户本就持有 agent 控制权,故该通道不额外收窄路径范围。
|
|
83
|
+
- **已知边界**:由于不校验工作区包含、也不使用 `O_NOFOLLOW` / 句柄级复核,工作区内指向外部的符号链接,以及 `statSync → createReadStream` 之间的目标替换,都不受保护;能读到进程有权读取的任意文件。
|
|
84
|
+
- **边界外的目录选择器**(`/m/api/directories`)同样是**有意保留的任意路径浏览**(新建会话需跨盘选工作目录),不适用工作区包含语义。
|
|
85
|
+
- **后续**:是否将文件传输收紧到工作区包含 + descriptor-relative 句柄保护,见 GitLab `ahedu/dsh-mobile-remote#16`;在决定前,本文件如实描述当前实现,不描述未落地的保护。
|
|
82
86
|
- **第三方推送通道脱敏(v2.6)**:Server酱/ntfy/Bark/generic 等推送默认只收到「事件类型 + 会话短码」(`pushContent: minimal`),会话标题/错误详情等核心内容默认不出本机;仅显式配置 `pushContent: standard` 后外发——第三方服务不可信。
|
|
83
|
-
- **用量与额度投影(v3.
|
|
84
|
-
- **悬浮球 overlay 面板脱敏(v3.
|
|
87
|
+
- **用量与额度投影(v3.1.5)**:`/m/api/account-usage` 只返回成功来源的余额/配额、脱敏 Codex 账户标签和汇总失败数;DeepSeek/OpenCode Go 密钥与 Codex OAuth token 永不进入响应、App 日志或持久化文件。OpenCode Go 只请求固定官方 HTTPS 端点;Codex 启用代理但代理不可用时拒绝直连。服务端只保留 60 秒进程内快照,单来源失败不回退到过期数据。
|
|
88
|
+
- **悬浮球 overlay 面板脱敏(v3.1.5,ADR 0008)**:悬浮球是 **`TYPE_APPLICATION_OVERLAY` 系统级浮层**,可能出现在锁屏、他人可见、或覆盖其它应用之上。因此面板的用量与额度区块只展示来源标题、金额文字与配额细条/颜色;**不显示账户身份(displayName/maskedEmail)**、不显示任何聚合或换算数值;配额窗口永不相加。数据仅按需获取(展开面板时,客户端节流)、失败沿用旧值并标注相对时间,不新增后台轮询或持久化。
|
|
85
89
|
## 7. 安全测试要点(并入 05-test-cases.md)
|
|
86
90
|
1. 口令启用后:未认证访问 bootstrap/send/events/history 返回 401;错误口令 401。
|
|
87
91
|
2. 口令关闭时:以上端点返回 200。
|
package/docs/05-test-cases.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# 05 测试用例设计文档 — dsh-mobile-remote
|
|
2
2
|
|
|
3
|
-
> 版本:v3.
|
|
3
|
+
> 版本:v3.1.5(功能用例见 F-24~F-35;v3.1.1 用例见 F-23) · 配套:03-api.md、04-security.md
|
|
4
4
|
> 环境:Windows + DSH Desktop(desktop profile,内核 0.1.1-rc.2;web profile 亦适用) + Android(DSH Remote App)
|
|
5
5
|
> 前置:插件已安装并启用(LAN 桥监听 0.0.0.0:3080);访问口令为安装时生成的随机串(下文 `<TOKEN>`)。
|
|
6
6
|
## 1. 测试范围与环境
|
|
7
7
|
- 功能:认证、发消息、事件回流、历史、会话、通知、新建会话、目录、默认配置、二维码。
|
|
8
|
+
- Git 移动端:仅有设计草案,当前版本未实现 `/m/api/git/*`;不要将 `docs/design/mobile-git.md` 中的目标验证视为已执行用例。
|
|
8
9
|
- 安全:口令校验、Host 校验、loopback 限制。
|
|
9
10
|
- 兼容:Android 深色/浅色主题。
|
|
10
11
|
- 自动化:`tools/e2e-check.mjs`(Node ≥ 20,`DSH_MOBILE_TOKEN` 环境变量)覆盖核心 API 链路。
|
|
12
|
+
|
|
11
13
|
## 2. 功能测试用例
|
|
12
14
|
|
|
13
15
|
### F-01 认证:未携带凭证访问 API
|
|
@@ -187,7 +189,7 @@
|
|
|
187
189
|
| 变体 B | 旧版 App(≤v3.0.0)+ 新版插件:浏览路径为 `/\home` 形态,服务端归一化后仍可正常进入/选择(`//home` 在 POSIX 与 `/home` 等价) |
|
|
188
190
|
| 单测 | `flutter test test/dirpicker_logic_test.dart`(joinDirPath/dirSepOf);`node tools/wsl-path-check.mjs`(normalizeServerPath,8/8) |
|
|
189
191
|
|
|
190
|
-
### F-24 用量与额度投影:DeepSeek
|
|
192
|
+
### F-24 用量与额度投影:DeepSeek
|
|
191
193
|
|
|
192
194
|
| 项目 | 内容 |
|
|
193
195
|
|---|---|
|
|
@@ -195,7 +197,7 @@
|
|
|
195
197
|
| 步骤 | 带 token 请求 `GET /m/api/account-usage`,打开 App 设置 → 账户 → 用量与额度 |
|
|
196
198
|
| 预期 | 200;`sources` 含 `deepseek` 的 CNY 金额;App 卡片显示金额;key 不出现在响应、日志或页面 |
|
|
197
199
|
|
|
198
|
-
### F-25 Codex Connect
|
|
200
|
+
### F-25 Codex Connect 当前活动账户
|
|
199
201
|
|
|
200
202
|
| 项目 | 内容 |
|
|
201
203
|
|---|---|
|
|
@@ -203,7 +205,7 @@
|
|
|
203
205
|
| 步骤 | 请求 `/m/api/account-usage`,切换 dsh-codex-connect 活动账户后再次刷新 |
|
|
204
206
|
| 预期 | `sources` 含 `codex`;仅显示当前活动账户的 displayName/maskedEmail 与有效主/附加配额窗口、Credits/个人上限(若接口返回);不返回 OAuth token;活动账户切换后下一次刷新跟随新账户 |
|
|
205
207
|
|
|
206
|
-
### F-26 OpenCode Go
|
|
208
|
+
### F-26 OpenCode Go 套餐窗口
|
|
207
209
|
|
|
208
210
|
| 项目 | 内容 |
|
|
209
211
|
|---|---|
|
|
@@ -211,36 +213,36 @@
|
|
|
211
213
|
| 步骤 | 请求 `/m/api/account-usage`,检查 App 详情卡片 |
|
|
212
214
|
| 预期 | `sources` 含 `opencode-go`;rolling/weekly/monthly 的原始已用百分比转换为剩余百分比;`rate-limited` 窗口保留为 0% 并标记限流;percent=0 的占位重置时间不显示;其它有效重置时间按手机本地时间显示 |
|
|
213
215
|
|
|
214
|
-
### F-27
|
|
216
|
+
### F-27 部分来源失败与空状态
|
|
215
217
|
|
|
216
218
|
| 项目 | 内容 |
|
|
217
219
|
|---|---|
|
|
218
220
|
| 步骤 | 让一个已配置来源返回 401/网络失败,另一个来源保持可用;再分别测试所有来源均未配置 |
|
|
219
221
|
| 预期 | 成功来源仍显示;失败来源从 `sources` 隐藏且 `failedCount` 增加,App 显示汇总提示;全部未配置时 `sources=[]`,App 保留入口并显示电脑端配置引导 |
|
|
220
222
|
|
|
221
|
-
### F-28
|
|
223
|
+
### F-28 缓存、并发与手动刷新
|
|
222
224
|
|
|
223
225
|
| 项目 | 内容 |
|
|
224
226
|
|---|---|
|
|
225
227
|
| 步骤 | 快速连续进入设置和详情页;点击详情页顶部刷新;观察服务端上游请求与 App 显示 |
|
|
226
228
|
| 预期 | 普通进入请求复用 60 秒成功快照且并发请求共享一轮上游查询;手动刷新请求 `?refresh=1` 并发查询全部来源;不产生本地持久化额度文件,不周期轮询上游 |
|
|
227
229
|
|
|
228
|
-
### F-29
|
|
230
|
+
### F-29 悬浮球面板三来源区块
|
|
229
231
|
|
|
230
232
|
| 项目 | 内容 |
|
|
231
233
|
|---|---|
|
|
232
|
-
| 前置 | 插件 v3.
|
|
234
|
+
| 前置 | 插件 v3.1.5+(含 `/account-usage`)、App 开启悬浮球,DeepSeek / Codex / OpenCode Go 至少各一个来源可用 |
|
|
233
235
|
| 步骤 | 展开悬浮球面板,等待用量与额度区块渲染 |
|
|
234
236
|
| 预期 | 出现「用量与额度」区块(含「详情 ▸」);每来源一行:DeepSeek 出金额文字(CNY ¥),Codex / OpenCode Go 配额行每个进度条上方居中显示窗口短标签(Codex:5h / 每周;OpenCode Go:5h / 每周 / 每月),条只出细条与颜色、不出百分比数字;主 bucket 窗口按 5h → 周 → 月排列;金额行在低余额时变红;点击区块或「详情 ▸」打开 App 用量页;底部「去充值」保留 |
|
|
235
237
|
|
|
236
|
-
### F-30
|
|
238
|
+
### F-30 悬浮球展开时按需获取与节流
|
|
237
239
|
|
|
238
240
|
| 项目 | 内容 |
|
|
239
241
|
|---|---|
|
|
240
242
|
| 步骤 | 反复快速展开/收起悬浮球面板;观察服务端 account-usage 调用次数 |
|
|
241
243
|
| 预期 | 每次展开最多触发一次查询;2 分钟节流窗内重复展开复用在途/缓存结果,不反复打上游;首次展开区块先显示「查询中…」再异步就地更新;App 被杀后展开面板仍能拉到数据 |
|
|
242
244
|
|
|
243
|
-
### F-31
|
|
245
|
+
### F-31 悬浮球面板降级与陈值
|
|
244
246
|
|
|
245
247
|
| 项目 | 内容 |
|
|
246
248
|
|---|---|
|
|
@@ -248,27 +250,26 @@
|
|
|
248
250
|
| 步骤 | 展开面板,分别观察 A / B / C |
|
|
249
251
|
| 预期 | A/B/C 下区块整体不出现、不显示任何报错,原有单行余额原位显示且点击仍=去充值(A 不退化现有功能);C 曾成功过 → 区块保留旧值并以小灰字标注相对时间(超过 10 分钟才标注);C 从未成功过 → 退回单行余额;不产生本地持久化额度文件 |
|
|
250
252
|
|
|
251
|
-
### F-32
|
|
253
|
+
### F-32 悬浮球面板隐私边界
|
|
252
254
|
|
|
253
255
|
| 项目 | 内容 |
|
|
254
256
|
|---|---|
|
|
255
257
|
| 步骤 | 展开面板,检查区块、球体与操作说明弹窗 |
|
|
256
258
|
| 预期 | 区块不显示 Codex 账户身份(displayName/maskedEmail);球体自身不常驻金额;配额窗口与来源永不相加、不换算;附加 Codex bucket(服务端命名「名称 · 5h」)不出现在面板;设置 → 悬浮球操作说明的面板内容已包含「用量与额度」 |
|
|
257
|
-
|
|
258
|
-
### F-33 对话时间线能力与历史/实时一致(PR #24 时间线)
|
|
259
|
+
### F-33 对话时间线能力与历史/实时一致
|
|
259
260
|
|
|
260
261
|
| 项目 | 内容 |
|
|
261
262
|
|---|---|
|
|
262
263
|
| 前置 | 插件返回 `capabilities.eventTimeline`;准备 user、assistant、tool/call、tool/result、unknown Visible event 与长参数/结果 |
|
|
263
|
-
| 步骤 | App 连接 SSE → 记录实时事件;断线后请求 `/history
|
|
264
|
-
| 预期 | 事件顺序和 durable seq 一致;工具按 callId
|
|
264
|
+
| 步骤 | App 连接 SSE → 记录实时事件;断线后请求 `/history`;展开工具详情并检查未知事件卡的降级呈现 |
|
|
265
|
+
| 预期 | 事件顺序和 durable seq 一致;工具按 callId 合并且显示完整生命周期;allow-list 内的长详情按需加载;未知事件通用卡保留顺序但详情不可用;历史分页 `hasMore` 可补齐所有缺口 |
|
|
265
266
|
| 变体 A | 旧插件无 capability / 无详情端点 |
|
|
266
267
|
| 预期 | 保留旧摘要与聊天;详情显示“不可用”,不伪造数据 |
|
|
267
268
|
| 变体 B | 手机离线后展开详情 |
|
|
268
269
|
| 预期 | 已缓存摘要保留;详情显示不可用并可在恢复连接后重试 |
|
|
269
|
-
| 单测 | `node tools/timeline-contract-check.mjs
|
|
270
|
+
| 单测 | `node tools/timeline-contract-check.mjs`(未知/内部事件过滤、详情 allow-list、`/event-detail` 鉴权与身份校验、bootstrap agentId→sessionId);`flutter test test/timeline_test.dart`(reducer 合并规则:tool/call 替换 delta 参数、锚点/detail seq 收敛、可见性分类)|
|
|
270
271
|
|
|
271
|
-
### F-34
|
|
272
|
+
### F-34 普通/调试模式与富内容
|
|
272
273
|
|
|
273
274
|
| 项目 | 内容 |
|
|
274
275
|
|---|---|
|
|
@@ -281,13 +282,52 @@
|
|
|
281
282
|
|
|
282
283
|
> 边界说明:问询(`question/requested`)在核心里只有瞬态远程帧、没有 durable 事件,因此**不保证**历史回放;审批有 durable 事件,重进会话仍可见。
|
|
283
284
|
|
|
284
|
-
### F-35 断线多页 catch-up
|
|
285
|
+
### F-35 断线多页 catch-up 与滚动锚点
|
|
285
286
|
|
|
286
287
|
| 项目 | 内容 |
|
|
287
288
|
|---|---|
|
|
288
289
|
| 步骤 | 生成超过 100 条未读 Visible event,断开 SSE,恢复连接并向上翻历史 |
|
|
289
290
|
| 预期 | catch-up 持续读取 `hasMore` 直到 durable cursor 收敛;重复帧不重复渲染;加载更早事件不改变当前 viewport 锚点 |
|
|
290
291
|
|
|
292
|
+
|
|
293
|
+
### F-36 会话列表:图标状态动效(待实现)
|
|
294
|
+
|
|
295
|
+
| 项目 | 内容 |
|
|
296
|
+
|---|---|
|
|
297
|
+
| 前置 | 至少三个会话:一个正在运行、一个正在等待用户审批或作答、一个空闲 |
|
|
298
|
+
| 步骤 | 在会话列表页观察图标;让运行中的会话结束,再让另一个开始运行;触发一个审批请求 |
|
|
299
|
+
| 预期 | `running` 会话的图标四周显示方形虚线并循环旋转;等待用户处理的会话显示**静态虚线加警示色**(不旋转);`idle` 无标记;状态切换后动效随之启停;会话列表页与首页「最近会话」表现一致 |
|
|
300
|
+
| 变体 | 会话 `idle` 但有 `running` 后台任务 |
|
|
301
|
+
| 预期 | 视为运行中,显示旋转动效 |
|
|
302
|
+
| 变体 | 系统开启「减弱动态效果」 |
|
|
303
|
+
| 预期 | 旋转降级为静态虚线,仍可区分运行中与等待处理(颜色) |
|
|
304
|
+
| 单测 | `flutter test`:运行判定(`agentStatus=running` 或有 running 任务)、等待判定(`questionForSession`/`approvalForSession`)、减弱动效分支 |
|
|
305
|
+
|
|
306
|
+
### F-37 会话列表隐藏子代理会话(待实现)
|
|
307
|
+
|
|
308
|
+
| 项目 | 内容 |
|
|
309
|
+
|---|---|
|
|
310
|
+
| 前置 | 插件返回 `origin` 字段;存在一个由父会话派生的子代理会话,以及一个用户主动 fork 的会话 |
|
|
311
|
+
| 步骤 | 打开会话列表确认子代理会话不可见;进入父会话 →「会话工具」→「子代理」,点击该子代理会话并返回 |
|
|
312
|
+
| 预期 | 子代理会话不出现在活跃与已归档列表,但能从父会话的「会话工具」进入;fork 出的会话**正常显示**;「活跃 N / 已归档 N」计数与可见条数一致;首页「最近会话」同样隐藏 |
|
|
313
|
+
| 变体 A | 旧插件不返回 `origin` 字段 |
|
|
314
|
+
| 预期 | 不过滤(宁可多显示不可错误隐藏),不弹错误、不阻塞列表 |
|
|
315
|
+
| 变体 B | 父会话已归档或休眠,或子代理会话又派生出子代理会话 |
|
|
316
|
+
| 预期 | 从相应父会话的「会话工具」仍可列出并进入其子代理会话;不能仅因父 agent 不活跃而报不存在 |
|
|
317
|
+
|
|
318
|
+
### F-38 会话列表按最新消息时间排序(待实现)
|
|
319
|
+
|
|
320
|
+
| 项目 | 内容 |
|
|
321
|
+
|---|---|
|
|
322
|
+
| 前置 | 插件返回 `lastMessageAt`;准备 A、B 两个都有消息时间的会话 |
|
|
323
|
+
| 步骤 | 在 A 中发消息 → 返回列表观察顺序;再打开 B(不发消息)→ 返回列表观察顺序;最后收到 B 的新回复 |
|
|
324
|
+
| 预期 | A 因新消息排到最前;**新版 App 只打开 B 不改变顺序**;B 的新回复晚于 A 的消息时 B 才排到最前;列表显示的时间与排序一致 |
|
|
325
|
+
| 变体 A | 旧插件不返回 `lastMessageAt`,或新版插件对无消息会话返回 `null` |
|
|
326
|
+
| 预期 | 回退 `lastActivity`、再回退 `createdAt` 排序,不显示降级提示;兼容旧 App 的 `touch` 与内部事件仍可能令回退排序中的会话上浮,属于已接受的例外 |
|
|
327
|
+
| 变体 B | 两个会话 `lastMessageAt` 相同 |
|
|
328
|
+
| 预期 | 顺序稳定不抖动(以 `id` 为次级键) |
|
|
329
|
+
| 单测 | `flutter test`:排序键回退链、双方有消息时间时打开不改序、旧 App 更新缺失消息时间会话活跃时间的兼容例外、等值稳定性 |
|
|
330
|
+
|
|
291
331
|
## 3. 安全测试用例
|
|
292
332
|
|
|
293
333
|
### S-01 Host 校验
|