dsh-mobile-plugin 0.4.0 → 0.4.4
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/README.md +71 -74
- package/dsh-mobile-credentials/lib/credentials.js +47 -24
- 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 -12
- package/lib/index.js +1 -1
- 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/allowed-zone.js +100 -0
- package/lib/mobile/approvals.js +18 -44
- package/lib/mobile/archive-clean.js +13 -21
- package/lib/mobile/attachments.js +16 -37
- 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/file-read.js +168 -0
- package/lib/mobile/git-repos.js +6 -15
- package/lib/mobile/git.js +11 -19
- package/lib/mobile/history-derive.js +85 -134
- 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 +48 -23
- package/lib/mobile/model-vendors.js +105 -0
- package/lib/mobile/monitor.js +8 -17
- package/lib/mobile/pairing.js +7 -19
- package/lib/mobile/plugin-market.js +475 -0
- package/lib/mobile/plugin-self.js +6 -23
- 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 +136 -33
- 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-cwd.js +28 -0
- package/lib/mobile/session-file.js +9 -33
- 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 +154 -37
- package/lib/mobile/stats-ledger.cjs +258 -75
- package/lib/mobile/stats-scheduler.cjs +41 -0
- package/lib/mobile/stream.js +39 -130
- package/lib/mobile/subagents.js +5 -18
- package/lib/mobile/uploads.js +75 -21
- package/lib/mobile/vendor-logos.cjs +30 -0
- package/lib/mobile/wire-cut.js +212 -0
- package/lib/mobile/workspaces.js +20 -8
- package/lib/panel/index.html +7 -7
- package/lib/read-receipt.cjs +10 -21
- package/lib/relay/apply-ledger.cjs +52 -0
- package/lib/relay/core.js +14 -3
- package/lib/relay/lan-network.cjs +14 -28
- package/lib/relay/lan-proxy.js +14 -21
- package/lib/relay/migrate.js +90 -0
- package/lib/relay/pairing-policy.js +6 -9
- package/lib/relay/preview.js +8 -25
- package/lib/relay/protocol.js +9 -12
- package/lib/relay/status.js +5 -8
- package/lib/relay/tunnel.js +9 -14
- package/lib/relay/verify.js +52 -0
- package/lib/routes/control-plane.js +39 -47
- package/lib/routes/mobile-api.js +47 -34
- package/lib/session-cleanup.cjs +26 -27
- package/lib/shared/credentials-entry.js +4 -11
- package/lib/shared/device-headers.js +12 -4
- 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 +15 -28
- package/lib/sidecar-facade-worker.cjs +75 -41
- package/lib/sidecar-facade.cjs +18 -8
- package/lib/sidecar-lifecycle.cjs +21 -10
- package/lib/update-install.cjs +24 -62
- package/lib/update-registry.cjs +6 -18
- package/package.json +1 -1
- package/lib/auto-archive.cjs +0 -98
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
* 本模块只做「事件 → 展示结构」的纯计算,不碰 ctx、不发 RPC,因此可以单独测;
|
|
7
7
|
* 原先这些函数与路由 handler 混在 lib/mobile/sessions.js 里(该文件 1964 行),
|
|
8
8
|
* 拆出来后 handler 层只负责 HTTP 与宿主交互。
|
|
9
|
+
*
|
|
10
|
+
* 「什么不下发手机」(推理 / 工具参数 / 工具结果正文与 diff)的唯一规则在 `wire-cut.js`,
|
|
11
|
+
* 这里只负责把留下的事件折成消息结构 —— 直播间那条路调的是同一份。
|
|
9
12
|
*/
|
|
10
13
|
|
|
14
|
+
const { cutAssistantBlock, cutToolResultEvent, createToolNameIndex } = require('./wire-cut.js');
|
|
15
|
+
|
|
11
16
|
/**
|
|
12
17
|
* 从 session.list 摘要行取会话标题(与桌面端侧栏同源,不再从 history 窗口猜):
|
|
13
18
|
* - 优先读 title 投影(dsh-session-title 在「完整日志」上折叠的最新 session/title
|
|
@@ -27,58 +32,6 @@ function listTitle(s) {
|
|
|
27
32
|
return s.sessionId || '';
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
/** 单文件一行文本的传输上限:整文件 create/overwrite 的 newText 可能极大,超出即截断并标记。 */
|
|
31
|
-
const DIFF_TEXT_MAX_LINES = 400;
|
|
32
|
-
/** 一次工具调用最多回传的 diff 文件数(对齐桌面「一行一张卡」的粒度,避免畸形 meta 撑爆载荷)。 */
|
|
33
|
-
const DIFF_MAX_FILES = 20;
|
|
34
|
-
|
|
35
|
-
/** 按行数截断一段 diff 文本;未超限原样返回,超限保留前 MAX 行并标记 truncated。 */
|
|
36
|
-
function capDiffText(text) {
|
|
37
|
-
if (typeof text !== 'string') return undefined;
|
|
38
|
-
let lines = 0;
|
|
39
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
40
|
-
if (text.charCodeAt(i) === 10) lines += 1;
|
|
41
|
-
if (lines >= DIFF_TEXT_MAX_LINES) {
|
|
42
|
-
return { text: text.slice(0, i), truncated: true };
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return { text, truncated: false };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* 收窄 tool/result 的 `data.meta`(write/edit 的 FsDiffMeta)为可回传的 diff 列表。
|
|
50
|
-
*
|
|
51
|
-
* 桌面 diff 卡的数据源头就是这里:fs 工具把「已应用的分块上下文 diff」挂在
|
|
52
|
-
* `result.meta` 上(dsh-tool-fs 的 computeHunkDiffs,DIFF_CONTEXT=3),
|
|
53
|
-
* agent loop 原样写进 tool/result 事件(dsh-agent-loop/lib/index.js:313
|
|
54
|
-
* `...result.meta !== void 0 ? { meta: result.meta } : {}`),并随会话日志持久化。
|
|
55
|
-
* 此前移动端投影只读 message.content,把 meta 整个丢掉——于是 App 里 write/edit
|
|
56
|
-
* 只剩一坨 JSON 参数,而桌面显示的是真正改了什么。
|
|
57
|
-
*
|
|
58
|
-
* 只接受形状正确的条目(path 非空字符串、newText 字符串、oldText 字符串或 null),
|
|
59
|
-
* 畸形数据返回 undefined 让调用方退回通用卡片,绝不抛错。
|
|
60
|
-
*/
|
|
61
|
-
function normalizeResultDiffs(meta) {
|
|
62
|
-
if (!meta || typeof meta !== 'object' || !Array.isArray(meta.diffs)) return undefined;
|
|
63
|
-
const out = [];
|
|
64
|
-
for (const raw of meta.diffs) {
|
|
65
|
-
if (!raw || typeof raw !== 'object') continue;
|
|
66
|
-
const path = typeof raw.path === 'string' && raw.path !== '' ? raw.path : undefined;
|
|
67
|
-
const newText = capDiffText(raw.newText);
|
|
68
|
-
if (path === undefined || newText === undefined) continue;
|
|
69
|
-
const oldText = typeof raw.oldText === 'string' ? capDiffText(raw.oldText) : undefined;
|
|
70
|
-
if (raw.oldText !== null && raw.oldText !== undefined && typeof raw.oldText !== 'string') continue;
|
|
71
|
-
out.push({
|
|
72
|
-
path,
|
|
73
|
-
oldText: oldText === undefined ? null : oldText.text,
|
|
74
|
-
newText: newText.text,
|
|
75
|
-
...newText.truncated || (oldText && oldText.truncated) ? { truncated: true } : {},
|
|
76
|
-
});
|
|
77
|
-
if (out.length >= DIFF_MAX_FILES) break;
|
|
78
|
-
}
|
|
79
|
-
return out.length > 0 ? out : undefined;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
35
|
/**
|
|
83
36
|
* tool/result 的可见内容:两种形状都认。
|
|
84
37
|
*
|
|
@@ -139,25 +92,32 @@ function toolResultImages(parts) {
|
|
|
139
92
|
return out;
|
|
140
93
|
}
|
|
141
94
|
|
|
142
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* 预扫描 tool/result 事件,构建 callId → 状态/图片 映射(对接桌面 toolRowModel 状态派生)。
|
|
97
|
+
*
|
|
98
|
+
* 每条结果先过 `wire-cut.js`:正文与结果上的 `meta`(已应用 diff)都不下发,只留状态与图片,
|
|
99
|
+
* 正文只给定时任务卡要解析的 `schedule_create` 留着。工具名从同一批事件里攒
|
|
100
|
+
* (`tool/call`、assistant 的 tool-call 块),攒不到就按「不留正文」处理。
|
|
101
|
+
*/
|
|
143
102
|
function buildToolResultMap(events) {
|
|
144
103
|
const map = new Map();
|
|
104
|
+
const toolNames = createToolNameIndex();
|
|
105
|
+
toolNames.absorbAll(events);
|
|
145
106
|
for (const entry of events) {
|
|
146
107
|
const e = entry.event || entry;
|
|
147
108
|
if (e.type !== 'tool/result' || !e.data || !e.data.message) continue;
|
|
148
109
|
const msg = e.data.message;
|
|
149
110
|
const src = msg.source;
|
|
150
111
|
if (!src || typeof src.callId !== 'string') continue;
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
map.set(src.callId, { isError, code, ...(output ? { output } : {}), ...(images.length ? { images } : {}), ...(diffs ? { diffs } : {}) });
|
|
112
|
+
const kept = cutToolResultEvent(e, toolNames.nameOf(src.callId));
|
|
113
|
+
const { parts, isError } = toolResultParts(kept.data.message);
|
|
114
|
+
const code = kept.data.error && typeof kept.data.error.code === 'string' ? kept.data.error.code : undefined;
|
|
115
|
+
const output = parts.map((part) => {
|
|
116
|
+
if (part && typeof part.text === 'string') return part.text;
|
|
117
|
+
return '';
|
|
118
|
+
}).filter(Boolean).join('');
|
|
119
|
+
const images = toolResultImages(parts);
|
|
120
|
+
map.set(src.callId, { isError, code, ...(output ? { output } : {}), ...(images.length ? { images } : {}) });
|
|
161
121
|
}
|
|
162
122
|
return map;
|
|
163
123
|
}
|
|
@@ -165,16 +125,14 @@ function buildToolResultMap(events) {
|
|
|
165
125
|
/**
|
|
166
126
|
* 把整窗工具结果折成客户端补丁(`/session/messages` 顶层的 `toolResults`)。
|
|
167
127
|
*
|
|
168
|
-
* 补丁本身必须取自整个窗口:调用方游标可能已经越过结果 seq
|
|
169
|
-
* 客户端按 callId
|
|
170
|
-
* `items` 里的 tool-call
|
|
171
|
-
*
|
|
128
|
+
* 补丁本身必须取自整个窗口:调用方游标可能已经越过结果 seq,而调用块还停在 running,
|
|
129
|
+
* 客户端按 callId 幂等打补丁。但**已经随消息块下发的**那份结果不再重复带料 ——
|
|
130
|
+
* `items` 里的 tool-call 块本来就是同一张表构出来的,图片与定时任务卡要的那份正文再传一遍
|
|
131
|
+
* 等于同一份内容在一条响应里走两趟。客户端打补丁时状态照写、料只在给了的时候才覆盖,所以
|
|
172
132
|
* 那几格省掉是安全的;消息块不在这份窗口里时才带全。
|
|
173
133
|
*
|
|
174
134
|
* `bodies: false`(增量拉取)连「消息块不在这份窗口里」的那些也只回状态。理由是同一个:
|
|
175
|
-
*
|
|
176
|
-
* 工具输出再灌一遍,等于每 15 秒把最近两百条消息里的工具输出重发一次(读文件、跑命令的
|
|
177
|
-
* 输出几十到几百 KB 很常见)。要全文的那条路是入口整份拉取(无游标)与展开时重新拉。
|
|
135
|
+
* 增量拉取的调用方手上已经有一份完整的会话,缺的只是「这几次调用结束了没有」。
|
|
178
136
|
*/
|
|
179
137
|
function toolResultPatchesFor(events, items, options = {}) {
|
|
180
138
|
const bodies = options.bodies !== false;
|
|
@@ -195,24 +153,20 @@ function toolResultPatchesFor(events, items, options = {}) {
|
|
|
195
153
|
state,
|
|
196
154
|
...(result.output ? { output: result.output } : {}),
|
|
197
155
|
...(result.images && result.images.length ? { images: result.images } : {}),
|
|
198
|
-
...(result.diffs && result.diffs.length ? { diffs: result.diffs } : {}),
|
|
199
156
|
};
|
|
200
157
|
});
|
|
201
158
|
}
|
|
202
159
|
|
|
203
|
-
/** 把 assistant message.content 解析为一个结构块数组(text/
|
|
160
|
+
/** 把 assistant message.content 解析为一个结构块数组(text/tool-call/image)。 */
|
|
204
161
|
function parseBlocks(blocks, toolResults) {
|
|
205
162
|
if (!Array.isArray(blocks)) return [];
|
|
206
163
|
const out = [];
|
|
207
|
-
for (const
|
|
164
|
+
for (const raw of blocks) {
|
|
165
|
+
// 推理不下发、tool-call 收成骨架(三张轮尾卡的工具留 arguments)—— 与直播间同一条规则。
|
|
166
|
+
const b = cutAssistantBlock(raw);
|
|
208
167
|
if (!b || typeof b.type !== 'string') continue;
|
|
209
168
|
if (b.type === 'text' && typeof b.text === 'string' && b.text) {
|
|
210
169
|
out.push({ type: 'text', text: b.text });
|
|
211
|
-
} else if (b.type === 'reasoning') {
|
|
212
|
-
// Reasoning blocks may be emitted as empty placeholders when the provider
|
|
213
|
-
// does not expose chain-of-thought. They are known blocks, not unsupported
|
|
214
|
-
// content; silently omit empty placeholders and avoid leaking their JSON.
|
|
215
|
-
if (typeof b.text === 'string' && b.text) out.push({ type: 'reasoning', text: b.text });
|
|
216
170
|
} else if (b.type === 'image' && b.dataUnavailable === true) {
|
|
217
171
|
// The image attachment resolver marked this block as unavailable. Keep it out of
|
|
218
172
|
// visible text; pushMessage projects its retry metadata onto unavailableImages.
|
|
@@ -223,21 +177,18 @@ function parseBlocks(blocks, toolResults) {
|
|
|
223
177
|
const entry = imageEntryOf(b);
|
|
224
178
|
if (entry) out.push({ type: 'image', ...entry });
|
|
225
179
|
} else if (b.type === 'tool-call') {
|
|
226
|
-
//
|
|
180
|
+
// 块已被上面那条规则收过:非白名单工具只剩 `{ id, name }`,所以 argsRaw 只有三张
|
|
181
|
+
// 轮尾卡的工具才有(App 从它读计划/扩展/定时任务的料)。
|
|
227
182
|
const callId = b.id != null ? String(b.id) : undefined;
|
|
228
183
|
let state;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
let diffs;
|
|
184
|
+
let output;
|
|
185
|
+
let images;
|
|
232
186
|
if (callId && toolResults) {
|
|
233
187
|
const result = toolResults.get(callId);
|
|
234
188
|
if (result) {
|
|
235
189
|
output = result.output;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// 有它就替换掉仅凭 arguments 猜出来的调用态。
|
|
239
|
-
diffs = result.diffs;
|
|
240
|
-
// 对齐桌面 toolRowModel 状态派生:error.code === 'interrupted' → stopped, isError → error, ok
|
|
190
|
+
images = result.images;
|
|
191
|
+
// 对齐桌面 toolRowModel 状态派生:error.code === 'interrupted' → stopped, isError → error, ok
|
|
241
192
|
if (result.isError) {
|
|
242
193
|
state = result.code === 'interrupted' ? 'stopped' : 'error';
|
|
243
194
|
} else {
|
|
@@ -251,9 +202,8 @@ function parseBlocks(blocks, toolResults) {
|
|
|
251
202
|
name: typeof b.name === 'string' ? b.name : undefined,
|
|
252
203
|
argsRaw: typeof b.arguments === 'string' ? b.arguments : (b.arguments != null ? JSON.stringify(b.arguments) : undefined),
|
|
253
204
|
...state ? { state } : {},
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
...diffs && diffs.length ? { diffs } : {},
|
|
205
|
+
...typeof output === 'string' && output ? { output } : {},
|
|
206
|
+
...images && images.length ? { images } : {},
|
|
257
207
|
});
|
|
258
208
|
} else {
|
|
259
209
|
// 桌面端对未知 assistant block 使用 JsonBlock 兜底;移动端没有对应组件,
|
|
@@ -264,6 +214,31 @@ function parseBlocks(blocks, toolResults) {
|
|
|
264
214
|
return out;
|
|
265
215
|
}
|
|
266
216
|
|
|
217
|
+
/**
|
|
218
|
+
* 一条消息里 write/edit 成功写过的文件路径。
|
|
219
|
+
*
|
|
220
|
+
* 参数不下发手机(见 `wire-cut.js`),但「这一轮动过哪些文件」仍要在本机从**原始**块的
|
|
221
|
+
* `arguments` 里派生出来:App 拿这份清单解析收尾正文里的行内文件引用。
|
|
222
|
+
*/
|
|
223
|
+
function writtenPaths(content, toolResults) {
|
|
224
|
+
if (!Array.isArray(content)) return [];
|
|
225
|
+
const out = [];
|
|
226
|
+
for (const b of content) {
|
|
227
|
+
if (!b || typeof b !== 'object' || b.type !== 'tool-call') continue;
|
|
228
|
+
if (b.name !== 'write' && b.name !== 'edit') continue;
|
|
229
|
+
const callId = b.id != null ? String(b.id) : undefined;
|
|
230
|
+
const result = callId && toolResults ? toolResults.get(callId) : undefined;
|
|
231
|
+
if (!result || result.isError) continue;
|
|
232
|
+
const raw = typeof b.arguments === 'string' ? b.arguments : (b.arguments != null ? JSON.stringify(b.arguments) : '');
|
|
233
|
+
try {
|
|
234
|
+
const args = JSON.parse(raw || '{}');
|
|
235
|
+
const path = typeof args.path === 'string' ? args.path : typeof args.file_path === 'string' ? args.file_path : null;
|
|
236
|
+
if (typeof path === 'string' && path) out.push(path);
|
|
237
|
+
} catch { /* 参数不是 JSON:这条调用不算产出,与「没有参数」同处理 */ }
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
267
242
|
/** 从结构块里拼接纯文本(列表预览/标题回退用,仍只取 text 块)。 */
|
|
268
243
|
function blocksToText(blocks) {
|
|
269
244
|
return blocks.filter((b) => b.type === 'text' && typeof b.text === 'string').map((b) => b.text).join('\n');
|
|
@@ -328,19 +303,14 @@ function presentedPathsOf(events) {
|
|
|
328
303
|
|
|
329
304
|
/**
|
|
330
305
|
* 从 history 事件折叠出消息与「结构化行」(wire v1 扩展,向后兼容):
|
|
331
|
-
* -
|
|
332
|
-
* - compaction
|
|
333
|
-
* summary
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* - retry:{ kind:'retry', id: retryId, seq, time, status:'scheduled'|'started', retry, maxRetries, delayMs?, failure? };
|
|
340
|
-
* - turn-error / max-tokens:{ kind:'turn-error', id:'turn-<turn>', message, code? } / { kind:'max-tokens', id:'turn-<turn>' };
|
|
341
|
-
* - command:{ kind:'command', id:commandId, name, status:'running'|'done', resultKind?, text? }
|
|
342
|
-
* (command/run 发出行,command/done 原地升级:name 保 run 的,resultKind/text 取 done 的)。
|
|
343
|
-
* 结构化项与消息按事件 seq 升序混排返回;旧载荷字段缺失时跳过对应项,绝不抛错。
|
|
306
|
+
* - 普通消息:{ id, seq, time, role, text, blocks?, images?, clientMessageId? };
|
|
307
|
+
* - compaction:渲染成 compaction 行而不是用户气泡,id=compactionId;同 compactionId 的
|
|
308
|
+
* compaction/summary 补 counts,compaction/end 落定 state(带 error 即 failed);
|
|
309
|
+
* 同一轮的多次尝试只占一行,新尝试原地顶替还没落定的那条;
|
|
310
|
+
* - retry:{ kind:'retry', id: retryId, status:'scheduled'|'started', retry, maxRetries, … };
|
|
311
|
+
* - turn-error / max-tokens:id 为 `turn-<turn>`;
|
|
312
|
+
* - command:command/run 发出行,command/done 原地升级。
|
|
313
|
+
* 结构化项与消息按 seq 升序混排;旧载荷字段缺失时跳过对应项,绝不抛错。
|
|
344
314
|
*/
|
|
345
315
|
function deriveMessages(events) {
|
|
346
316
|
const out = [];
|
|
@@ -357,7 +327,7 @@ function deriveMessages(events) {
|
|
|
357
327
|
// 交付声明预扫描(`deliverables/presented`):模型调 `present` 声明「这几份请你收下」时,
|
|
358
328
|
// 宿主的 dsh-tool-present 会往会话日志追一条这个事件。桌面那张「交付文件」卡只认它 ——
|
|
359
329
|
// **不是**「这一轮改过哪些文件」。手机第一版从 write/edit 反推,于是「每改一次都冒卡、
|
|
360
|
-
//
|
|
330
|
+
// 命令行改的却一张都没有」。这里按 **turn** 归并,
|
|
361
331
|
// 同一个路径后声明的覆盖先声明的(桌面 `presentedForClosing` 的 `files.set` 同口径)。
|
|
362
332
|
const presentedByTurn = new Map();
|
|
363
333
|
for (const entry of events) {
|
|
@@ -383,8 +353,7 @@ function deriveMessages(events) {
|
|
|
383
353
|
* 这一轮**被 write / edit 动过**的文件,按轮攒。
|
|
384
354
|
*
|
|
385
355
|
* 为什么按轮而不是按单条消息:一份文件是一轮里某一步写的,而收尾答复那条消息自己一个
|
|
386
|
-
* 写操作都没有 ——
|
|
387
|
-
* 现场:手机上那个文件名一直是灰的,数一遍 54 条消息只有 4 条带得动这份清单)。
|
|
356
|
+
* 写操作都没有 —— 按消息算的话,正文里提到刚写好的文件时那份清单永远是空的。
|
|
388
357
|
* 桌面 `dsh-client-ui-deliverables` 的 `selectProducedFiles(owner)` 也是按**整轮**取。
|
|
389
358
|
*
|
|
390
359
|
* 攒完在循环之后回填:同一轮里先发的消息也要能看到这一轮后几步才写出来的文件。
|
|
@@ -512,15 +481,8 @@ function deriveMessages(events) {
|
|
|
512
481
|
// 这一轮的轮号(老日志没有):产出清单与交付声明都按它归并。
|
|
513
482
|
const turn = e.data && typeof e.data.turn === 'number' ? e.data.turn : undefined;
|
|
514
483
|
// 本条消息自己写 / 改过的文件:并进**这一轮**的那一份,不单独挂在它自己头上。
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
.map((block) => {
|
|
518
|
-
try {
|
|
519
|
-
const args = JSON.parse(block.argsRaw || '{}');
|
|
520
|
-
return typeof args.path === 'string' ? args.path : typeof args.file_path === 'string' ? args.file_path : null;
|
|
521
|
-
} catch { return null; }
|
|
522
|
-
})
|
|
523
|
-
.filter((path) => typeof path === 'string');
|
|
484
|
+
// 从原始块的 arguments 算(那些参数不下发手机)。
|
|
485
|
+
const ownWrites = writtenPaths(msg.content, toolResults);
|
|
524
486
|
if (turn !== undefined && ownWrites.length > 0) {
|
|
525
487
|
const bucket = producedByTurn.get(turn) || new Set();
|
|
526
488
|
for (const path of ownWrites) bucket.add(path);
|
|
@@ -804,25 +766,16 @@ function deriveMessages(events) {
|
|
|
804
766
|
* 入口窗口的「内容化」裁剪(`GET /session/messages?thin=1`,只对不带 beforeSeq / afterSeq 的
|
|
805
767
|
* 一次性入口窗口生效;往上翻页与增量补拉保持完整)。
|
|
806
768
|
*
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
* 现在入口按「轮」分组、只留用户看得见的内容:
|
|
812
|
-
* - 用户消息 / 定时任务唤醒行(原样);
|
|
813
|
-
* - 助手正文(中途汇报与最终答复都算)与带图的消息;
|
|
814
|
-
* - 结构化行(压缩、重试、本轮失败等,体量小,且带着这一轮的状态)。
|
|
815
|
-
* 过程明细(推理块、工具调用块、只有过程没有文字的消息)不下发,开启某一轮时再逐轮补取。
|
|
816
|
-
* 每轮被裁掉多少项、范围到哪,挂在**这一轮开头那条用户消息**的 `turnProcess` 上:App 用它
|
|
817
|
-
* 渲染「还有 N 项处理过程」,点开时按 startSeq / endSeq 回头要那一轮的事件。
|
|
769
|
+
* 入口原来按「消息条数」截尾巴,而宿主每跑一个 agent 步就落一条 assistant 消息,长回合最后
|
|
770
|
+
* 几十条可能全是过程消息,用户自己那句话被挤出窗口。现在按「轮」分组,只留用户消息、助手
|
|
771
|
+
* 正文、带图消息与结构化行;过程明细不下发,每轮裁掉多少项挂在开头那条用户消息的
|
|
772
|
+
* `turnProcess` 上,点开时按 startSeq / endSeq 回头要那一轮的事件。
|
|
818
773
|
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
774
|
+
* 窗口最老那一组没有起点(截断正好切在一轮中间)时整组丢掉,否则会画出「凭空冒出的 AI
|
|
775
|
+
* 发言」,它会被「加载更早」整轮补回。
|
|
821
776
|
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
* extensionTailCard),整块丢掉会让卡在收口时凭空不见。它们体量很小。
|
|
825
|
-
* **新增一种轮尾卡时要同步这里**,否则那张卡只在「点开过程之后」才出现。
|
|
777
|
+
* 三种「出卡」工具(定时任务 / 计划 / 扩展卡)的调用块要留一条,轮尾那几张卡是从工具块现算
|
|
778
|
+
* 的,整块丢掉会让卡凭空不见。新增一种轮尾卡时要同步这里。
|
|
826
779
|
*/
|
|
827
780
|
const ENTRY_OUTCOME_TOOLS = new Set(['schedule_create', 'exit_plan_mode', 'extension_present']);
|
|
828
781
|
|
|
@@ -999,8 +952,6 @@ module.exports = {
|
|
|
999
952
|
errorDetails,
|
|
1000
953
|
promptErrorObject,
|
|
1001
954
|
MODEL_UNAVAILABLE_ERROR,
|
|
1002
|
-
capDiffText,
|
|
1003
|
-
normalizeResultDiffs,
|
|
1004
955
|
buildToolResultMap,
|
|
1005
956
|
toolResultPatchesFor,
|
|
1006
957
|
parseBlocks,
|
|
@@ -3,12 +3,10 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 端点表:旧 `apiProxy.<ns>.<method>` 的键 ⇄ 宿主 wire 名与参数形状。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 只摘多余的键,救不了「少一个」)。改动前请先在 test/host-bridge-endpoints.test.cjs
|
|
11
|
-
* 里补一条参数化用例。
|
|
6
|
+
* 这张表是人肉对照宿主 descriptor 校验的,没有任何机器校验,错的后果是静默的:少一层
|
|
7
|
+
* `request` 时宿主只回 400,而 App 把 400 catch 掉,整个页面凭空消失。args 的键名必须恰好
|
|
8
|
+
* 是 descriptor 的 wire 名,多一个少一个都报 gateway/arguments-invalid。改动前先在
|
|
9
|
+
* test/host-bridge-endpoints.test.cjs 里补一条参数化用例。
|
|
12
10
|
*/
|
|
13
11
|
|
|
14
12
|
const { invoke, invokeEnvelope, ok, fail, pick, requestId, unsupported, warn } = require('./transport.js');
|
|
@@ -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;
|