dsh-mobile-plugin 0.3.1 → 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/config/agent-presets/standard/agent.cordis.yml +7 -5
- package/dsh-mobile-credentials/lib/credentials.js +8 -8
- package/host.js +108 -39
- 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 +120 -0
- 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 +8 -5
|
@@ -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');
|
package/lib/mobile/questions.js
CHANGED
|
@@ -3,31 +3,15 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 手机端提问桥(DSH 0.2.x):`ask_user_question` 与「计划评审」。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 提问只剩一个**进程内** waterfall 钩子 `user-questions/request`(`dsh-user-questions`
|
|
10
|
-
* 发起,`dsh-tool-ask-user` 与 `dsh-plan-mode` 是它的两个调用方),帧级 rpcId 随之消失,
|
|
11
|
-
* 于是手机既收不到卡片、也没有能回传的 id。
|
|
6
|
+
* 0.2.x 删掉了 apiProxy,提问只剩一个进程内 waterfall 钩子 `user-questions/request`,帧级
|
|
7
|
+
* rpcId 随之消失,手机既收不到卡片、也没有能回传的 id。这里按新形状补起来,App 契约不变:
|
|
8
|
+
* prepend 钩子、自己铸 rpcId 推卡、`/question/respond` 找到挂起项把回答交回钩子。
|
|
12
9
|
*
|
|
13
|
-
*
|
|
14
|
-
* `question/
|
|
10
|
+
* 钩子必须 prepend:宿主的转发器在桌面窗口开着时会一直等桌面回答、不调用 `next()`。下游
|
|
11
|
+
* 总是拿到请求,桌面那张卡与手机那张同时存在,谁先答用谁的,落定后广播 `question/resolved`。
|
|
15
12
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* rpcId,把卡片推给正在看这个会话的手机,然后把请求挂住。
|
|
19
|
-
* 2. 下游**总是**拿到请求(桌面那张卡照旧出现,`dsh-plan-mode` 也一样),与手机那张
|
|
20
|
-
* 卡同时存在:手机答得早就用手机的,桌面答得早就用桌面的,落定后广播
|
|
21
|
-
* `question/resolved`,另一边那张卡被收掉。手机在看的时候也照转,否则桌面根本
|
|
22
|
-
* 没有卡可显示 —— 桌面那张卡只由「被转发的请求」长出来,切回去也补不回来
|
|
23
|
-
* (2026-10-24 用户报「APP 有卡、桌面什么都没有」的就是这条)。
|
|
24
|
-
* 3. `/question/respond` 拿 rpcId 找到挂起项,把回答交回钩子 —— 这才是真正让 agent
|
|
25
|
-
* 继续往下跑的那一步。
|
|
26
|
-
*
|
|
27
|
-
* 「计划评审」不走另一条通道:`dsh-plan-mode` 的 `exit_plan_mode` 就是拿
|
|
28
|
-
* `ctx.userQuestions.ask()` 问的(问题带 `intent.kind='plan-review'`、`detail` 是计划正文、
|
|
29
|
-
* 选项里有一个「批准」标签),所以这道桥同时修好它。App 的问题面板本来就渲染
|
|
30
|
-
* `detail` 与 `options`,计划正文直接显示在详情里。
|
|
13
|
+
* 「计划评审」不走另一条通道:`exit_plan_mode` 就是拿 `ctx.userQuestions.ask()` 问的
|
|
14
|
+
* (`intent.kind='plan-review'`,detail 是计划正文),这道桥同时修好它。
|
|
31
15
|
*/
|
|
32
16
|
|
|
33
17
|
const { randomUUID } = require('node:crypto');
|
|
@@ -298,17 +282,12 @@ function installQuestionBridge(ctx) {
|
|
|
298
282
|
/**
|
|
299
283
|
* POST /api/mobile/v1/question/respond —— `{rpcId, sessionId, answer:{answers}}`。
|
|
300
284
|
*
|
|
301
|
-
* 先查本桥的挂起项;查不到就交回 `ctx.apiProxy.respond`(0.1.x
|
|
302
|
-
*
|
|
285
|
+
* 先查本桥的挂起项;查不到就交回 `ctx.apiProxy.respond`(0.1.x 真 apiProxy 的归宿;0.2.x
|
|
286
|
+
* 兼容桥会回 `host-unsupported`,落到下面那条 409)。
|
|
303
287
|
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* `{result:{ok:false,error:{code:'host-unsupported'}}}`,它压根不是 Promise。
|
|
308
|
-
* 早先这里直接 `.catch()`,于是在 0.2.x 上表达缺席的那一发会抛
|
|
309
|
-
* `TypeError: ctx.apiProxy.respond(...).catch is not a function`,被路由守卫兜成
|
|
310
|
-
* **500 internal** 而不是下面那句 409。手机端因此只看到「提交失败」的原始异常,
|
|
311
|
-
* 而桌面那张卡还挂着 —— 用户看到的就是「APP 提交没到 DSH,还得回桌面重答一遍」。
|
|
288
|
+
* respond 在两条通道上的返回形态不一样:0.1.x 是 `Promise<{accepted}>`,0.2.x 兼容桥是同步
|
|
289
|
+
* 对象。同步对象上没有 `.catch`,直接链 Promise 会抛 TypeError 被路由守卫兜成 500,而不是
|
|
290
|
+
* 409,手机只看到「提交失败」的原始异常。
|
|
312
291
|
*/
|
|
313
292
|
async function handleQuestionRespond(ctx, req, res) {
|
|
314
293
|
const body = await readJsonBody(req);
|
|
@@ -349,14 +328,10 @@ async function handleQuestionRespond(ctx, req, res) {
|
|
|
349
328
|
}
|
|
350
329
|
|
|
351
330
|
/**
|
|
352
|
-
* 把回答交回旧通道的 `ctx.apiProxy.respond
|
|
353
|
-
*
|
|
354
|
-
* 两条通道的形态不同(见 `handleQuestionRespond` 的注释):0.1.x 是 Promise,0.2.x 的兼容桥
|
|
355
|
-
* 是同步的 `{result:{ok:false,...}}`。同步对象上没有 `.catch`,所以不能在这里链 Promise。
|
|
356
|
-
* 抛错与「通道说没有」一律折算成 null —— 调用方据此如实回 409,而不是把 500 抛给手机。
|
|
331
|
+
* 把回答交回旧通道的 `ctx.apiProxy.respond`,Promise 与同步对象两种返回都收。
|
|
357
332
|
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
333
|
+
* 0.1.x 是 Promise,0.2.x 兼容桥是同步的 `{result:{ok:false,...}}`;同步对象上没有 `.catch`,
|
|
334
|
+
* 不能链 Promise。抛错与「通道说没有」一律折算成 null,调用方据此如实回 409。
|
|
360
335
|
*/
|
|
361
336
|
async function respondViaLegacyProxy(ctx, message) {
|
|
362
337
|
const proxy = ctx && ctx.apiProxy;
|
package/lib/mobile/references.js
CHANGED
|
@@ -3,29 +3,15 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 移动端「引用发现」API:把桌面端输入框里的 @文件 / @会话 补全补齐到手机上。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 结果按「文件在前、会话在后」的确定性顺序分组展示;选中文件插入普通 @路径 文本,
|
|
9
|
-
* 选中会话插入宿主给的规范 mention(@[label](dsh-session:…))。
|
|
6
|
+
* 两个宿主服务(fileReferences、sessionReferenceResolver)不在 apiProxy 上,是 Typert
|
|
7
|
+
* Remote 服务,手机侧经 host-bridge/transport.js 的 `invoke` 到达。
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* lib/mobile/sessions.js 已经派发过 commands.execute,lib/mobile/plugins.js 也走同一条路
|
|
15
|
-
* 读 pluginInventory —— 都经 host-bridge/transport.js 的 `invoke`,这里照搬。
|
|
9
|
+
* 两条接口只转发发现域,不做排序或截断之外的加工:
|
|
10
|
+
* - GET /file-references → fileReferences.list
|
|
11
|
+
* - GET /session-references → sessionReferenceResolver.candidates
|
|
16
12
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* - GET /api/mobile/v1/session-references?sessionId=…&query=… → sessionReferenceResolver.candidates
|
|
20
|
-
*
|
|
21
|
-
* 参数按宿主的 **wire 名** 精确校验:两个方法都是 { agentId, query },query 是普通
|
|
22
|
-
* JSON 字符串(与 messageFeedback 的 { request } 包装不同)。
|
|
23
|
-
*
|
|
24
|
-
* 语义按宿主原文透传:
|
|
25
|
-
* - query 是「@ 或 @" 后面的那段文字」;空串表示列出全部(宿主自己限量)。
|
|
26
|
-
* - 文件候选只有 path/kind,目录候选带在路径尾部追加 / 的责任在客户端。
|
|
27
|
-
* - 会话候选的 mention 是宿主的规范写法,客户端必须原样发送,不能拿 label 重建。
|
|
28
|
-
* - 两个域各自独立降级:任一域不可用不影响另一个域(桌面端也是这个约定)。
|
|
13
|
+
* 参数按宿主 wire 名精确校验(都是 `{ agentId, query }`)。语义透传:空 query 列出全部;
|
|
14
|
+
* 会话候选的 mention 必须原样发送,不能拿 label 重建;两个域各自独立降级。
|
|
29
15
|
*/
|
|
30
16
|
|
|
31
17
|
const { sendJson } = require('../shared/http-util');
|
|
@@ -52,17 +38,11 @@ function errorText(error) {
|
|
|
52
38
|
/**
|
|
53
39
|
* 经网关调一次引用发现。
|
|
54
40
|
*
|
|
55
|
-
* 走 host-bridge/transport.js 的 `invoke`:Peer
|
|
56
|
-
*
|
|
57
|
-
* 要求每个请求自带 Peer,绕过去就是对 undefined 读 `.signal`,手机端 @ 补全整块失效。
|
|
41
|
+
* 走 host-bridge/transport.js 的 `invoke`:Peer 身份与「服务/方法不存在」的收敛都在那里。
|
|
42
|
+
* 不要自己直连网关 —— 0.2.1 起每个请求都要自带 Peer,绕过去就是对 undefined 读 `.signal`。
|
|
58
43
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* 那层 `{ok:true,value}` 外壳是浏览器传输(connection.rpc.intercept('/api'))加的,
|
|
62
|
-
* invoke 不负责包。所以形状取决于服务声明的 result schema:
|
|
63
|
-
* - messageFeedback 声明的是 ok/error 联合 → invoke 回来就是 {ok:true,value}
|
|
64
|
-
* - fileReferences.list 声明的是 z.array(…) → invoke 回来就是一个裸数组
|
|
65
|
-
* 两个都认,这样宿主以后把 schema 换成联合也不会坏。
|
|
44
|
+
* 网关回的是 decode 后的业务结果本体,`{ok,value}` 外壳是浏览器传输加的:messageFeedback
|
|
45
|
+
* 声明的是联合,回 `{ok:true,value}`;fileReferences.list 声明的是数组,回裸数组。两个都认。
|
|
66
46
|
*
|
|
67
47
|
* @returns {Promise<{value?: unknown, error?: {code: string, message: string}}>}
|
|
68
48
|
*/
|
|
@@ -3,35 +3,14 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 移动端「这台设备的中转与连接配置」:读写、配对码、吊销、重连、端口自检、地址候选。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 这些项不会让插件代码变,最坏结果只是这台电脑从网上找不到,而重连的主动权在手机这一侧,
|
|
7
|
+
* 所以能力面有意放开给已配对手机;控制面那条 `localOnly` 路由防的是「中转进来的人」,不能
|
|
8
|
+
* 复用,这里直接持有同一份 settings 与 schema 校验,两个入口写同一份真值。
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 于是「手机上发现连不上、想改个中转地址」这件事必须回电脑前坐下才能做,而那往往正是
|
|
11
|
-
* 人不在电脑前的时候。
|
|
10
|
+
* 改地址 / 关遥控 / 吊销 / 重连 / 重启后台服务会掐断承载当前请求的隧道,所以一律先把响应
|
|
11
|
+
* 吐完再动手,手机看到 200 只代表「已受理」,结果靠重连后读状态。
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* 网上找不到 —— 而重连的主动权在手机这一侧(换地址、重新配对、同网走局域网),不会像
|
|
15
|
-
* 装坏插件那样死锁。所以这一层的能力面是**有意放开给已配对手机**的。
|
|
16
|
-
*
|
|
17
|
-
* ## 直接调内部实现,不绕本机 HTTP
|
|
18
|
-
*
|
|
19
|
-
* 控制面那几条 `localOnly` 路由是为了防「中转进来的人」而存在的:它们能读进程级错误账本
|
|
20
|
-
* (含堆栈)、能吊销全部授权。手机这边不能复用那道口子,所以这里**直接持有同一份
|
|
21
|
-
* `settings`(relay.json 的 config store)**,走同一套 schema 校验 —— 两个入口写的是同一
|
|
22
|
-
* 份真值,不会出现「面板说改了、手机说没改」。
|
|
23
|
-
*
|
|
24
|
-
* ## 会掐断自己那条隧道的动作
|
|
25
|
-
*
|
|
26
|
-
* 改中转地址 / 关遥控 / 吊销全部 / 重连 / 重启后台服务:这几条一动手,承载**这一发请求**
|
|
27
|
-
* 的那条隧道就可能立刻断。所以它们一律「先把响应吐完再动手」(`AFTER_RESPONSE_MS`),
|
|
28
|
-
* 手机看到 200 只代表「已经受理」,结果靠断线重连之后读状态判 —— 与插件自更新那条
|
|
29
|
-
* `apply` 同一个手感,理由也一样:响应发不出去的时候,任何「返回结果」的设计都是假的。
|
|
30
|
-
*
|
|
31
|
-
* ## 注册密钥只进不出
|
|
32
|
-
*
|
|
33
|
-
* `status()` 与二维码都不回显它(宿主那边本来就这么写),这里也照做:只接受写入,
|
|
34
|
-
* 状态里只报「设没设」。手机上看不到它,也就没有「读出来再抄走」这条路。
|
|
13
|
+
* 注册密钥只进不出:状态里只报「设没设」,手机看不到它。
|
|
35
14
|
*/
|
|
36
15
|
|
|
37
16
|
const { randomBytes } = require('node:crypto');
|
package/lib/mobile/rpc.js
CHANGED
|
@@ -3,20 +3,12 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 调宿主 apiProxy 的两扇门:`sessions.*` 与 `agentPresets.*`。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 一旦 sessions.js 被拆分,21 处调用就得跟着搬家。
|
|
6
|
+
* 抽出来是因为两扇门此前各有一份逐字相同的实现,而 callSessions 是 sessions.js 里二十几处
|
|
7
|
+
* 的公共依赖。这里收的不只是转发,还有两件必须跟着走的语义:
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 取消信号,宿主内部 `isAborted(signal)` 直接读 `signal.aborted`、不做
|
|
14
|
-
* undefined 兜底:search 这类会查 signal 的方法不传就抛
|
|
15
|
-
* `Cannot read properties of undefined (reading 'aborted')`。
|
|
16
|
-
* 2. list 的在途合流。sessions.list 会扫描所有压缩日志;同一个 Host 可能同时收到
|
|
17
|
-
* 桌面 completion、移动端列表和通知监控的请求,不合流就是一次刷新被放大成 N 次
|
|
18
|
-
* 磁盘扫描。**但写后刷新(?fresh=1)不能搭在途 list 的车**:被合流到的那个请求
|
|
19
|
-
* 发起于写入之前,拿回来的是旧列表,于是「刚改完又显示成改之前」。
|
|
9
|
+
* 1. signal 必须显式传:宿主内部直接读 `signal.aborted`,不做 undefined 兜底,不传就抛。
|
|
10
|
+
* 2. list 的在途合流:每次 list 要扫全部压缩日志,不合流会把一次刷新放大成 N 次扫描。
|
|
11
|
+
* 但写后刷新(`?fresh=1`)不能搭车 —— 被合流到的请求发起于写入之前,拿回的是旧列表。
|
|
20
12
|
*
|
|
21
13
|
* 依赖方向:rpc.js → history-derive(只为 rpcId 前缀常量),是叶子之上的薄层。
|
|
22
14
|
*/
|
package/lib/mobile/schedule.js
CHANGED
|
@@ -3,42 +3,17 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 移动端「定时任务」API:把宿主 `ScheduleService` 的四件事转给手机。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 什么时候」或者删掉一条排错的任务。本文件是宿主 `schedule` 服务的移动端代理。
|
|
6
|
+
* 服务走同进程直查 `ctx.get('schedule')`,不写进 `inject`:它是 Typert Remote 服务、不在
|
|
7
|
+
* apiProxy 命名空间里,写进 inject 会因为老宿主没有这个服务而挡住整个插件。
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 里,写进 `inject` 反而会因为老宿主没有这个服务而挡住整个插件。
|
|
9
|
+
* 四条接口:catalog(全部任务)、list(该会话的任务)、history(一条任务的运行记录)、
|
|
10
|
+
* delete(硬删)。update 不做:手机端没有编辑入口,接一条没人调的路只会多一份参数校验。
|
|
13
11
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* - GET /api/mobile/v1/schedule/list → `catalog()` 里筛出该会话的任务(**active + inactive**)
|
|
17
|
-
* - GET /api/mobile/v1/schedule/history → `history({...})` 一条任务的运行记录
|
|
18
|
-
* - POST /api/mobile/v1/schedule/delete → `delete({...})` 硬删
|
|
12
|
+
* list 走 `catalog()` 再按 sessionId 筛,不用宿主的 `list({sessionId})`:后者只回还在跑的
|
|
13
|
+
* 任务、行上没有 sessionId / status,一次性任务跑完就会从手机上「凭空消失」。
|
|
19
14
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* 宿主 catalog 的每项是「裸记录 + sessionId + status + 可选 lastDelivery」。手机只认一种
|
|
24
|
-
* 形状:sessionId 必须带上,状态统一收成 `state`,删除 / 历史 / 修改都必须带行上的
|
|
25
|
-
* sessionId,否则宿主恒回 not_found。
|
|
26
|
-
*
|
|
27
|
-
* **`list` 为什么不用宿主的 `list({sessionId})`**(2026-10-23 改):宿主那个方法只回
|
|
28
|
-
* **还在跑**的任务、而且回的是裸记录(没有 sessionId / status)。于是手机上按对话看任务时,
|
|
29
|
-
* 一次性任务跑完就从这个入口消失了 —— 用户看到的是「任务凭空没了」,实际只是「结束了」。
|
|
30
|
-
* 现在改成从 `catalog()`(active + inactive 全都有、行上带 status)里按 sessionId 筛,
|
|
31
|
-
* 手机端因此能列出这条会话的全部任务(含已结束),筛选里那三档也才真正有意义。
|
|
32
|
-
* 代价是一次多拉这份目录,它是宿主内存里的账、不是磁盘扫描,与统一页那一发等价。
|
|
33
|
-
*
|
|
34
|
-
* 收敛规则与 mobile/jobs.js 同口径:必填(id / kind / title / sessionId)坏了丢整条,
|
|
35
|
-
* 可选字段坏了只丢字段 —— 少一个「下次运行时间」只是不显示,整条消失才是信息丢失。
|
|
36
|
-
* 超长一律截断而不是丢整条:宿主已经校验过形状,这里的上限只防异常宿主把手机端列表
|
|
37
|
-
* 撑爆。
|
|
38
|
-
*
|
|
39
|
-
* 服务不存在(老宿主)必须回 503 而不是空列表:App 要能分辨「这台电脑没有这个功能」
|
|
40
|
-
* 和「一条任务都没有」,后者画空状态、前者画「升级宿主」。宿主调用抛错走 502,把原文
|
|
41
|
-
* 带给 App —— 定时任务这块的错误多半是宿主的,插件不该换成一句「操作失败」。
|
|
15
|
+
* 收敛规则与 mobile/jobs.js 同口径:必填字段坏了丢整条,可选字段坏了只丢字段,超长截断。
|
|
16
|
+
* 服务不存在(老宿主)回 503 而不是空列表,App 才能分辨「没有这个功能」和「一条都没有」。
|
|
42
17
|
*/
|
|
43
18
|
|
|
44
19
|
const { sendJson, readJsonBody } = require('../shared/http-util');
|