dsh-mobile-plugin 0.4.0 → 0.4.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/dsh-mobile-credentials/lib/credentials.js +8 -8
- package/host.js +22 -36
- package/lib/client.js +29 -82
- package/lib/desktop-view.cjs +5 -10
- package/lib/git/read.js +6 -8
- package/lib/host-compat.cjs +13 -25
- package/lib/host-guard.cjs +9 -39
- package/lib/host-patches.cjs +5 -16
- package/lib/http/routing.js +7 -27
- package/lib/impl.js +6 -8
- package/lib/mirror/pusher.cjs +33 -85
- package/lib/mirror-snapshot.cjs +6 -24
- package/lib/mobile/agent-presets.js +6 -26
- package/lib/mobile/approvals.js +18 -44
- package/lib/mobile/archive-clean.js +10 -19
- package/lib/mobile/attachments.js +11 -36
- package/lib/mobile/channels.js +15 -41
- package/lib/mobile/credential-upgrade.js +7 -16
- package/lib/mobile/credentials.js +5 -21
- package/lib/mobile/directory-browse.js +8 -27
- package/lib/mobile/directory-listing.js +10 -23
- package/lib/mobile/drive-roots.js +59 -0
- package/lib/mobile/envelope.js +4 -15
- package/lib/mobile/events.js +35 -65
- package/lib/mobile/extensions.js +5 -8
- package/lib/mobile/git-repos.js +6 -15
- package/lib/mobile/git.js +11 -19
- package/lib/mobile/history-derive.js +23 -41
- package/lib/mobile/host-bridge/endpoints.js +4 -6
- package/lib/mobile/host-bridge/event-synthesis.js +21 -35
- package/lib/mobile/host-bridge/shims.js +12 -22
- package/lib/mobile/host-bridge.js +8 -20
- package/lib/mobile/host-settings.js +9 -19
- package/lib/mobile/inbox-clean.js +7 -24
- package/lib/mobile/local-images.js +11 -17
- package/lib/mobile/monitor.js +8 -17
- package/lib/mobile/pairing.js +7 -19
- package/lib/mobile/plugin-self.js +5 -22
- package/lib/mobile/plugin-update.js +8 -27
- package/lib/mobile/plugins.js +6 -24
- package/lib/mobile/questions.js +15 -40
- package/lib/mobile/references.js +11 -31
- package/lib/mobile/relay-control.js +6 -27
- package/lib/mobile/rpc.js +5 -13
- package/lib/mobile/schedule.js +8 -33
- package/lib/mobile/self-check.cjs +9 -36
- package/lib/mobile/session-cache.js +13 -19
- package/lib/mobile/session-file.js +8 -21
- package/lib/mobile/session-flags.js +7 -13
- package/lib/mobile/session-mirror.js +8 -15
- package/lib/mobile/session-models.js +3 -3
- package/lib/mobile/session-schedule-mark.js +8 -14
- package/lib/mobile/session-send.js +13 -21
- package/lib/mobile/session-snapshot.js +4 -6
- package/lib/mobile/sessions.js +7 -23
- package/lib/mobile/stats-collect.cjs +10 -24
- package/lib/mobile/stats-ledger.cjs +13 -20
- package/lib/mobile/stream.js +12 -23
- package/lib/mobile/subagents.js +5 -18
- package/lib/mobile/uploads.js +8 -17
- package/lib/mobile/workspaces.js +20 -8
- package/lib/panel/index.html +7 -7
- package/lib/read-receipt.cjs +10 -21
- package/lib/relay/lan-network.cjs +14 -28
- package/lib/relay/lan-proxy.js +14 -21
- package/lib/relay/pairing-policy.js +6 -9
- package/lib/relay/preview.js +8 -25
- package/lib/relay/protocol.js +6 -13
- package/lib/relay/status.js +5 -8
- package/lib/relay/tunnel.js +9 -14
- package/lib/routes/control-plane.js +28 -45
- package/lib/routes/mobile-api.js +19 -32
- package/lib/session-cleanup.cjs +16 -27
- package/lib/shared/credentials-entry.js +4 -11
- package/lib/shared/dshtoken.js +4 -4
- package/lib/shared/e2ee-request.js +1 -1
- package/lib/shared/e2ee.js +8 -14
- package/lib/shared/fresh-request.js +6 -22
- package/lib/shared/http-util.js +3 -4
- package/lib/shared/list-revision.js +5 -14
- package/lib/shared/session-signal.js +7 -16
- package/lib/shared/sse-write.js +14 -20
- package/lib/shared/workspace-guard.js +7 -15
- package/lib/shell/device-ota.js +4 -6
- package/lib/shell/manager.js +12 -27
- package/lib/shell/ota-package.js +7 -12
- package/lib/shell/runtime.js +8 -16
- package/lib/shell/self-check-after-swap.js +9 -23
- package/lib/shell/updater.js +13 -26
- package/lib/sidecar-facade-worker.cjs +20 -33
- package/lib/sidecar-facade.cjs +7 -7
- package/lib/sidecar-lifecycle.cjs +7 -9
- package/lib/update-install.cjs +14 -59
- package/lib/update-registry.cjs +6 -18
- package/package.json +1 -1
|
@@ -52,10 +52,9 @@ function sessionHint(request) {
|
|
|
52
52
|
/**
|
|
53
53
|
* 这条单会话流该按哪种地址订阅。
|
|
54
54
|
*
|
|
55
|
-
* 子会话必须换成 `{kind:'subagent'}` 的 durable
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* 就是「这是个子会话」的信号(与历史那条同源,见 stream.js 的 handleStream)。
|
|
55
|
+
* 子会话必须换成 `{kind:'subagent'}` 的 durable 地址:宿主对 `origin === 'subagent'` 的会话
|
|
56
|
+
* 拿普通地址一律回 agent-busy,subscribe 直接失败,表现是「子代理页面打得开,但内容永远
|
|
57
|
+
* 不自己更新」。请求体里的 `parentSessionId` 就是「这是个子会话」的信号。
|
|
59
58
|
*/
|
|
60
59
|
function sessionAddress(payload) {
|
|
61
60
|
const sessionId = payload && payload.sessionId;
|
|
@@ -86,11 +85,9 @@ const BASELINE_DELTA_TYPES = new Set(['text-delta', 'reasoning-delta', 'tool-cal
|
|
|
86
85
|
/**
|
|
87
86
|
* 展开 follow 首帧里那条「正在跑的那一步已生成的内容」。
|
|
88
87
|
*
|
|
89
|
-
*
|
|
90
|
-
* `
|
|
91
|
-
*
|
|
92
|
-
* `nextIndex` 是「下一帧实时增量应有的密排位置」,也就是这条前缀展开后应有的成员数。
|
|
93
|
-
* 中途进入会话的人只有这一份,宿主不会把已发过的实时帧补给他。
|
|
88
|
+
* 首帧的 `assistantStream.activeAttempt.stream` 是一份压缩过的增量记录(连续同块 delta 被
|
|
89
|
+
* 压成一条记录多个成员),`nextIndex` 是展开后应有的成员数。中途进入会话的人只有这一份,
|
|
90
|
+
* 宿主不会把已发过的实时帧补给他。
|
|
94
91
|
*
|
|
95
92
|
* 返回 `[{ time, chunk }]`;形状认不出就跳过那一条记录,绝不让整条流挂掉。
|
|
96
93
|
*/
|
|
@@ -166,20 +163,15 @@ function coalesceBaselineStream(members) {
|
|
|
166
163
|
/**
|
|
167
164
|
* follow 流的一帧 → 0..n 个 mux 帧(事件原样转发;实时增量通知合成 assistant/chunk)。
|
|
168
165
|
*
|
|
169
|
-
* 0.2.0
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* 出现(真机故障 2026-10-21:回复整段蹦出来、运行中那一行没有实时明细)。
|
|
166
|
+
* 0.2.0 把逐块流式增量从会话事件里拿了出去:每个 delta 只以 follow 流的 `assistant-stream`
|
|
167
|
+
* 通知下发,而且要显式传 `assistantStream: true` 才发,事件流里只剩「一步落定」的
|
|
168
|
+
* `assistant/message`。不订阅通知的话,正文与运行中明细都只能在一步结束时整块出现。
|
|
173
169
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* turn/step 由最近一次 start 通知补上。设备级推送只看 turn/start、turn/end、session/title
|
|
177
|
-
* (`lib/mobile/events.js` 的 LIST_EVENT_INNER),合成增量到不了那条通道,不会形成推送风暴。
|
|
170
|
+
* 合成的 `assistant/chunk` 不带 seq:它没有持久化事件的身份,不该参与客户端的 seq 序门、
|
|
171
|
+
* 去重与续传水位(App 对无 seq 帧一律放行),turn/step 由最近一次 start 通知补上。
|
|
178
172
|
*
|
|
179
|
-
* `state` 是这条流的记账({ turn, step, streaming }
|
|
180
|
-
*
|
|
181
|
-
* `assistantStream.activeAttempt` 不能丢:中途进入会话的人拿不到开跑之前的实时增量,
|
|
182
|
-
* 那一段只存在于这份基线里(见 expandBaselineStream)。
|
|
173
|
+
* `state` 是这条流的记账({ turn, step, streaming }),由调用方持有。首帧 snapshot 里那份
|
|
174
|
+
* `assistantStream.activeAttempt` 不能丢:中途进入会话的人拿不到开跑之前的实时增量。
|
|
183
175
|
*/
|
|
184
176
|
function* followFramePayloads(frame, sessionId, state) {
|
|
185
177
|
if (!frame) return;
|
|
@@ -293,22 +285,16 @@ function projectionState() {
|
|
|
293
285
|
}
|
|
294
286
|
|
|
295
287
|
/**
|
|
296
|
-
* 宿主投影 `inbox` → 移动端 `session/queue`
|
|
288
|
+
* 宿主投影 `inbox` → 移动端 `session/queue` 帧的平铺 items。
|
|
297
289
|
*
|
|
298
290
|
* 形状差是这条链上唯一的坑:宿主把 inbox 定义成按键分组的对象
|
|
299
|
-
* `{ 'next-turn': Message[], 'next-step': Message[] }
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* `[{ id, placement, message }]`(`app/src/types/chatStream.ts` 的 QueueItem、
|
|
303
|
-
* `app/src/hooks/streamFrame.ts` 的 `session/queue` 分支)。原样转发对象时 App 的
|
|
304
|
-
* `Array.isArray(items)` 判定失败、整帧被静默丢弃 —— 排队条一次都不显示。
|
|
291
|
+
* `{ 'next-turn': Message[], 'next-step': Message[] }`,而移动端帧的约定是数组
|
|
292
|
+
* `[{ id, placement, message }]`。原样转发对象时 App 的 `Array.isArray(items)` 判定失败、
|
|
293
|
+
* 整帧被静默丢弃,排队条一次都不显示。
|
|
305
294
|
*
|
|
306
|
-
* `next-turn
|
|
307
|
-
* `
|
|
308
|
-
*
|
|
309
|
-
* 到真被插进去中间可能隔着好几个工具往返,此前手机上只挂着一颗「发送中…」。页脚那排插话
|
|
310
|
-
* 气泡会把本机自己发的那几条排掉(`steeringForFooter`),不会与待发气泡双画。
|
|
311
|
-
* `message.id` 用宿主行自己的 id,App 靠它与正文去重(chatRenderRows)。
|
|
295
|
+
* `next-turn` → `placement: 'queued'`(排队条只认这个值);`next-step` → `'steering'`,
|
|
296
|
+
* 它只是「宿主已经收下了」的回执,不会多画一行。`message.id` 用宿主行自己的 id,App 靠它
|
|
297
|
+
* 与正文去重。
|
|
312
298
|
*/
|
|
313
299
|
function queueItems(inbox, ctx) {
|
|
314
300
|
const items = [];
|
|
@@ -385,7 +371,7 @@ function muxFrames(ctx, request, signal) {
|
|
|
385
371
|
// **设备级这一支不能跟着宿主那几条流一起结束**:这条 SSE 还捎带着插件自己产的
|
|
386
372
|
// 清单变更、正文水位与 15s 心跳(由 `handleDeviceStream` 直接写这条连接)。
|
|
387
373
|
// 迭代器一返回,SSE 立刻收尾,手机端只会看到「连几秒就断」:信号永远不「活着」,
|
|
388
|
-
//
|
|
374
|
+
// 兜底轮询降不下去,每次重连还要重放一遍水位。
|
|
389
375
|
// 单会话那一支不适用 —— 会话结束了就该收尾。
|
|
390
376
|
if (!signal || signal.aborted) return;
|
|
391
377
|
await new Promise((resolve) => {
|
|
@@ -72,24 +72,16 @@ async function historyViaPage(ctx, payload, signal) {
|
|
|
72
72
|
/**
|
|
73
73
|
* `session.models` → `session.modelCatalog` + 会话自己的 `session/projections`。
|
|
74
74
|
*
|
|
75
|
-
* 宿主改成全局目录了(args 为空),字段也换了名:default → current
|
|
76
|
-
*
|
|
77
|
-
* 原样带过去即可。
|
|
75
|
+
* 宿主改成全局目录了(args 为空),字段也换了名:default → current;移动端要的 groups 结构
|
|
76
|
+
* 两边一致,原样带过去即可。
|
|
78
77
|
*
|
|
79
|
-
*
|
|
80
|
-
* `session/projections` 的 `modelSelection` 投影里(`next`
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* 这份会话级投影,不是全局默认。移动端这里也要对同一口径,否则 A、B 两个会话各选
|
|
84
|
-
* 了不同模型,手机却都显示同一个「默认模型」。
|
|
78
|
+
* `default` 是全局默认,不是「这个会话实际用的模型」。会话自己的模型记在
|
|
79
|
+
* `session/projections` 的 `modelSelection` 投影里(`next` 是下一次请求要用,`lastUsed` 是
|
|
80
|
+
* 最近一次真正跑过的);从没选过时才回落到全局默认。读不到投影不是错误,退回全局默认即可,
|
|
81
|
+
* 别让整个模型面板消失。
|
|
85
82
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* `defaultModel` 单独回一份**全局默认**,与 `current` 分开:宿主的新会话读的就是
|
|
90
|
-
* 「最近一次成功保存的默认值」(`session.create` 不带模型时落这一条),所以 App 要
|
|
91
|
-
* 显示「新对话会用哪个模型」只能看它 —— 拿任何一个会话的 `current` 去顶都是猜,
|
|
92
|
-
* 会话各自选过的模型未必是默认。旧 App 忽略这个字段,行为不变。
|
|
83
|
+
* `defaultModel` 单独回一份全局默认:宿主的新会话读的就是最近一次成功保存的默认值,所以
|
|
84
|
+
* App 要显示「新对话会用哪个模型」只能看它,拿任何一个会话的 `current` 去顶都是猜。
|
|
93
85
|
*/
|
|
94
86
|
async function modelCatalog(ctx, payload, signal) {
|
|
95
87
|
const sessionId = payload && typeof payload.sessionId === 'string' ? payload.sessionId : '';
|
|
@@ -208,13 +200,11 @@ async function projectionBaseline(ctx, signal) {
|
|
|
208
200
|
/**
|
|
209
201
|
* `subagents.list` → `session.control` 投影基线 + `session.list`。
|
|
210
202
|
*
|
|
211
|
-
* 0.2.0 撤掉了 subagent.list
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* 父子两层的目录在同一帧里 —— hasChildren 因此不需要按子会话再发 N 次投影查询。
|
|
203
|
+
* 0.2.0 撤掉了 subagent.list,但目录数据没少:它变成会话投影里的 `subagentCatalog` 键。
|
|
204
|
+
* 投影基线一次给出全部会话的 values,父子两层的目录在同一帧里,hasChildren 不需要再按子
|
|
205
|
+
* 会话逐个查。
|
|
215
206
|
*
|
|
216
|
-
*
|
|
217
|
-
* summaries 合成 activity 的,这里同口径再取一次会话列表,两边判定完全一致。
|
|
207
|
+
* 投影里没有运行时状态(跑没在跑、父会话在不在线),所以同口径再取一次会话列表的 summaries。
|
|
218
208
|
*/
|
|
219
209
|
async function subagentCatalog(ctx, payload, signal) {
|
|
220
210
|
const parentSessionId = payload && payload.parentSessionId;
|
|
@@ -3,23 +3,13 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 0.2.0 兼容桥:旧 `apiProxy` 调用形状 → 宿主 `typertGateway`。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* 0.2.0 删掉了 `apiProxy` 服务,宿主 RPC 全改走 `ctx.get('typertGateway')` 的 invoke /
|
|
7
|
+
* stream。本插件十几个移动端模块都按旧形状调用(`ctx.apiProxy.<ns>.<method>(…)`,返回
|
|
8
|
+
* `{ result: { ok, value|error } }`),这里提供同名同形状的服务,内部翻译成 gateway 调用并
|
|
9
|
+
* 把结果包回旧信封;将来宿主若恢复真 apiProxy,删掉这个目录即可。
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 与其逐个改 55 处调用点(每处都要对上宿主的 wire 参数名,而参数名错了只在运行时
|
|
14
|
-
* 报 `gateway/arguments-invalid`),这里提供**同名同形状**的服务,内部翻译成
|
|
15
|
-
* gateway 调用并把结果包回旧信封 —— 17 个文件一行都不用动,将来 DSH 若恢复真
|
|
16
|
-
* `apiProxy`,把这个目录删掉、apply 里那一行 provide 去掉即可。
|
|
17
|
-
*
|
|
18
|
-
* 本文件只剩装配:把端点表铺成 `apiProxy.<ns>.<method>` 的形状。实现分四份:
|
|
19
|
-
* host-bridge/transport.js 网关传输与信封翻译(唯一知道「宿主怎么调」的地方)
|
|
20
|
-
* host-bridge/endpoints.js 端点表(键 ⇄ 宿主 wire 名与参数形状)
|
|
21
|
-
* host-bridge/shims.js 没有 1:1 端点、需要跨端点合成的几处
|
|
22
|
-
* host-bridge/event-synthesis.js 旧设备级 events.mux ⇄ 0.2.0 拆开的三条流
|
|
11
|
+
* 本文件只剩装配:实现分四份——transport(网关传输与信封翻译)、endpoints(端点表)、
|
|
12
|
+
* shims(需要跨端点合成的那几处)、event-synthesis(旧 mux ⇄ 三条新流)。
|
|
23
13
|
*/
|
|
24
14
|
|
|
25
15
|
const { ENDPOINTS } = require('./host-bridge/endpoints.js');
|
|
@@ -31,10 +21,8 @@ const { unsupported, UNAVAILABLE } = require('./host-bridge/transport.js');
|
|
|
31
21
|
/**
|
|
32
22
|
* 把端点表铺成旧的 `apiProxy.<ns>.<method>` 形状。
|
|
33
23
|
*
|
|
34
|
-
* 键表直接从 ENDPOINTS
|
|
35
|
-
*
|
|
36
|
-
* 别的模块、别的请求里炸出来的 TypeError(新增 sessions.projections 时就漏登记过一次,
|
|
37
|
-
* /session/stats 会跟着一起挂)。现在新增端点只改 ENDPOINTS 一处。
|
|
24
|
+
* 键表直接从 ENDPOINTS 推,不再手写一份:两份表对不上时的症状不是报错,而是别的模块里炸
|
|
25
|
+
* 出 `ctx.apiProxy.<ns>.<method> is not a function`。新增端点只改 ENDPOINTS 一处。
|
|
38
26
|
*/
|
|
39
27
|
function createApiProxy(ctx) {
|
|
40
28
|
const namespaces = {};
|
|
@@ -3,19 +3,12 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 移动端「宿主设置」API:只读暴露宿主的模型提供方名单与设置命名空间。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* settings.describe 与 llm.providers 都是只读接口,且宿主自己已经做了脱敏
|
|
9
|
-
* (secrets 只给出 path 与 set,从不返回值),所以可以原样转给手机。
|
|
10
|
-
*
|
|
11
|
-
* 命名空间里的 schema 字段是序列化后的 zod 树(几百个 uid/refs),对手机没有任何用处,
|
|
6
|
+
* settings.describe 与 llm.providers 都是只读接口,宿主自己已做脱敏(secrets 只给 path 与
|
|
7
|
+
* set,从不返回值),可以原样转给手机。命名空间里的 schema 是序列化后的 zod 树,对手机没用,
|
|
12
8
|
* 直接丢掉,只保留 value / base / user / applies / revision 与密钥槽。
|
|
13
9
|
*
|
|
14
|
-
* 写入侧(POST /host-settings/mutate)与桌面端同一口径:只走宿主的 settings.mutate
|
|
15
|
-
*
|
|
16
|
-
* 桌面端就是这么写的:dsh-client-ui-settings/lib/client.js 一次只提交一个 op
|
|
17
|
-
* (`settings.mutate({ ns, ops: [op], expectedRevision })`),失败后重新 describe 再重试。
|
|
18
|
-
* 手机上一般一次改好几个字段,所以这里一次收一组 op,但语义与桌面端逐字段提交完全一致。
|
|
10
|
+
* 写入侧(POST /host-settings/mutate)与桌面端同一口径:只走宿主的 settings.mutate,用按路径
|
|
11
|
+
* 寻址的 op(set / unset)+ expectedRevision 做乐观并发,绝不整段 replace。
|
|
19
12
|
*/
|
|
20
13
|
|
|
21
14
|
const { sendJson, readJsonBody } = require('../shared/http-util');
|
|
@@ -61,15 +54,12 @@ function boundValue(value, depth = 0) {
|
|
|
61
54
|
/**
|
|
62
55
|
* 从序列化的 zod 树里读出「这个命名空间的密钥写到哪个凭据引用上」。
|
|
63
56
|
*
|
|
64
|
-
* 宿主把两个标记留在 schema 的 meta 里:`.role('secret')`
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* settings.describe 的 secrets 槽就是由 role('secret') 生成的,而真正写入时
|
|
68
|
-
* 要用的是 role('credential-ref') 指向字段的值(桌面端也是这么取的:
|
|
69
|
-
* dsh-client-ui-settings-plugins/lib/client.js 的 refOf() 读 snapshot.value.apiKeyEnv)。
|
|
57
|
+
* 宿主把两个标记留在 schema 的 meta 里:`.role('secret')` 标出密钥字段,`.role('credential-ref')`
|
|
58
|
+
* 标出「写着凭据引用名」的那个字段。settings.describe 的 secrets 槽由前者生成,而真正写入时
|
|
59
|
+
* 要用后者指向字段的值。
|
|
70
60
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
61
|
+
* 按同一口径从 schema 推导,不写死任何命名空间;推导不出来就返回 null,于是 App 不会给
|
|
62
|
+
* 没有密钥槽的设置项画密码框。
|
|
73
63
|
*/
|
|
74
64
|
function credentialRefOf(schema, value) {
|
|
75
65
|
const refs = schema && typeof schema === 'object' && !Array.isArray(schema) ? schema.refs : null;
|
|
@@ -3,31 +3,14 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 收件目录(`~/.dsh/mobile-inbox`)的占用统计与按龄清理。
|
|
5
5
|
*
|
|
6
|
-
* 手机分享进来的文件由 `uploads.js` 落到
|
|
7
|
-
*
|
|
8
|
-
* 它传上来的文件」可言 —— 唯一的引用是会话日志正文里那句 `@路径`,那是文本,宿主不会
|
|
9
|
-
* 跟着它去回收。不设一个出口,这个目录只会按天堆下去。
|
|
6
|
+
* 手机分享进来的文件由 `uploads.js` 落到 `<收件根>/<日期>/<消毒名>`,那条通道只进不出:盘上
|
|
7
|
+
* 只认文件名,不认识会话,会话日志里的 `@路径` 也不会触发回收。不设出口,目录只会按天堆。
|
|
10
8
|
*
|
|
11
|
-
*
|
|
9
|
+
* 口径是按龄:只删 mtime 超过 24 小时的文件。按文件而不是按日期目录删,才和「放满一天」这个
|
|
10
|
+
* 承诺一致;今天传的文件不在范围内,正在跑的那一回合碰不到它。空出来的日期目录顺手删掉。
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* 一致。空出来的日期目录顺手删掉。
|
|
16
|
-
*
|
|
17
|
-
* 为什么留着 24 小时而不是立刻删:助手拿到路径之后可能在同一个回合里反复读它,用户也可能
|
|
18
|
-
* 隔一晚上让助手接着看同一份文件。今天传的文件一开始就不在清理范围内,正在跑的那一回合
|
|
19
|
-
* 因此碰不到这里。
|
|
20
|
-
*
|
|
21
|
-
* 与 `session-cleanup.cjs` 的取舍刻意不同:那边进程活着时一条日志都不删(宿主随时可能去
|
|
22
|
-
* 打开,ENOENT 会带走整个进程);这里删的是收件目录里的普通文件,宿主只在助手明确要读
|
|
23
|
-
* 某个路径时才碰它,而那个路径只有在 24 小时窗口内才可能还有人要。
|
|
24
|
-
*
|
|
25
|
-
* ## 自动清理是开关,默认关(2026-10-21 用户定)
|
|
26
|
-
*
|
|
27
|
-
* 启动时那次自动清理**只有开关打开才跑**;面板上那颗按钮永远可用(那是用户自己按的,
|
|
28
|
-
* 不需要开关授权)。开关落盘在 `<DSH_HOME>/storages/mobile-plugin/inbox-auto-clear.json`,
|
|
29
|
-
* 与自动更新开关同一套写法(读不到 / 读坏了都当关)—— 自动删盘这种事,缺省必须在用户
|
|
30
|
-
* 这一侧,而不是「装了就替你删」。
|
|
12
|
+
* 自动清理是开关、默认关:启动时那次只有开关打开才跑,面板上的按钮永远可用。开关落盘在
|
|
13
|
+
* `<DSH_HOME>/storages/mobile-plugin/inbox-auto-clear.json`,读不到或读坏都当关。
|
|
31
14
|
*/
|
|
32
15
|
|
|
33
16
|
const fs = require('node:fs');
|
|
@@ -241,7 +224,7 @@ function usageView(scan) {
|
|
|
241
224
|
keepHours: Math.round(scan.keepMs / (60 * 60 * 1000)),
|
|
242
225
|
/**
|
|
243
226
|
* 自动清理开着没有。两个界面都靠它画那颗开关 —— 不自己猜、也不各自存一份,
|
|
244
|
-
* 真值只在这台机器的 json 里。**默认 false
|
|
227
|
+
* 真值只在这台机器的 json 里。**默认 false**。
|
|
245
228
|
*/
|
|
246
229
|
autoClear: autoClearEnabled(),
|
|
247
230
|
};
|
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 正文图片内联:把消息文本里指向本机文件的图片语法(``)换成 data URI,
|
|
5
|
+
* 让手机端能直接画出来。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* base64,见 attachments.js 与 App 侧 chat.ts 的 images[]),这里只是把「正文里写的本机图片」
|
|
11
|
-
* 拉齐到同一种形状,App 侧因此不需要新增任何通道或依赖。
|
|
7
|
+
* 手机拿到的只是文本,`/Users/…/a.png` 既不是手机上的文件,移动 API 也没有「按路径读文件」
|
|
8
|
+
* 的能力面,原样过去只会显示成一串字面 markdown。这里把「正文里写的本机图片」拉齐到附件那种
|
|
9
|
+
* 内联形状,App 不需要新增通道。
|
|
12
10
|
*
|
|
13
|
-
*
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
18
|
-
* -
|
|
19
|
-
* 不截断、不缩水成半张图。
|
|
20
|
-
* - 读不到、不是普通文件、读取失败:原样保留 —— 一条图片取不到不能让整段历史或整帧 SSE 挂掉。
|
|
21
|
-
* - 围栏代码块与行内代码里的图片语法一律不碰:那是**写法示例**,不是要画的图;换掉会把
|
|
22
|
-
* 三行代码变成几十万字符的 base64,App 的代码块会铺成很长很长的一块。
|
|
11
|
+
* 保守取舍——宁可保留字面 markdown,也不给半截东西:
|
|
12
|
+
* - 只认绝对路径(`/x/y.png`、`file:///x/y.png`、`~/y.png`),http(s)、`data:`、相对路径不碰;
|
|
13
|
+
* - 只认 png/jpg/jpeg/webp/gif,并核对魔术字节,不按扩展名撒谎;
|
|
14
|
+
* - 三级体积预算(单图 / 单段 / 单次响应),超预算就保持字面;
|
|
15
|
+
* - 读不到、不是普通文件、读取失败一律原样保留;
|
|
16
|
+
* - 围栏代码块与行内代码里的图片语法不碰,那是写法示例。
|
|
23
17
|
*/
|
|
24
18
|
|
|
25
19
|
const fs = require('node:fs/promises');
|
package/lib/mobile/monitor.js
CHANGED
|
@@ -3,22 +3,14 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* GET /api/mobile/v1/monitor —— 安卓后台提醒服务专用的「状态位」视图(只读)。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 会话清单(`?fresh=1`),而那份清单为了画侧栏还捎着**最后一条消息预览** —— 预览要为最近
|
|
9
|
-
* 20 个会话各读一次 history。后台每 8 秒白读 20 次日志,只为判断「completed 翻没翻」。
|
|
6
|
+
* 那条前台服务活在 JS 之外,只能自己定时来问「这一轮跑完了 / 跑挂了 / 停下来等你回话」。
|
|
7
|
+
* 它此前问完整清单,而清单为了画侧栏还捎着最后一条消息预览,后台每 8 秒白读 20 次 history。
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* 真的变了才回 —— 安卓后台服务靠它把「定时去问」变成「一有变化立刻送到」。
|
|
9
|
+
* 这条只回监控需要的那几格,支持 `?rev=` 条件刷新(状态位没变就回 unchanged)与 `?wait=秒`
|
|
10
|
+
* (条件不满足时先挂住,真变了才回),让安卓服务不必轮询。完成态仍以 completion 状态机为准,
|
|
11
|
+
* 常态下不读 history,只有「跑过、不在跑、结局还没定」的那几条才读一次。
|
|
15
12
|
*
|
|
16
|
-
*
|
|
17
|
-
* 只有「跑过、现在不在跑、结局还没定」的那几条才读一次 —— 那是唯一能把 failedTurn 折出来
|
|
18
|
-
* 的地方(事件流漏帧时全靠它,见 completion.js 的坑)。
|
|
19
|
-
*
|
|
20
|
-
* 刻意不进 `/sessions`:那边一旦按调用方分叉视图,镜像推送与 App 的写后刷新也会跟着
|
|
21
|
-
* 受影响,而这两条都要求「一份完整清单」。分路由比加参数安全。
|
|
13
|
+
* 刻意不进 `/sessions`:那边一旦按调用方分叉视图,镜像推送与写后刷新都会受影响。
|
|
22
14
|
*/
|
|
23
15
|
|
|
24
16
|
const { sendJson } = require('../shared/http-util');
|
|
@@ -50,8 +42,7 @@ function monitorItem(summary, state, fact) {
|
|
|
50
42
|
// 为什么:那个按设备记账的完成态要靠「这一次真的读回了 history」才折得出来,而
|
|
51
43
|
// 「刚跑完」那一刻宿主的镜像推送早把同一份预览按新指纹写进缓存了 —— 监控这一侧
|
|
52
44
|
// 拿到的是缓存命中(events 为空)、永远折不出来,`completed` 就一直 false,后台
|
|
53
|
-
//
|
|
54
|
-
// 事实那本账由 turn/end 事件当场记下,不依赖预览缓存,所以拿它判完成。
|
|
45
|
+
// 一条提醒都不会响。事实那本账由 turn/end 事件当场记下,不依赖预览缓存。
|
|
55
46
|
//
|
|
56
47
|
// 已读水位仍然生效:这一轮结束在「这台手机读过」之前就不算提醒。
|
|
57
48
|
const clearedAt = typeof state?.clearedAt === 'number' ? state.clearedAt : null;
|
|
@@ -103,7 +94,7 @@ async function buildMonitorState(ctx, deviceKey) {
|
|
|
103
94
|
const deleted = deletedSessionIdsOf(ctx);
|
|
104
95
|
const { archived } = await workspaceSessionFlags(ctx);
|
|
105
96
|
// 子代理会话不参与后台提醒:它们是主轮内部的执行单元,App 侧栏也不列;把它们算进来
|
|
106
|
-
//
|
|
97
|
+
// 会在后台弹一堆「子代理跑完了」。
|
|
107
98
|
const live = summaries.filter((s) => s && typeof s.sessionId === 'string' && s.sessionId
|
|
108
99
|
&& s.origin !== 'subagent'
|
|
109
100
|
&& !deleted.has(s.sessionId) && !archived.has(s.sessionId));
|
package/lib/mobile/pairing.js
CHANGED
|
@@ -3,27 +3,15 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 移动端配对:手机在中转上用自己的配对码换到 token 与设备凭据。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* (`dsh-mobile-server/server.js` 的 `/pair`),换成 Rust 中转时整块没搬过去,于是手机
|
|
9
|
-
* 按老地址 POST `/pair`、请求被中转原样转给宿主、宿主没有这条路由,只能回 405。
|
|
6
|
+
* 这条路由归宿主、不归中转:「谁可以进这台机器」是宿主自己的判断,配对码存在宿主的 state
|
|
7
|
+
* 里,中转只按实例标识把请求转过来。
|
|
10
8
|
*
|
|
11
|
-
* 与局域网那条(`lib/relay/lan-proxy.js` 的 `/__lan_pair
|
|
12
|
-
*
|
|
13
|
-
* (都用宿主 lanSecret 签名),所以经中转配对拿到的 token 可以直接用于局域网请求。
|
|
9
|
+
* 与局域网那条(`lib/relay/lan-proxy.js` 的 `/__lan_pair`)是同一件事的两个入口,签发的
|
|
10
|
+
* 是同一种 HMAC(都用宿主 lanSecret 签名),所以经中转配对拿到的 token 也能用于局域网请求。
|
|
14
11
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* 密钥封的,解出来是设备主密钥与设备信息。
|
|
19
|
-
* 新形状里**没有配对码**:票据本身就是资格证明。回包同样是密文 —— 里面是 token / cookie /
|
|
20
|
-
* 设备凭据,明文回包等于让中转顺手拿到这台设备未来一年的通行证。
|
|
21
|
-
*
|
|
22
|
-
* 故意**不**在这里做的事:
|
|
23
|
-
* - 不问宿主再确认一次。宿主的配对策略(pairingPolicy)已经决定了它接不接受新设备,
|
|
24
|
-
* `deny` 时中转根本连不上;而这条路由本身就跑在宿主进程里。
|
|
25
|
-
* - 不做「多台机器撞同一个配对码」的处理。那是中转的职责(它才看得见全部实例),
|
|
26
|
-
* 在这台宿主上,配对码要么对上要么对不上。
|
|
12
|
+
* 请求体两种形状都认:老 App 的 `{ instanceId?, code, device }` 明文照旧;协议 v2 的
|
|
13
|
+
* `{ instanceId, sealed }` 没有配对码,票据本身就是资格证明,回包同样是密文 —— 里面是
|
|
14
|
+
* token / cookie / 设备凭据,明文回包等于让中转拿到这台设备的长期通行证。
|
|
27
15
|
*/
|
|
28
16
|
|
|
29
17
|
const { sendJson } = require('../shared/http-util');
|
|
@@ -3,29 +3,12 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 移动端「这台设备上的移动端插件」自我管理:版本、来源、运行状态、更新与回滚结果。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 只管 dsh-mobile-plugin 自己在这台电脑上的版本,不碰电脑上装的全部插件:动别人的插件会把
|
|
7
|
+
* 承载 App 连接的那条链一起带走,而手机没有在电脑前的那双手。判定只有一份真源(外壳的
|
|
8
|
+
* `dshMobilePluginRuntime`),这里只做投影与瘦身。
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 把承载 App 连接的那条链一起带走,而手机没有在电脑前的那双手。
|
|
11
|
-
*
|
|
12
|
-
* ## 与 `/plugin/update/*` 那三条的关系
|
|
13
|
-
*
|
|
14
|
-
* 那三条是上一版给「应用更新」页做的,只有 `status / check / apply`。这里补的是同一件事
|
|
15
|
-
* 在**设备页**里需要的另外几格:来源与版本要不要并成一行、更新完**有没有自动退回去**、
|
|
16
|
-
* 以及「这一台能不能更新」为什么不给按钮的原因。判定仍然只有一份真源(外壳的
|
|
17
|
-
* `dshMobilePluginRuntime`),这里只做投影与瘦身,不自己算。
|
|
18
|
-
*
|
|
19
|
-
* ## 回滚是宿主自己做的,不是手机按的
|
|
20
|
-
*
|
|
21
|
-
* 装坏了 App 就连不上这台设备 —— 那时候手机上任何「回滚」按钮都发不出去。所以外壳在
|
|
22
|
-
* 装之前备份正在跑的那一代、装完自检(见 `lib/shell/self-check-after-swap.js`),
|
|
23
|
-
* 不过就自己退回去。手机端只做两件事:
|
|
24
|
-
* · 更新后断了就按现有逻辑重连(`usePluginUpdate` 的轮询本身);
|
|
25
|
-
* · 连上之后读 `rolledBack`,如实显示「上次装失败了,已自动退回 X」。
|
|
26
|
-
*
|
|
27
|
-
* `rolledBack` 由外壳落进 `lastApply`,这里只认它、**不重新判**:判据写在会真去医院的那
|
|
28
|
-
* 一头,手机端各写一套只会出现两套口径。
|
|
10
|
+
* 回滚是宿主自己做的,不是手机按的:装坏了 App 就连不上,那时手机上的回滚按钮发不出去。
|
|
11
|
+
* 外壳装前备份、装完自检,不过就自己退回去;手机只读 `rolledBack` 如实显示,不重新判。
|
|
29
12
|
*/
|
|
30
13
|
|
|
31
14
|
const { sendJson, readJsonBody } = require('../shared/http-util');
|
|
@@ -3,35 +3,16 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 手机端「插件更新」:APP 的「应用更新」页直接给这台电脑上的插件升级。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 自动更新(外壳的 10 分钟轮询)有闸:有对话在跑就不换文件。用户在手机上聊天时这道闸几乎
|
|
7
|
+
* 永远关着,而更新入口此前只在电脑面板(`localOnly`),手机打不到。这三条接口把它开给已配对
|
|
8
|
+
* 的手机,能力面不新增:来源判据照旧(只放行注册表来源,force 也跳不过),换代仍就地做。
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* 的手机。
|
|
10
|
+
* apply 只回一句「开始了」:装一次要跑包管理器,装完换代会把当前这一代实现连同这条路由一起
|
|
11
|
+
* 卸掉,旧代码手里的响应发不出去,结果只能靠前端轮询 status。状态里给身份而不是时间戳,
|
|
12
|
+
* 两台机器的钟不一定对得上。
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* · **来源判据不放开**:`update-install.cjs` 的 `installAllowedFor` 只放行注册表来源
|
|
17
|
-
* (`npm`)。本地目录 / `file:` tgz / `workspace:` 装的机器(开发机就是)即便带
|
|
18
|
-
* `force` 也会被拒 —— force 只跳过「有对话在跑」和「新发布先晾一会儿」,跳不过来源。
|
|
19
|
-
* · **换代仍然就地做**:装完由外壳 `runtime.reload()` 换实现代,不重启 DSH(见 lib/index.js)。
|
|
20
|
-
*
|
|
21
|
-
* ## 为什么 apply 只回一句「开始了」
|
|
22
|
-
*
|
|
23
|
-
* 装一次要跑包管理器,几十秒到几分钟;装完换代会把**当前这一代实现连同这条路由一起卸掉**,
|
|
24
|
-
* 旧代码手里的响应发不出去。所以结果不能靠这条请求的返回值,只能靠前端轮询 status。
|
|
25
|
-
* 顺带一个好处:apply 期间没有在飞请求,换代时 `runtime.drain()` 不用等谁。
|
|
26
|
-
*
|
|
27
|
-
* 状态里给的是**身份**而不是时间戳:前端用「运行中版本变了」或「appliedAt 变了」判完成,
|
|
28
|
-
* 手机与电脑的钟不一定对得上,比较时间会误判。
|
|
29
|
-
*
|
|
30
|
-
* ## 三条接口
|
|
31
|
-
*
|
|
32
|
-
* · `GET /api/mobile/v1/plugin/update/status` 运行中/盘上版本、安装来源、上一轮更新结果
|
|
33
|
-
* · `POST /api/mobile/v1/plugin/update/check` 问 npm 注册表有没有新版(与桌面面板同一个实现)
|
|
34
|
-
* · `POST /api/mobile/v1/plugin/update/apply` 开始一次强制更新,立即返回
|
|
14
|
+
* 三条接口:status(版本 / 来源 / 上一轮结果)、check(问 npm,与桌面同一实现)、
|
|
15
|
+
* apply(强制更新,立即返回)。
|
|
35
16
|
*/
|
|
36
17
|
|
|
37
18
|
const path = require('node:path');
|
package/lib/mobile/plugins.js
CHANGED
|
@@ -3,32 +3,14 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 移动端「插件清单」API:把桌面端 设置 → 插件 的只读快照搬到手机上。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 展开后展示 Loader 条目 id 与 Cordis 状态;选中该标签页时才懒调用一次
|
|
9
|
-
* `ctx.remote.pluginInventory.list()`,不订阅 Loader 变化(重新打开设置才取新快照)。
|
|
6
|
+
* 宿主服务 pluginInventory 不在 apiProxy 上,是 Typert Remote 服务,手机侧经
|
|
7
|
+
* host-bridge/transport.js 的 `invoke` 到达(由它补 Peer 身份与错误收敛)。
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* fileReferences / sessionReferenceResolver,这里照搬同一条路 —— 都经
|
|
15
|
-
* host-bridge/transport.js 的 `invoke`,由它统一补 Peer 身份与网关错误收敛。
|
|
9
|
+
* 这个方法没有声明任何参数,args 必须是空对象,多给一个字段就会被拒。返回值是 decode 后的
|
|
10
|
+
* 业务结果本体(`{ entries: […] }`),不是 `{ok,value}` 外壳。语义按宿主原文透传:moduleName
|
|
11
|
+
* 是 npm 包名,enabled 是有效启停(与 fiberPhase 不是一回事),fiberPhase 可能为 null。
|
|
16
12
|
*
|
|
17
|
-
*
|
|
18
|
-
* - GET /api/mobile/v1/plugins → pluginInventory.list
|
|
19
|
-
*
|
|
20
|
-
* 参数:这个方法 **没有声明任何参数**,args 必须是空对象——assertExactArguments
|
|
21
|
-
* 按声明逐位比对,多给一个字段就会被拒。
|
|
22
|
-
*
|
|
23
|
-
* 返回值形状:`gateway.invoke()` 回的是 decode 后的业务结果本体,不是
|
|
24
|
-
* {ok:true,value} 外壳(那层壳是浏览器传输加的,见 lib/mobile/references.js 里的长注释)。
|
|
25
|
-
* pluginInventory.list 声明的是 { entries: […] } 对象,所以 invoke 回来就是这个对象。
|
|
26
|
-
*
|
|
27
|
-
* 语义按宿主原文透传:
|
|
28
|
-
* - moduleName 是 npm 包名(如 @deepseek-ai/dsh-host-directory-picker-native),
|
|
29
|
-
* 展示用的短名截断属于客户端的事。
|
|
30
|
-
* - enabled 是「有效启停状态」,和 fiberPhase 不是一回事:停用的条目 fiberPhase 是 null。
|
|
31
|
-
* - fiberPhase 只有 failed/pending/active/loading/unloading 五种,宿主也可能给 null。
|
|
13
|
+
* 接口:GET /api/mobile/v1/plugins → pluginInventory.list。
|
|
32
14
|
*/
|
|
33
15
|
|
|
34
16
|
const { sendJson } = require('../shared/http-util');
|