dsh-deeppilot 0.9.15 → 0.9.17
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 +5 -2
- package/README.zh-CN.md +3 -1
- package/bin/SHA256SUMS +6 -6
- package/bin/darwin-amd64/dsh-deeppilot-tunnel +0 -0
- package/bin/darwin-arm64/dsh-deeppilot-tunnel +0 -0
- package/bin/linux-amd64/dsh-deeppilot-tunnel +0 -0
- package/bin/linux-arm64/dsh-deeppilot-tunnel +0 -0
- package/bin/windows-amd64/dsh-deeppilot-tunnel.exe +0 -0
- package/bin/windows-arm64/dsh-deeppilot-tunnel.exe +0 -0
- package/docs/DSH_RELEASE_MEMORY.md +60 -0
- package/docs/NPM_PUBLISHING.md +31 -14
- package/lib/client.js +10 -79
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +140 -5
- package/lib/index.js +875 -148
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomB
|
|
|
3
3
|
import { access, appendFile, chmod, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { WebSocketServer } from "ws";
|
|
6
|
+
import { deflateRawSync } from "node:zlib";
|
|
6
7
|
import { homedir, networkInterfaces } from "node:os";
|
|
7
8
|
import { closeSync, constants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
8
9
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -292,6 +293,23 @@ function sanitizeImageName(value) {
|
|
|
292
293
|
function sanitizeDocumentField(value, maxChars) {
|
|
293
294
|
return value.replace(/[\u0000-\u001F\u007F]/g, " ").trim().slice(0, maxChars);
|
|
294
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* 解析关注集合:字符串数组、去重、去空、超长 ID 丢弃,最多取前 16 个。
|
|
298
|
+
* 不合法时返回 undefined。auth.prove 上宁可退回全量广播也不因优化字段拒绝认证;
|
|
299
|
+
* c2s.session.watch 由行校验先拒绝非法载荷。
|
|
300
|
+
*/
|
|
301
|
+
function parseWatchSessions(value) {
|
|
302
|
+
if (!Array.isArray(value)) return void 0;
|
|
303
|
+
const ids = /* @__PURE__ */ new Set();
|
|
304
|
+
for (const item of value) {
|
|
305
|
+
if (typeof item !== "string") continue;
|
|
306
|
+
const id = item.trim();
|
|
307
|
+
if (id.length === 0 || id.length > 256) continue;
|
|
308
|
+
ids.add(id);
|
|
309
|
+
if (ids.size >= 16) break;
|
|
310
|
+
}
|
|
311
|
+
return [...ids];
|
|
312
|
+
}
|
|
295
313
|
/** 二进制模式下导出与附件的单块字节数。连接在发送缓冲降到 256 KiB 以下才发
|
|
296
314
|
* 下一块,缓冲峰值约 512 KiB,远低于 4 MiB 的慢客户端断开线。 */
|
|
297
315
|
const BINARY_CHUNK_BYTES = 262144;
|
|
@@ -305,6 +323,13 @@ function encodeBinaryFrame(header, body) {
|
|
|
305
323
|
frame.set(body, 5 + headerBytes.length);
|
|
306
324
|
return frame;
|
|
307
325
|
}
|
|
326
|
+
function encodeCompressedFrame(json) {
|
|
327
|
+
const deflated = deflateRawSync(Buffer.from(json, "utf8"), { level: 1 });
|
|
328
|
+
const frame = Buffer.allocUnsafe(1 + deflated.length);
|
|
329
|
+
frame.writeUInt8(2, 0);
|
|
330
|
+
deflated.copy(frame, 1);
|
|
331
|
+
return frame;
|
|
332
|
+
}
|
|
308
333
|
// src/auth-rate-limit.ts
|
|
309
334
|
const DEFAULT_AUTH_RATE_POLICY = {
|
|
310
335
|
windowMs: 6e4,
|
|
@@ -1943,7 +1968,8 @@ function projectEvent(sessionId, event) {
|
|
|
1943
1968
|
state: "running",
|
|
1944
1969
|
summary: summarizeArgs(data?.arguments),
|
|
1945
1970
|
...data?.callId ? { callId: String(data.callId) } : {},
|
|
1946
|
-
...editField(data?.name, data?.arguments)
|
|
1971
|
+
...editField(data?.name, data?.arguments),
|
|
1972
|
+
...targetField(data?.name, data?.arguments)
|
|
1947
1973
|
},
|
|
1948
1974
|
ts: tsOf(event)
|
|
1949
1975
|
}
|
|
@@ -2114,6 +2140,40 @@ function attachmentProjection(data) {
|
|
|
2114
2140
|
const attachments = messageAttachments(data);
|
|
2115
2141
|
return attachments.length > 0 ? { attachments } : {};
|
|
2116
2142
|
}
|
|
2143
|
+
/**
|
|
2144
|
+
* 取出一条事件里手机能看到的图片引用,供附件读回补全参数。
|
|
2145
|
+
*
|
|
2146
|
+
* 范围与投影保持一致:只看 user/message 与成功的 tool/result——手机只会拿到这两
|
|
2147
|
+
* 类行上的 attachmentId,不在投影里的图片也不该被读回。
|
|
2148
|
+
* 职责:DSH 0.2.2 起宿主读图接口要完整引用,且不再核对会话是否引用过这张图;
|
|
2149
|
+
* Bridge 用这里收集的引用同时完成「补全参数」和「会话归属校验」。
|
|
2150
|
+
*/
|
|
2151
|
+
function eventImageRefs(event) {
|
|
2152
|
+
let message;
|
|
2153
|
+
if (event.type === "user/message") message = event.data;
|
|
2154
|
+
else if (event.type === "tool/result") {
|
|
2155
|
+
const data = event.data;
|
|
2156
|
+
if (data?.error !== void 0) return [];
|
|
2157
|
+
message = data?.message;
|
|
2158
|
+
} else return [];
|
|
2159
|
+
if (!message || typeof message !== "object") return [];
|
|
2160
|
+
const inner = message.message;
|
|
2161
|
+
const content = (inner && typeof inner === "object" ? inner : message).content;
|
|
2162
|
+
if (!Array.isArray(content)) return [];
|
|
2163
|
+
const refs = [];
|
|
2164
|
+
for (const part of content) {
|
|
2165
|
+
const block = part;
|
|
2166
|
+
if (!block || block.type !== "image") continue;
|
|
2167
|
+
const ref = block.attachment;
|
|
2168
|
+
if (!ref || typeof ref !== "object") continue;
|
|
2169
|
+
if (typeof ref.attachmentId !== "string" || ref.attachmentId.length === 0) continue;
|
|
2170
|
+
refs.push({
|
|
2171
|
+
...ref,
|
|
2172
|
+
attachmentId: ref.attachmentId
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
return refs;
|
|
2176
|
+
}
|
|
2117
2177
|
/** Extract reasoning ("thinking") text from assistant message payloads. */
|
|
2118
2178
|
function messageThinking(data) {
|
|
2119
2179
|
if (!data || typeof data !== "object") return "";
|
|
@@ -2153,6 +2213,125 @@ function editField(name, raw) {
|
|
|
2153
2213
|
const edit = toolEditOf(name, raw);
|
|
2154
2214
|
return edit ? { edit } : {};
|
|
2155
2215
|
}
|
|
2216
|
+
function targetField(name, raw) {
|
|
2217
|
+
const target = toolTargetOf(name, raw);
|
|
2218
|
+
return target ? { target } : {};
|
|
2219
|
+
}
|
|
2220
|
+
/** `tool.target.value` / `scope` 的 UTF-8 上限。 */
|
|
2221
|
+
const MAX_TOOL_TARGET_VALUE_BYTES = 1024;
|
|
2222
|
+
/** 读写文件类工具:路径即操作对象。 */
|
|
2223
|
+
const PATH_TARGET_TOOLS = /* @__PURE__ */ new Set([
|
|
2224
|
+
"read",
|
|
2225
|
+
"write",
|
|
2226
|
+
"edit",
|
|
2227
|
+
"str_replace_editor"
|
|
2228
|
+
]);
|
|
2229
|
+
/** 搜索类工具:搜索式为主值,搜索目录为范围。 */
|
|
2230
|
+
const PATTERN_TARGET_TOOLS = /* @__PURE__ */ new Set(["grep", "glob"]);
|
|
2231
|
+
/** 网页类工具:有网址给网址,否则给查询词。 */
|
|
2232
|
+
const WEB_TARGET_TOOLS = /* @__PURE__ */ new Set(["web_fetch", "web_search"]);
|
|
2233
|
+
/** 协作类工具:对象是另一个智能体或后台任务,按工具各取一个能认出对象的参数。 */
|
|
2234
|
+
const TEXT_TARGET_KEYS = {
|
|
2235
|
+
send_message: ["target", "agent_id"],
|
|
2236
|
+
spawn_teammate: ["name"],
|
|
2237
|
+
subagent: ["description"],
|
|
2238
|
+
job_output: ["job_id"]
|
|
2239
|
+
};
|
|
2240
|
+
/**
|
|
2241
|
+
* 从工具调用的原始参数里挑出操作对象,归一化成 `ToolTargetProjection`。
|
|
2242
|
+
*
|
|
2243
|
+
* 为什么需要它:`tool.summary` 在结果回来后被替换成结果摘要(read 变成文件正文片段),
|
|
2244
|
+
* 客户端从此拿不到「读的是哪个文件」。target 只在调用时生成一次,结果合并时不动。
|
|
2245
|
+
* 规则表之外的工具、无法解析或缺少对应参数时返回 undefined,客户端退回 summary。
|
|
2246
|
+
* 参数名只在这里出现,客户端不依赖宿主工具参数命名。
|
|
2247
|
+
*/
|
|
2248
|
+
function toolTargetOf(name, raw) {
|
|
2249
|
+
if (typeof name !== "string" || typeof raw !== "string" || raw.length === 0) return void 0;
|
|
2250
|
+
let args;
|
|
2251
|
+
try {
|
|
2252
|
+
const parsed = JSON.parse(raw);
|
|
2253
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
2254
|
+
args = parsed;
|
|
2255
|
+
} catch {
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
/** 取第一个非空字符串参数。 */
|
|
2259
|
+
const pick = (...keys) => {
|
|
2260
|
+
for (const key of keys) {
|
|
2261
|
+
const value = args[key];
|
|
2262
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
if (PATH_TARGET_TOOLS.has(name)) {
|
|
2266
|
+
const path = pick("file_path", "path");
|
|
2267
|
+
return path ? {
|
|
2268
|
+
kind: "path",
|
|
2269
|
+
value: boundPath(path)
|
|
2270
|
+
} : void 0;
|
|
2271
|
+
}
|
|
2272
|
+
if (name === "bash") {
|
|
2273
|
+
const command = pick("command");
|
|
2274
|
+
if (!command) return void 0;
|
|
2275
|
+
const label = pick("description");
|
|
2276
|
+
return {
|
|
2277
|
+
kind: "command",
|
|
2278
|
+
value: boundText(command, MAX_TOOL_TARGET_VALUE_BYTES),
|
|
2279
|
+
...label ? { label: boundText(label, 256) } : {}
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
if (PATTERN_TARGET_TOOLS.has(name)) {
|
|
2283
|
+
const pattern = pick("pattern");
|
|
2284
|
+
if (!pattern) return void 0;
|
|
2285
|
+
const scope = pick("path");
|
|
2286
|
+
return {
|
|
2287
|
+
kind: "pattern",
|
|
2288
|
+
value: boundText(pattern, MAX_TOOL_TARGET_VALUE_BYTES),
|
|
2289
|
+
...scope ? { scope: boundPath(scope) } : {}
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
if (WEB_TARGET_TOOLS.has(name)) {
|
|
2293
|
+
const url = pick("url");
|
|
2294
|
+
if (url) return {
|
|
2295
|
+
kind: "url",
|
|
2296
|
+
value: boundText(url, MAX_TOOL_TARGET_VALUE_BYTES)
|
|
2297
|
+
};
|
|
2298
|
+
const query = pick("query");
|
|
2299
|
+
return query ? {
|
|
2300
|
+
kind: "text",
|
|
2301
|
+
value: boundText(query, MAX_TOOL_TARGET_VALUE_BYTES)
|
|
2302
|
+
} : void 0;
|
|
2303
|
+
}
|
|
2304
|
+
const keys = TEXT_TARGET_KEYS[name];
|
|
2305
|
+
if (keys) {
|
|
2306
|
+
const text = pick(...keys);
|
|
2307
|
+
return text ? {
|
|
2308
|
+
kind: "text",
|
|
2309
|
+
value: boundText(text, MAX_TOOL_TARGET_VALUE_BYTES)
|
|
2310
|
+
} : void 0;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
/** 命令 / 文本:空白折叠成单个空格(手机上只显示一行),超限砍结尾加「…」。 */
|
|
2314
|
+
function boundText(value, maxBytes) {
|
|
2315
|
+
const flat = value.replace(/\s+/g, " ");
|
|
2316
|
+
if (Buffer.byteLength(flat, "utf8") <= maxBytes) return flat;
|
|
2317
|
+
return truncateUtf8(flat, maxBytes - Buffer.byteLength("…", "utf8")) + "…";
|
|
2318
|
+
}
|
|
2319
|
+
/** 路径:超限时砍开头、保留结尾(如 `…/Views/A.swift`),保证文件名完整可认。 */
|
|
2320
|
+
function boundPath(value) {
|
|
2321
|
+
const maxBytes = MAX_TOOL_TARGET_VALUE_BYTES;
|
|
2322
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
|
|
2323
|
+
const budget = maxBytes - Buffer.byteLength("…", "utf8");
|
|
2324
|
+
let low = 0;
|
|
2325
|
+
let high = value.length;
|
|
2326
|
+
while (low < high) {
|
|
2327
|
+
const mid = Math.ceil((low + high) / 2);
|
|
2328
|
+
if (Buffer.byteLength(value.slice(value.length - mid), "utf8") <= budget) low = mid;
|
|
2329
|
+
else high = mid - 1;
|
|
2330
|
+
}
|
|
2331
|
+
let start = value.length - low;
|
|
2332
|
+
if (start < value.length && /[\uDC00-\uDFFF]/.test(value[start])) start += 1;
|
|
2333
|
+
return "…" + value.slice(start);
|
|
2334
|
+
}
|
|
2156
2335
|
function summarizeArgs(raw) {
|
|
2157
2336
|
if (typeof raw !== "string" || raw.length === 0) return "";
|
|
2158
2337
|
try {
|
|
@@ -2166,15 +2345,22 @@ function summarizeArgs(raw) {
|
|
|
2166
2345
|
}
|
|
2167
2346
|
/** 宿主的文件编辑工具名(DSH `dsh-tool-str-replace-editor`)。 */
|
|
2168
2347
|
const EDITOR_TOOL_NAME = "str_replace_editor";
|
|
2348
|
+
/** 宿主的字符串替换工具(`file_path` / `old_string` / `new_string` / `replace_all`),实际会话里最常用。 */
|
|
2349
|
+
const EDIT_TOOL_NAME = "edit";
|
|
2350
|
+
/** 宿主的整文件写入工具(`file_path` / `content`)。 */
|
|
2351
|
+
const WRITE_TOOL_NAME = "write";
|
|
2169
2352
|
/**
|
|
2170
2353
|
* 把编辑类工具调用的原始参数归一化成 `ToolEditProjection`。
|
|
2171
2354
|
*
|
|
2172
|
-
*
|
|
2173
|
-
*
|
|
2174
|
-
*
|
|
2355
|
+
* 认三种宿主工具,其他工具或无法解析的参数返回 undefined,工具行保持原样:
|
|
2356
|
+
* - `edit`:`old_string` → `new_string` 为 `replace`;`old_string` 为空串视为新建(`create`)。
|
|
2357
|
+
* - `write`:`content` 为写入后的文件全文,记作 `create`(宿主覆盖已有文件时也一样,原文拿不到)。
|
|
2358
|
+
* - `str_replace_editor`:`str_replace` / `create` / `insert`(`view` 不改文件)。
|
|
2359
|
+
* 与 iOS `ToolEditDiff.parse` 的解析口径一致:新文本缺失视为删除(空串)。
|
|
2175
2360
|
*/
|
|
2176
2361
|
function toolEditOf(name, raw) {
|
|
2177
|
-
if (name !== EDITOR_TOOL_NAME
|
|
2362
|
+
if (name !== EDITOR_TOOL_NAME && name !== EDIT_TOOL_NAME && name !== WRITE_TOOL_NAME) return void 0;
|
|
2363
|
+
if (typeof raw !== "string" || raw.length === 0) return void 0;
|
|
2178
2364
|
let args;
|
|
2179
2365
|
try {
|
|
2180
2366
|
const parsed = JSON.parse(raw);
|
|
@@ -2183,6 +2369,10 @@ function toolEditOf(name, raw) {
|
|
|
2183
2369
|
} catch {
|
|
2184
2370
|
return;
|
|
2185
2371
|
}
|
|
2372
|
+
if (name === EDIT_TOOL_NAME || name === WRITE_TOOL_NAME) {
|
|
2373
|
+
const edit = plainEditOf(name, args);
|
|
2374
|
+
return edit ? boundedEdit(edit) : void 0;
|
|
2375
|
+
}
|
|
2186
2376
|
const path = args.path;
|
|
2187
2377
|
if (typeof path !== "string" || path.length === 0) return void 0;
|
|
2188
2378
|
let edit;
|
|
@@ -2215,6 +2405,32 @@ function toolEditOf(name, raw) {
|
|
|
2215
2405
|
break;
|
|
2216
2406
|
default: return;
|
|
2217
2407
|
}
|
|
2408
|
+
return boundedEdit(edit);
|
|
2409
|
+
}
|
|
2410
|
+
/** `edit` / `write` 的参数(路径键为 `file_path`)。 */
|
|
2411
|
+
function plainEditOf(name, args) {
|
|
2412
|
+
const path = args.file_path;
|
|
2413
|
+
if (typeof path !== "string" || path.length === 0) return void 0;
|
|
2414
|
+
if (name === WRITE_TOOL_NAME) return typeof args.content === "string" ? {
|
|
2415
|
+
path,
|
|
2416
|
+
operation: "create",
|
|
2417
|
+
newText: args.content
|
|
2418
|
+
} : void 0;
|
|
2419
|
+
if (typeof args.old_string !== "string") return void 0;
|
|
2420
|
+
const newText = typeof args.new_string === "string" ? args.new_string : "";
|
|
2421
|
+
return args.old_string.length === 0 ? {
|
|
2422
|
+
path,
|
|
2423
|
+
operation: "create",
|
|
2424
|
+
newText
|
|
2425
|
+
} : {
|
|
2426
|
+
path,
|
|
2427
|
+
operation: "replace",
|
|
2428
|
+
oldText: args.old_string,
|
|
2429
|
+
newText
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
/** 任一侧正文超过上限时两侧都不发,只留路径、操作与 truncated。 */
|
|
2433
|
+
function boundedEdit(edit) {
|
|
2218
2434
|
return [edit.oldText, edit.newText].some((text) => text !== void 0 && Buffer.byteLength(text, "utf8") > 16384) ? withoutEditBodies(edit) : edit;
|
|
2219
2435
|
}
|
|
2220
2436
|
/** 去掉改动正文,只留路径、操作与 truncated 标记(超限或整条消息超限时用)。 */
|
|
@@ -2229,7 +2445,12 @@ function withoutEditBodies(edit) {
|
|
|
2229
2445
|
function truncate(text, max) {
|
|
2230
2446
|
return text.length <= max ? text : text.slice(0, max - 1) + "…";
|
|
2231
2447
|
}
|
|
2232
|
-
|
|
2448
|
+
/**
|
|
2449
|
+
* `touched`(可选,增量 tail 用):记录每一行最后一次被哪个事件改动,键为行的 seq。
|
|
2450
|
+
* 行由自身事件生成时等于它的 seq;工具结果合并回较早的工具调用行时更新为结果事件的
|
|
2451
|
+
* seq。增量 tail 据此找出「sinceSeq 之后改动过的行」,而不只是 seq 更大的行。
|
|
2452
|
+
*/
|
|
2453
|
+
function projectHistory(events, touched) {
|
|
2233
2454
|
const messages = [];
|
|
2234
2455
|
const toolByCall = /* @__PURE__ */ new Map();
|
|
2235
2456
|
for (const entry of events) {
|
|
@@ -2238,6 +2459,7 @@ function projectHistory(events) {
|
|
|
2238
2459
|
seq: event.seq,
|
|
2239
2460
|
ts: tsOf(event)
|
|
2240
2461
|
};
|
|
2462
|
+
touched?.set(event.seq, Math.max(touched.get(event.seq) ?? event.seq, event.seq));
|
|
2241
2463
|
switch (event.type) {
|
|
2242
2464
|
case "system/message": break;
|
|
2243
2465
|
case "user/message":
|
|
@@ -2270,7 +2492,8 @@ function projectHistory(events) {
|
|
|
2270
2492
|
name: String(data?.name ?? "tool"),
|
|
2271
2493
|
state: "running",
|
|
2272
2494
|
summary: summarizeArgs(data?.arguments),
|
|
2273
|
-
...editField(data?.name, data?.arguments)
|
|
2495
|
+
...editField(data?.name, data?.arguments),
|
|
2496
|
+
...targetField(data?.name, data?.arguments)
|
|
2274
2497
|
}
|
|
2275
2498
|
};
|
|
2276
2499
|
messages.push(row);
|
|
@@ -2291,6 +2514,7 @@ function projectHistory(events) {
|
|
|
2291
2514
|
summary
|
|
2292
2515
|
};
|
|
2293
2516
|
if (attachments.attachments) Object.assign(target, attachments);
|
|
2517
|
+
touched?.set(target.seq, Math.max(touched.get(target.seq) ?? target.seq, event.seq));
|
|
2294
2518
|
} else messages.push({
|
|
2295
2519
|
...base,
|
|
2296
2520
|
role: "tool",
|
|
@@ -2611,12 +2835,13 @@ const sessionRows = [
|
|
|
2611
2835
|
const invalid = requireSessionId(checked.value);
|
|
2612
2836
|
if (invalid) return invalid;
|
|
2613
2837
|
if (!isOptionalField(checked.value, "tailCount", (v) => isInteger(v, 1, 1e4))) return reject$1("E_PROTOCOL", "invalid tailCount");
|
|
2838
|
+
if (!isOptionalField(checked.value, "sinceSeq", (v) => typeof v === "number" && Number.isFinite(v) && v >= 0)) return reject$1("E_PROTOCOL", "invalid sinceSeq");
|
|
2614
2839
|
return checked;
|
|
2615
2840
|
},
|
|
2616
2841
|
handle: async (ctx, payload) => {
|
|
2617
2842
|
const sessionId = payload.sessionId;
|
|
2618
2843
|
const buffered = ctx.bufferOpenEvents(sessionId);
|
|
2619
|
-
if (!await ctx.bridge.openSession(ctx, sessionId, payload.tailCount ?? 100)) {
|
|
2844
|
+
if (!await ctx.bridge.openSession(ctx, sessionId, payload.tailCount ?? 100, payload.sinceSeq)) {
|
|
2620
2845
|
ctx.discardOpenBuffer(sessionId, buffered);
|
|
2621
2846
|
return ctx.fail("E_NOT_FOUND", "session history unavailable");
|
|
2622
2847
|
}
|
|
@@ -2638,6 +2863,24 @@ const sessionRows = [
|
|
|
2638
2863
|
ctx.send("s2c.ack", {});
|
|
2639
2864
|
}
|
|
2640
2865
|
},
|
|
2866
|
+
{
|
|
2867
|
+
type: "c2s.session.watch",
|
|
2868
|
+
stage: "authenticated",
|
|
2869
|
+
scopes: READ,
|
|
2870
|
+
capability: "sessionInterest",
|
|
2871
|
+
doc: "PROTOCOL.md c2s.session.watch",
|
|
2872
|
+
validate: (payload) => {
|
|
2873
|
+
const checked = payloadObject(payload);
|
|
2874
|
+
if (!checked.ok) return checked;
|
|
2875
|
+
const ids = checked.value.sessionIds;
|
|
2876
|
+
if (!Array.isArray(ids) || ids.length > 16 || !ids.every((id) => isText(id, 256))) return reject$1("E_PROTOCOL", `sessionIds must be an array of at most 16 session ids`);
|
|
2877
|
+
return checked;
|
|
2878
|
+
},
|
|
2879
|
+
handle: (ctx, payload) => {
|
|
2880
|
+
ctx.setWatchedSessions(parseWatchSessions(payload.sessionIds) ?? []);
|
|
2881
|
+
ctx.send("s2c.ack", {});
|
|
2882
|
+
}
|
|
2883
|
+
},
|
|
2641
2884
|
{
|
|
2642
2885
|
type: "c2s.session.create",
|
|
2643
2886
|
stage: "authenticated",
|
|
@@ -3061,128 +3304,153 @@ const categories = [
|
|
|
3061
3304
|
"security-privacy-permission",
|
|
3062
3305
|
"other"
|
|
3063
3306
|
];
|
|
3064
|
-
const commandRows = [
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
if (
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
"image
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3307
|
+
const commandRows = [
|
|
3308
|
+
...[
|
|
3309
|
+
"commands",
|
|
3310
|
+
"permissions",
|
|
3311
|
+
"feedback",
|
|
3312
|
+
"command"
|
|
3313
|
+
].map((name) => ({
|
|
3314
|
+
type: `c2s.session.${name}`,
|
|
3315
|
+
stage: "authenticated",
|
|
3316
|
+
capability: "commands",
|
|
3317
|
+
scopes: name === "commands" || name === "permissions" ? ["sessions.read"] : ["prompt.send"],
|
|
3318
|
+
doc: `PROTOCOL.md c2s.session.${name}`,
|
|
3319
|
+
validate: (value) => {
|
|
3320
|
+
const result = sessionPayload(value);
|
|
3321
|
+
if (!result.ok) return result;
|
|
3322
|
+
const p = result.value;
|
|
3323
|
+
if (name === "command") {
|
|
3324
|
+
if (typeof p.line !== "string" || p.line.length > 65536 || !/^\/[a-z][a-z0-9_-]*(?:\s|$)/.test(p.line)) return reject$1("E_PROTOCOL", "指令格式无效");
|
|
3325
|
+
if (p.expectedDefinitionId !== void 0 && (typeof p.expectedDefinitionId !== "string" || !p.expectedDefinitionId || p.expectedDefinitionId.length > 512)) return reject$1("E_PROTOCOL", "指令定义标识无效");
|
|
3326
|
+
const attachments = p.attachments ?? [];
|
|
3327
|
+
if (!Array.isArray(attachments) || attachments.length > 4) return reject$1("E_PROTOCOL", "附件数量无效");
|
|
3328
|
+
for (const item of attachments) if (!item || !["image", "document"].includes(item.type) || typeof item.name !== "string" || item.name.length > 512 || typeof item.mediaType !== "string" || typeof item.data !== "string" || item.data.length > 16777216 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(item.data) || item.type === "image" && ![
|
|
3329
|
+
"image/png",
|
|
3330
|
+
"image/jpeg",
|
|
3331
|
+
"image/webp",
|
|
3332
|
+
"image/gif"
|
|
3333
|
+
].includes(item.mediaType)) return reject$1("E_PROTOCOL", "附件格式无效");
|
|
3334
|
+
return {
|
|
3335
|
+
ok: true,
|
|
3336
|
+
value: {
|
|
3337
|
+
...p,
|
|
3338
|
+
attachments
|
|
3339
|
+
}
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
if (name === "feedback" && (typeof p.text !== "string" || p.text.length > 65536 || p.category !== void 0 && !categories.includes(String(p.category)))) return reject$1("E_PROTOCOL", "反馈内容无效");
|
|
3343
|
+
return result;
|
|
3344
|
+
},
|
|
3345
|
+
handle: async (ctx, payload) => {
|
|
3346
|
+
const operation = name === "commands" ? "list" : name === "command" ? "execute" : name;
|
|
3347
|
+
const result = await ctx.bridge.commandOperation(operation, payload);
|
|
3348
|
+
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
3349
|
+
const type = name === "commands" ? "s2c.session.commands" : name === "permissions" ? "s2c.session.permissions" : name === "command" ? "s2c.session.commandResult" : "s2c.ack";
|
|
3350
|
+
ctx.send(type, {
|
|
3351
|
+
sessionId: payload.sessionId,
|
|
3352
|
+
...result.value
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
3355
|
+
})),
|
|
3356
|
+
{
|
|
3357
|
+
type: "c2s.session.references",
|
|
3358
|
+
stage: "authenticated",
|
|
3359
|
+
scopes: ["sessions.read"],
|
|
3360
|
+
capability: "references",
|
|
3361
|
+
doc: "PROTOCOL.md c2s.session.references",
|
|
3362
|
+
validate: (value) => {
|
|
3363
|
+
const result = sessionPayload(value);
|
|
3364
|
+
if (!result.ok) return result;
|
|
3365
|
+
if (typeof result.value.query !== "string" || result.value.query.length > 1024 || /[\r\n]/.test(result.value.query)) return reject$1("E_PROTOCOL", "引用查询无效");
|
|
3366
|
+
return result;
|
|
3367
|
+
},
|
|
3368
|
+
handle: async (ctx, payload) => {
|
|
3369
|
+
const result = await ctx.bridge.referenceCandidates(payload.sessionId, payload.query, ctx.signal);
|
|
3370
|
+
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
3371
|
+
ctx.send("s2c.session.references", {
|
|
3372
|
+
sessionId: payload.sessionId,
|
|
3373
|
+
query: payload.query,
|
|
3374
|
+
...result.value
|
|
3375
|
+
});
|
|
3097
3376
|
}
|
|
3098
|
-
if (name === "feedback" && (typeof p.text !== "string" || p.text.length > 65536 || p.category !== void 0 && !categories.includes(String(p.category)))) return reject$1("E_PROTOCOL", "反馈内容无效");
|
|
3099
|
-
return result;
|
|
3100
|
-
},
|
|
3101
|
-
handle: async (ctx, payload) => {
|
|
3102
|
-
const operation = name === "commands" ? "list" : name === "command" ? "execute" : name;
|
|
3103
|
-
const result = await ctx.bridge.commandOperation(operation, payload);
|
|
3104
|
-
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
3105
|
-
const type = name === "commands" ? "s2c.session.commands" : name === "permissions" ? "s2c.session.permissions" : name === "command" ? "s2c.session.commandResult" : "s2c.ack";
|
|
3106
|
-
ctx.send(type, {
|
|
3107
|
-
sessionId: payload.sessionId,
|
|
3108
|
-
...result.value
|
|
3109
|
-
});
|
|
3110
|
-
}
|
|
3111
|
-
})), {
|
|
3112
|
-
type: "c2s.session.export",
|
|
3113
|
-
stage: "authenticated",
|
|
3114
|
-
scopes: ["sessions.read"],
|
|
3115
|
-
capability: "commands",
|
|
3116
|
-
conditionalCapabilities: ["sessionMarkdown"],
|
|
3117
|
-
doc: "PROTOCOL.md c2s.session.export",
|
|
3118
|
-
validate: (value) => {
|
|
3119
|
-
const result = sessionPayload(value);
|
|
3120
|
-
if (!result.ok) return result;
|
|
3121
|
-
if (result.value.format !== void 0 && result.value.format !== "zip" && result.value.format !== "markdown") return reject$1("E_PROTOCOL", "导出格式无效");
|
|
3122
|
-
if (result.value.binary !== void 0 && typeof result.value.binary !== "boolean") return reject$1("E_PROTOCOL", "传输方式无效");
|
|
3123
|
-
return result;
|
|
3124
3377
|
},
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
});
|
|
3378
|
+
{
|
|
3379
|
+
type: "c2s.session.export",
|
|
3380
|
+
stage: "authenticated",
|
|
3381
|
+
scopes: ["sessions.read"],
|
|
3382
|
+
capability: "commands",
|
|
3383
|
+
conditionalCapabilities: ["sessionMarkdown"],
|
|
3384
|
+
doc: "PROTOCOL.md c2s.session.export",
|
|
3385
|
+
validate: (value) => {
|
|
3386
|
+
const result = sessionPayload(value);
|
|
3387
|
+
if (!result.ok) return result;
|
|
3388
|
+
if (result.value.format !== void 0 && result.value.format !== "zip" && result.value.format !== "markdown") return reject$1("E_PROTOCOL", "导出格式无效");
|
|
3389
|
+
if (result.value.binary !== void 0 && typeof result.value.binary !== "boolean") return reject$1("E_PROTOCOL", "传输方式无效");
|
|
3390
|
+
return result;
|
|
3391
|
+
},
|
|
3392
|
+
handle: async (ctx, payload) => {
|
|
3393
|
+
const format = payload.format;
|
|
3394
|
+
if (format === "markdown" && ctx.bridge.capabilities.sessionMarkdown !== true) return ctx.fail("E_UNSUPPORTED", "当前主机不支持导出对话正文");
|
|
3395
|
+
const signal = AbortSignal.any([ctx.signal ?? new AbortController().signal, AbortSignal.timeout(6e5)]);
|
|
3396
|
+
let reader;
|
|
3397
|
+
const cancel = () => {
|
|
3398
|
+
reader?.cancel().catch(() => {});
|
|
3146
3399
|
};
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
signal.
|
|
3150
|
-
const
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3400
|
+
try {
|
|
3401
|
+
reader = (format === void 0 ? await ctx.bridge.exportSession(payload.sessionId, signal) : await ctx.bridge.exportSession(payload.sessionId, signal, format)).body.getReader();
|
|
3402
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
3403
|
+
const binary = payload.binary === true && ctx.sendBinary !== void 0;
|
|
3404
|
+
let index = 0, bytes = 0;
|
|
3405
|
+
const sendChunk = async (chunk) => {
|
|
3406
|
+
bytes += chunk.length;
|
|
3407
|
+
await ctx.waitForWritable?.();
|
|
3408
|
+
if (binary) ctx.sendBinary("s2c.session.export.chunk", { index: index++ }, chunk);
|
|
3409
|
+
else ctx.send("s2c.session.export.chunk", {
|
|
3410
|
+
index: index++,
|
|
3411
|
+
data: Buffer.from(chunk).toString("base64")
|
|
3412
|
+
});
|
|
3413
|
+
};
|
|
3414
|
+
let carry;
|
|
3415
|
+
while (true) {
|
|
3416
|
+
signal.throwIfAborted();
|
|
3417
|
+
const next = await reader.read();
|
|
3418
|
+
if (next.done) break;
|
|
3419
|
+
if (!binary) {
|
|
3420
|
+
for (let offset = 0; offset < next.value.length; offset += 49152) await sendChunk(next.value.subarray(offset, offset + 49152));
|
|
3421
|
+
continue;
|
|
3422
|
+
}
|
|
3423
|
+
carry = carry ? Buffer.concat([carry, next.value]) : Buffer.from(next.value);
|
|
3424
|
+
while (carry.length >= BINARY_CHUNK_BYTES) {
|
|
3425
|
+
await sendChunk(carry.subarray(0, BINARY_CHUNK_BYTES));
|
|
3426
|
+
carry = carry.subarray(BINARY_CHUNK_BYTES);
|
|
3427
|
+
}
|
|
3160
3428
|
}
|
|
3429
|
+
signal.throwIfAborted();
|
|
3430
|
+
if (carry !== void 0 && carry.length > 0) await sendChunk(carry);
|
|
3431
|
+
ctx.send("s2c.ack", {
|
|
3432
|
+
sessionId: payload.sessionId,
|
|
3433
|
+
fileName: format === "markdown" ? "dsh-session.md" : "dsh-session-log.zip",
|
|
3434
|
+
chunks: index,
|
|
3435
|
+
bytes,
|
|
3436
|
+
...format === void 0 ? {} : { mediaType: format === "markdown" ? "text/markdown; charset=utf-8" : "application/zip" }
|
|
3437
|
+
});
|
|
3438
|
+
} catch (error) {
|
|
3439
|
+
const failure = hostFailureOf(error, "command/export-cancelled");
|
|
3440
|
+
const message = failure.code === "command/export-cancelled" ? "会话日志导出失败,请重试" : failure.message;
|
|
3441
|
+
const mapped = wireErrorOf("command", {
|
|
3442
|
+
...failure,
|
|
3443
|
+
message
|
|
3444
|
+
});
|
|
3445
|
+
ctx.fail(mapped.code, mapped.message);
|
|
3446
|
+
} finally {
|
|
3447
|
+
signal.removeEventListener("abort", cancel);
|
|
3448
|
+
await reader?.cancel().catch(() => {});
|
|
3449
|
+
reader?.releaseLock();
|
|
3161
3450
|
}
|
|
3162
|
-
signal.throwIfAborted();
|
|
3163
|
-
if (carry !== void 0 && carry.length > 0) await sendChunk(carry);
|
|
3164
|
-
ctx.send("s2c.ack", {
|
|
3165
|
-
sessionId: payload.sessionId,
|
|
3166
|
-
fileName: format === "markdown" ? "dsh-session.md" : "dsh-session-log.zip",
|
|
3167
|
-
chunks: index,
|
|
3168
|
-
bytes,
|
|
3169
|
-
...format === void 0 ? {} : { mediaType: format === "markdown" ? "text/markdown; charset=utf-8" : "application/zip" }
|
|
3170
|
-
});
|
|
3171
|
-
} catch (error) {
|
|
3172
|
-
const failure = hostFailureOf(error, "command/export-cancelled");
|
|
3173
|
-
const message = failure.code === "command/export-cancelled" ? "会话日志导出失败,请重试" : failure.message;
|
|
3174
|
-
const mapped = wireErrorOf("command", {
|
|
3175
|
-
...failure,
|
|
3176
|
-
message
|
|
3177
|
-
});
|
|
3178
|
-
ctx.fail(mapped.code, mapped.message);
|
|
3179
|
-
} finally {
|
|
3180
|
-
signal.removeEventListener("abort", cancel);
|
|
3181
|
-
await reader?.cancel().catch(() => {});
|
|
3182
|
-
reader?.releaseLock();
|
|
3183
3451
|
}
|
|
3184
3452
|
}
|
|
3185
|
-
|
|
3453
|
+
];
|
|
3186
3454
|
// src/plugin-update.ts
|
|
3187
3455
|
const PACKAGE = "dsh-deeppilot";
|
|
3188
3456
|
const PHASES = [
|
|
@@ -3588,9 +3856,8 @@ var PluginUpdater = class {
|
|
|
3588
3856
|
record.restartPhase = "rolled-back";
|
|
3589
3857
|
return this.transition(record, "restart-required", "目标安装失败,旧版已重新安装;请在电脑端重启 DSH 后确认恢复");
|
|
3590
3858
|
}
|
|
3591
|
-
if (restored.
|
|
3859
|
+
if (restored.version === void 0 || restored.version === record.fromVersion) {
|
|
3592
3860
|
if ((await this.listBundles()).find((bundle) => bundle.name === PACKAGE)?.version === record.fromVersion) return this.transition(record, "rolled-back", "目标安装失败,宿主已自动恢复旧版本");
|
|
3593
|
-
return this.transition(record, "failed", "宿主已尝试回滚,但无法核对实际旧版本,请在电脑端检查插件");
|
|
3594
3861
|
}
|
|
3595
3862
|
return this.transition(record, "failed", "目标安装失败,宿主恢复旧版也未完成,请在电脑端检查插件");
|
|
3596
3863
|
}
|
|
@@ -4076,6 +4343,8 @@ var ConnectionGate = class {
|
|
|
4076
4343
|
scopes: this.scopes,
|
|
4077
4344
|
widgetClient: this.widgetClient,
|
|
4078
4345
|
resumeCursor: this.widgetClient ? void 0 : resumeCursor,
|
|
4346
|
+
watchSessions: this.widgetClient ? void 0 : parseWatchSessions(p.watchSessions),
|
|
4347
|
+
acceptsDeflate: Array.isArray(p.acceptEncoding) && p.acceptEncoding.includes("deflate-raw"),
|
|
4079
4348
|
deviceName,
|
|
4080
4349
|
appVersion
|
|
4081
4350
|
});
|
|
@@ -4130,6 +4399,7 @@ var ConnectionGate = class {
|
|
|
4130
4399
|
fail: (code, message) => host.fail(env.id, code, message),
|
|
4131
4400
|
close: (code, reason) => host.close(code, reason),
|
|
4132
4401
|
canReceive: (scope) => host.canReceive(scope),
|
|
4402
|
+
accepts: (type, payload) => host.accepts(type, payload),
|
|
4133
4403
|
push: (type, payload, seq) => host.push(type, payload, seq),
|
|
4134
4404
|
lastCursor: () => host.lastCursor(),
|
|
4135
4405
|
replay: (entries) => host.replay(entries),
|
|
@@ -4139,6 +4409,7 @@ var ConnectionGate = class {
|
|
|
4139
4409
|
discardOpenBuffer: (sessionId, buffer) => host.discardOpenBuffer(sessionId, buffer),
|
|
4140
4410
|
flushOpenBuffer: (sessionId, buffer) => host.flushOpenBuffer(sessionId, buffer),
|
|
4141
4411
|
closeOpenSession: (sessionId) => host.closeOpenSession(sessionId),
|
|
4412
|
+
setWatchedSessions: (sessionIds) => host.setWatchedSessions(sessionIds),
|
|
4142
4413
|
nextLiveActivityGeneration: () => host.nextLiveActivityGeneration(),
|
|
4143
4414
|
get liveActivityGeneration() {
|
|
4144
4415
|
return host.liveActivityGeneration;
|
|
@@ -4175,6 +4446,14 @@ var BridgeConnection = class {
|
|
|
4175
4446
|
*/
|
|
4176
4447
|
openingSessionEvents = /* @__PURE__ */ new Map();
|
|
4177
4448
|
openSessions = /* @__PURE__ */ new Set();
|
|
4449
|
+
/**
|
|
4450
|
+
* 关注集合(能力 `sessionInterest`):客户端本机有缓存、需要保持最新的会话。
|
|
4451
|
+
* undefined = 旧客户端从未声明,照旧收全部会话事件。来源:auth.prove 的
|
|
4452
|
+
* `watchSessions`(须在重放前生效)与之后的 c2s.session.watch(整体替换)。
|
|
4453
|
+
*/
|
|
4454
|
+
watchedSessions;
|
|
4455
|
+
/** 客户端认证时声明接受 deflate-raw(能力 `compressedFrames`);认证前一律文本帧。 */
|
|
4456
|
+
compressDownlink = false;
|
|
4178
4457
|
liveActivityRegistrationGeneration = 0;
|
|
4179
4458
|
pendingLiveActivityId;
|
|
4180
4459
|
lastActivity = Date.now();
|
|
@@ -4230,6 +4509,8 @@ var BridgeConnection = class {
|
|
|
4230
4509
|
fail: (id, code, message) => connection.fail(id, code, message),
|
|
4231
4510
|
close: (code, reason) => connection.close(code, reason),
|
|
4232
4511
|
canReceive: (scope) => connection.canReceive(scope),
|
|
4512
|
+
accepts: (type, payload) => connection.accepts(type, payload),
|
|
4513
|
+
setWatchedSessions: (sessionIds) => connection.setWatchedSessions(sessionIds),
|
|
4233
4514
|
push: (type, payload, seq) => connection.push(type, payload, seq),
|
|
4234
4515
|
lastCursor: () => connection.lastCursor(),
|
|
4235
4516
|
replay: (entries) => connection.replay(entries),
|
|
@@ -4315,7 +4596,23 @@ var BridgeConnection = class {
|
|
|
4315
4596
|
canReceive(scope) {
|
|
4316
4597
|
return this.identity?.scopes.has(scope) ?? false;
|
|
4317
4598
|
}
|
|
4599
|
+
/**
|
|
4600
|
+
* 关注过滤:只管按会话投递的两类帧。正在打开(tail 未发出、事件进缓冲)、
|
|
4601
|
+
* 已打开或在关注集合里的会话放行;未声明关注集合的连接全部放行。
|
|
4602
|
+
*/
|
|
4603
|
+
accepts(type, payload) {
|
|
4604
|
+
if (this.watchedSessions === void 0) return true;
|
|
4605
|
+
if (type !== "s2c.session.event" && type !== "s2c.session.queue") return true;
|
|
4606
|
+
const sessionId = payload?.sessionId;
|
|
4607
|
+
if (typeof sessionId !== "string") return true;
|
|
4608
|
+
return this.openSessions.has(sessionId) || this.openingSessionEvents.has(sessionId) || this.watchedSessions.has(sessionId);
|
|
4609
|
+
}
|
|
4610
|
+
/** c2s.session.watch:整体替换关注集合;也让从未声明的连接切到按关注投递。 */
|
|
4611
|
+
setWatchedSessions(sessionIds) {
|
|
4612
|
+
this.watchedSessions = new Set(sessionIds);
|
|
4613
|
+
}
|
|
4318
4614
|
push(type, payload, seq) {
|
|
4615
|
+
if (!this.accepts(type, payload)) return;
|
|
4319
4616
|
if (type === "s2c.notify") payload = {
|
|
4320
4617
|
...payload,
|
|
4321
4618
|
hostAudience: this.deps.audience
|
|
@@ -4352,8 +4649,11 @@ var BridgeConnection = class {
|
|
|
4352
4649
|
/** 认证成功:门只做安全判定,欢迎与重放归连接(数据面)。 */
|
|
4353
4650
|
onAuthenticated(identity) {
|
|
4354
4651
|
this.identity = identity;
|
|
4652
|
+
if (identity.watchSessions !== void 0) this.watchedSessions = new Set(identity.watchSessions);
|
|
4653
|
+
this.compressDownlink = identity.acceptsDeflate;
|
|
4355
4654
|
const cursor = identity.resumeCursor;
|
|
4356
4655
|
const canResume = cursor !== void 0 && this.deps.bridge.canResumeFrom(cursor);
|
|
4656
|
+
const routes = identity.widgetClient ? void 0 : this.deps.routes?.();
|
|
4357
4657
|
this.send("s2c.welcome", {
|
|
4358
4658
|
protocolVersion: 2,
|
|
4359
4659
|
serverVersion: this.deps.serverVersion,
|
|
@@ -4361,7 +4661,8 @@ var BridgeConnection = class {
|
|
|
4361
4661
|
scopes: [...identity.scopes],
|
|
4362
4662
|
capabilities: this.deps.bridge.capabilities,
|
|
4363
4663
|
cursor: this.deps.bridge.currentCursor(),
|
|
4364
|
-
resumed: canResume
|
|
4664
|
+
resumed: canResume,
|
|
4665
|
+
...routes !== void 0 ? { routes } : {}
|
|
4365
4666
|
});
|
|
4366
4667
|
if (!identity.widgetClient) this.deps.bridge.addSink(this);
|
|
4367
4668
|
if (cursor !== void 0) {
|
|
@@ -4411,7 +4712,16 @@ var BridgeConnection = class {
|
|
|
4411
4712
|
this.close(1013, "client too slow");
|
|
4412
4713
|
return;
|
|
4413
4714
|
}
|
|
4414
|
-
|
|
4715
|
+
if (body !== void 0) {
|
|
4716
|
+
this.ws.send(encodeBinaryFrame(envelope, body));
|
|
4717
|
+
return;
|
|
4718
|
+
}
|
|
4719
|
+
const json = JSON.stringify(envelope);
|
|
4720
|
+
if (this.compressDownlink && Buffer.byteLength(json) > 8192) {
|
|
4721
|
+
this.ws.send(encodeCompressedFrame(json));
|
|
4722
|
+
return;
|
|
4723
|
+
}
|
|
4724
|
+
this.ws.send(json);
|
|
4415
4725
|
}
|
|
4416
4726
|
fail(id, code, message) {
|
|
4417
4727
|
this.send("s2c.error", {
|
|
@@ -5495,7 +5805,9 @@ function pushContent(notification, mode) {
|
|
|
5495
5805
|
* Relay connectivity self-test used by the settings page "测试中继" button.
|
|
5496
5806
|
*
|
|
5497
5807
|
* Two steps mirror what zero-touch enrollment actually does:
|
|
5498
|
-
* 1. health — GET {url}/healthz → is the relay reachable?
|
|
5808
|
+
* 1. health — GET {url}/healthz → is the relay reachable? The
|
|
5809
|
+
* relay's `version` (relay 0.2.0+) is appended to the message for
|
|
5810
|
+
* troubleshooting; it never affects the verdict.
|
|
5499
5811
|
* 2. enroll — POST {url}/v1/enroll → does the shared key grant a token?
|
|
5500
5812
|
*
|
|
5501
5813
|
* The enroll step doubles as a repair path: a token it issues is handed back
|
|
@@ -5505,6 +5817,16 @@ function pushContent(notification, mode) {
|
|
|
5505
5817
|
* Transport is injectable (`fetchImpl`) so tests never touch the network.
|
|
5506
5818
|
*/
|
|
5507
5819
|
const DEFAULT_TIMEOUT_MS = 6e3;
|
|
5820
|
+
/**
|
|
5821
|
+
* semver-shaped characters only, bounded: the value comes from a remote
|
|
5822
|
+
* server and is rendered verbatim in the settings page.
|
|
5823
|
+
*/
|
|
5824
|
+
const RELAY_VERSION_PATTERN = /^[0-9A-Za-z.+-]{1,32}$/;
|
|
5825
|
+
/** Display-only relay version from a /healthz body; undefined for older relays or unsafe values. */
|
|
5826
|
+
function relayVersionOf(body) {
|
|
5827
|
+
const version = body?.version;
|
|
5828
|
+
return typeof version === "string" && RELAY_VERSION_PATTERN.test(version) ? version : void 0;
|
|
5829
|
+
}
|
|
5508
5830
|
async function requestJson(fetchImpl, url, init, timeoutMs) {
|
|
5509
5831
|
const response = await fetchImpl(url, {
|
|
5510
5832
|
...init,
|
|
@@ -5529,13 +5851,16 @@ async function runRelayProbe(options) {
|
|
|
5529
5851
|
const startedAt = Date.now();
|
|
5530
5852
|
const { status, body } = await requestJson(fetchImpl, `${base}/healthz`, { method: "GET" }, timeoutMs);
|
|
5531
5853
|
const latencyMs = Date.now() - startedAt;
|
|
5532
|
-
if (status === 200 && body?.ok === true)
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5854
|
+
if (status === 200 && body?.ok === true) {
|
|
5855
|
+
const version = relayVersionOf(body);
|
|
5856
|
+
const message = version ? `中继服务可达(版本 ${version})` : "中继服务可达";
|
|
5857
|
+
steps.push({
|
|
5858
|
+
id: "health",
|
|
5859
|
+
ok: true,
|
|
5860
|
+
message,
|
|
5861
|
+
latencyMs
|
|
5862
|
+
});
|
|
5863
|
+
} else steps.push({
|
|
5539
5864
|
id: "health",
|
|
5540
5865
|
ok: false,
|
|
5541
5866
|
message: `中继响应异常(HTTP ${status})`,
|
|
@@ -6279,7 +6604,7 @@ var TransportReconciler = class {
|
|
|
6279
6604
|
function createLocalTransport(deps) {
|
|
6280
6605
|
return new TransportReconciler({
|
|
6281
6606
|
keyOf: (config) => ({
|
|
6282
|
-
enabled: config.enabled === true
|
|
6607
|
+
enabled: config.enabled === true,
|
|
6283
6608
|
port: normalizeLocalPort(config.local?.port)
|
|
6284
6609
|
}),
|
|
6285
6610
|
skipWhenDisabled: true,
|
|
@@ -6347,7 +6672,7 @@ function createLocalTransport(deps) {
|
|
|
6347
6672
|
};
|
|
6348
6673
|
}
|
|
6349
6674
|
}, deps.config, deps.log, {
|
|
6350
|
-
phase: deps.config().enabled === true
|
|
6675
|
+
phase: deps.config().enabled === true ? "starting" : "disabled",
|
|
6351
6676
|
port: normalizeLocalPort(deps.config().local?.port),
|
|
6352
6677
|
endpoints: [],
|
|
6353
6678
|
updatedAt: Date.now()
|
|
@@ -7146,6 +7471,7 @@ const HOST_CAPABILITY_PROBES = {
|
|
|
7146
7471
|
models: (proxy) => typeof proxy.sessions.models === "function",
|
|
7147
7472
|
skills: (proxy) => typeof proxy.sessions.skills === "function",
|
|
7148
7473
|
commands: (proxy) => typeof proxy.commands?.list === "function" && typeof proxy.commands?.execute === "function",
|
|
7474
|
+
references: (proxy) => typeof proxy.references?.list === "function",
|
|
7149
7475
|
sessionManagement: (proxy) => typeof proxy.sessions.rename === "function" && typeof proxy.workspace?.archiveSession === "function",
|
|
7150
7476
|
sessionFork: (proxy) => typeof proxy.sessions.fork === "function",
|
|
7151
7477
|
sessionRestore: (proxy) => typeof proxy.workspace?.unarchiveSession === "function",
|
|
@@ -7166,6 +7492,10 @@ function capabilityBits(proxy, pushOutlet) {
|
|
|
7166
7492
|
historyPaging: true,
|
|
7167
7493
|
replay: true,
|
|
7168
7494
|
binaryFrames: true,
|
|
7495
|
+
sessionInterest: true,
|
|
7496
|
+
compressedFrames: true,
|
|
7497
|
+
incrementalTail: true,
|
|
7498
|
+
routeHints: true,
|
|
7169
7499
|
approvals: true,
|
|
7170
7500
|
questions: true,
|
|
7171
7501
|
pendingSnapshot: true,
|
|
@@ -7174,6 +7504,7 @@ function capabilityBits(proxy, pushOutlet) {
|
|
|
7174
7504
|
models: HOST_CAPABILITY_PROBES.models(proxy),
|
|
7175
7505
|
skills: HOST_CAPABILITY_PROBES.skills(proxy),
|
|
7176
7506
|
commands: HOST_CAPABILITY_PROBES.commands(proxy),
|
|
7507
|
+
references: HOST_CAPABILITY_PROBES.references(proxy),
|
|
7177
7508
|
sessionManagement: HOST_CAPABILITY_PROBES.sessionManagement(proxy),
|
|
7178
7509
|
sessionFork: HOST_CAPABILITY_PROBES.sessionFork(proxy),
|
|
7179
7510
|
sessionRestore: HOST_CAPABILITY_PROBES.sessionRestore(proxy),
|
|
@@ -7441,8 +7772,75 @@ var AssistantStream = class {
|
|
|
7441
7772
|
this.attempts.clear();
|
|
7442
7773
|
}
|
|
7443
7774
|
};
|
|
7775
|
+
// src/image-ref-index.ts
|
|
7776
|
+
/** 缓存条目上限:引用只有几十字节,4096 条足够覆盖手机近期能看到的图片。 */
|
|
7777
|
+
const MAX_CACHED_IMAGE_REFS = 4096;
|
|
7778
|
+
var ImageRefIndex = class {
|
|
7779
|
+
maxEntries;
|
|
7780
|
+
/** 键为 `sessionId\0attachmentId`;Map 的插入顺序即 LRU 顺序。 */
|
|
7781
|
+
refs = /* @__PURE__ */ new Map();
|
|
7782
|
+
/** 同一会话的并发回查共用一次扫描(冷启动时多张缩略图会同时请求)。 */
|
|
7783
|
+
scans = /* @__PURE__ */ new Map();
|
|
7784
|
+
constructor(maxEntries = MAX_CACHED_IMAGE_REFS) {
|
|
7785
|
+
this.maxEntries = maxEntries;
|
|
7786
|
+
}
|
|
7787
|
+
/** 记下一批事件里的图片引用;对非图片事件是空操作。 */
|
|
7788
|
+
remember(sessionId, events) {
|
|
7789
|
+
for (const event of events) for (const ref of eventImageRefs(event)) this.put(sessionId, ref);
|
|
7790
|
+
}
|
|
7791
|
+
/** 只查缓存,不触发扫描。 */
|
|
7792
|
+
peek(sessionId, attachmentId) {
|
|
7793
|
+
const key = keyOf(sessionId, attachmentId);
|
|
7794
|
+
const ref = this.refs.get(key);
|
|
7795
|
+
if (ref) {
|
|
7796
|
+
this.refs.delete(key);
|
|
7797
|
+
this.refs.set(key, ref);
|
|
7798
|
+
}
|
|
7799
|
+
return ref;
|
|
7800
|
+
}
|
|
7801
|
+
/** 缓存优先;未命中时扫描会话历史后再查一次。找不到返回 undefined。 */
|
|
7802
|
+
async resolve(sessionId, attachmentId, readPage) {
|
|
7803
|
+
const cached = this.peek(sessionId, attachmentId);
|
|
7804
|
+
if (cached) return cached;
|
|
7805
|
+
let scan = this.scans.get(sessionId);
|
|
7806
|
+
if (!scan) {
|
|
7807
|
+
scan = this.scan(sessionId, readPage).finally(() => this.scans.delete(sessionId));
|
|
7808
|
+
this.scans.set(sessionId, scan);
|
|
7809
|
+
}
|
|
7810
|
+
await scan;
|
|
7811
|
+
return this.peek(sessionId, attachmentId);
|
|
7812
|
+
}
|
|
7813
|
+
async scan(sessionId, readPage) {
|
|
7814
|
+
let beforeSeq;
|
|
7815
|
+
for (let page = 0; page < 20; page++) {
|
|
7816
|
+
const result = await readPage(sessionId, beforeSeq);
|
|
7817
|
+
if (!result || result.events.length === 0) return;
|
|
7818
|
+
this.remember(sessionId, result.events);
|
|
7819
|
+
const oldest = Math.min(...result.events.map((event) => event.seq));
|
|
7820
|
+
if (!result.hasMore || !Number.isFinite(oldest) || beforeSeq !== void 0 && oldest >= beforeSeq) return;
|
|
7821
|
+
beforeSeq = oldest;
|
|
7822
|
+
}
|
|
7823
|
+
}
|
|
7824
|
+
put(sessionId, ref) {
|
|
7825
|
+
const key = keyOf(sessionId, ref.attachmentId);
|
|
7826
|
+
this.refs.delete(key);
|
|
7827
|
+
this.refs.set(key, ref);
|
|
7828
|
+
while (this.refs.size > this.maxEntries) {
|
|
7829
|
+
const oldest = this.refs.keys().next().value;
|
|
7830
|
+
if (oldest === void 0) break;
|
|
7831
|
+
this.refs.delete(oldest);
|
|
7832
|
+
}
|
|
7833
|
+
}
|
|
7834
|
+
};
|
|
7835
|
+
function keyOf(sessionId, attachmentId) {
|
|
7836
|
+
return `${sessionId}\u0000${attachmentId}`;
|
|
7837
|
+
}
|
|
7444
7838
|
// src/host-bridge.ts
|
|
7445
7839
|
const MAX_RING_DEFAULT = 2e3;
|
|
7840
|
+
/** 补发量超过它就改发 s2c.resync(经 Funnel 实测吞吐 40~80 KB/s,约 6~12 秒)。 */
|
|
7841
|
+
const REPLAY_RESYNC_BYTES = 524288;
|
|
7842
|
+
/** 只改了用量统计的列表推送,同一会话这段时间内最多推一次(第二批 P5)。 */
|
|
7843
|
+
const SUMMARY_STATS_THROTTLE_MS = 5e3;
|
|
7446
7844
|
/**
|
|
7447
7845
|
* Process-wide bridge state: session mirror, pending approvals/questions,
|
|
7448
7846
|
* and the per-device replay ring. Consumes the in-process mux/host streams
|
|
@@ -7455,6 +7853,13 @@ var HostBridge = class {
|
|
|
7455
7853
|
log;
|
|
7456
7854
|
id = ++BRIDGE_SEQ;
|
|
7457
7855
|
summaries = /* @__PURE__ */ new Map();
|
|
7856
|
+
/**
|
|
7857
|
+
* 列表推送节流(连接传输第二批 P5)。每个会话最近一次推出去的摘要指纹,
|
|
7858
|
+
* 用来丢掉内容完全相同的重复推送;只改了 `stats` 的变化按 SUMMARY_STATS_THROTTLE_MS
|
|
7859
|
+
* 合并,状态、标题、任务清单、待处理标记、活动描述的变化照旧立即推。
|
|
7860
|
+
*/
|
|
7861
|
+
summaryPushed = /* @__PURE__ */ new Map();
|
|
7862
|
+
summaryStatsTimers = /* @__PURE__ */ new Map();
|
|
7458
7863
|
activeTools = /* @__PURE__ */ new Map();
|
|
7459
7864
|
approvals = /* @__PURE__ */ new Map();
|
|
7460
7865
|
questions = /* @__PURE__ */ new Map();
|
|
@@ -7478,6 +7883,8 @@ var HostBridge = class {
|
|
|
7478
7883
|
abort = new AbortController();
|
|
7479
7884
|
started = false;
|
|
7480
7885
|
disposed = false;
|
|
7886
|
+
/** attachmentId → 宿主完整图片引用,供 attachmentData 补全 DSH 0.2.2+ 的读图参数。 */
|
|
7887
|
+
imageRefs = new ImageRefIndex();
|
|
7481
7888
|
promptDeliveries;
|
|
7482
7889
|
scheduleMutations;
|
|
7483
7890
|
forkMutations;
|
|
@@ -7647,16 +8054,35 @@ var HostBridge = class {
|
|
|
7647
8054
|
* reader without interactions.respond never gets the missed approval
|
|
7648
8055
|
* frames back (R1/P2).
|
|
7649
8056
|
*/
|
|
8057
|
+
/**
|
|
8058
|
+
* 按序补发游标之后的帧。先按权限与关注集合筛出该 sink 真正会收到的帧,
|
|
8059
|
+
* 估算字节数超过 REPLAY_RESYNC_BYTES 时改发 `s2c.resync`:补发是同步一次性
|
|
8060
|
+
* 写出的,经 Funnel 时插件这侧看不到积压,大补发会把之后所有请求响应堵在
|
|
8061
|
+
* 链路上(实测 450 帧堵 37 秒)。重新同步让客户端只拉列表和正在看的会话。
|
|
8062
|
+
*/
|
|
7650
8063
|
resumeFrom(cursor, target) {
|
|
7651
8064
|
const oldest = this.ring.length > 0 ? this.ring[0].seq : this.cursor + 1;
|
|
7652
8065
|
if (cursor + 1 < oldest) return false;
|
|
7653
8066
|
const receivers = target !== void 0 ? [target] : [...this.sinks];
|
|
7654
|
-
for (const
|
|
7655
|
-
const
|
|
7656
|
-
|
|
7657
|
-
|
|
8067
|
+
for (const sink of receivers) {
|
|
8068
|
+
const window = [];
|
|
8069
|
+
let bytes = 0;
|
|
8070
|
+
for (const entry of this.ring) {
|
|
8071
|
+
if (entry.seq <= cursor) continue;
|
|
8072
|
+
const need = pushScopeFor(entry.type, entry.payload);
|
|
8073
|
+
if (need === void 0 || !sink.canReceive(need)) continue;
|
|
8074
|
+
if (sink.accepts && !sink.accepts(entry.type, entry.payload)) continue;
|
|
8075
|
+
window.push(entry);
|
|
8076
|
+
bytes += Buffer.byteLength(JSON.stringify(entry.payload) ?? "");
|
|
8077
|
+
}
|
|
8078
|
+
if (bytes > 524288) {
|
|
8079
|
+
this.log?.(`resume window ${bytes}B over ${REPLAY_RESYNC_BYTES}B; sending resync`);
|
|
8080
|
+
sink.resync();
|
|
8081
|
+
continue;
|
|
8082
|
+
}
|
|
8083
|
+
for (const entry of window) sink.replay([entry]);
|
|
8084
|
+
sink.replayDone();
|
|
7658
8085
|
}
|
|
7659
|
-
for (const sink of receivers) sink.replayDone();
|
|
7660
8086
|
return true;
|
|
7661
8087
|
}
|
|
7662
8088
|
refreshLiveActivities() {
|
|
@@ -7745,6 +8171,8 @@ var HostBridge = class {
|
|
|
7745
8171
|
if (this.disposed) return;
|
|
7746
8172
|
this.disposed = true;
|
|
7747
8173
|
this.abort.abort();
|
|
8174
|
+
for (const timer of this.summaryStatsTimers.values()) clearTimeout(timer);
|
|
8175
|
+
this.summaryStatsTimers.clear();
|
|
7748
8176
|
this.assistantStream.dispose();
|
|
7749
8177
|
this.sinks.clear();
|
|
7750
8178
|
this.sinkSessions.clear();
|
|
@@ -7833,6 +8261,7 @@ var HostBridge = class {
|
|
|
7833
8261
|
this.noteActivity(sessionId, event);
|
|
7834
8262
|
if (event.type === "user/message") this.quietCompletion.noteUserMessage(sessionId, userMessageSourceKind(event.data), event.seq);
|
|
7835
8263
|
this.captureAssistantText(sessionId, event);
|
|
8264
|
+
this.imageRefs.remember(sessionId, [event]);
|
|
7836
8265
|
const projection = projectEvent(sessionId, event);
|
|
7837
8266
|
this.captureActivity(sessionId, event, projection?.data.tool);
|
|
7838
8267
|
if (projection) this.record("s2c.session.event", {
|
|
@@ -7856,6 +8285,7 @@ var HostBridge = class {
|
|
|
7856
8285
|
const p = frame;
|
|
7857
8286
|
if (!p.sessionId) break;
|
|
7858
8287
|
if (p.key === "workingDirectory" || p.key === "agentPreset") this.record("s2c.skills.changed", { sessionId: p.sessionId });
|
|
8288
|
+
if (p.key === "permissions") this.record("s2c.commands.changed", { sessionId: p.sessionId });
|
|
7859
8289
|
if (p.key === "modelSelection") {
|
|
7860
8290
|
const next = p.value?.next;
|
|
7861
8291
|
const current = next && typeof next.provider === "string" && typeof next.model === "string" ? {
|
|
@@ -7999,6 +8429,8 @@ var HostBridge = class {
|
|
|
7999
8429
|
this.archivedSummaries = nextArchived;
|
|
8000
8430
|
const removedIds = [...previousIds].filter((id) => !next.has(id));
|
|
8001
8431
|
for (const id of this.lastAssistantText.keys()) if (!next.has(id)) this.lastAssistantText.delete(id);
|
|
8432
|
+
for (const id of removedIds) this.forgetSummaryPush(id);
|
|
8433
|
+
for (const row of next.values()) this.noteSummaryPushed(row);
|
|
8002
8434
|
this.record("s2c.sessions.delta", {
|
|
8003
8435
|
upserted: [...next.values()],
|
|
8004
8436
|
removedIds
|
|
@@ -8118,11 +8550,62 @@ var HostBridge = class {
|
|
|
8118
8550
|
this.pushSummary(row);
|
|
8119
8551
|
}
|
|
8120
8552
|
pushSummary(row) {
|
|
8553
|
+
const full = JSON.stringify(row);
|
|
8554
|
+
const previous = this.summaryPushed.get(row.id);
|
|
8555
|
+
if (previous?.full === full) return;
|
|
8556
|
+
const withoutStats = JSON.stringify({
|
|
8557
|
+
...row,
|
|
8558
|
+
stats: void 0
|
|
8559
|
+
});
|
|
8560
|
+
const now = Date.now();
|
|
8561
|
+
if (previous !== void 0 && previous.withoutStats === withoutStats && now - previous.at < 5e3) {
|
|
8562
|
+
if (!this.summaryStatsTimers.has(row.id)) {
|
|
8563
|
+
const timer = setTimeout(() => {
|
|
8564
|
+
this.summaryStatsTimers.delete(row.id);
|
|
8565
|
+
const latest = this.summaries.get(row.id);
|
|
8566
|
+
if (latest && !this.disposed) this.pushSummary(latest);
|
|
8567
|
+
}, SUMMARY_STATS_THROTTLE_MS - (now - previous.at));
|
|
8568
|
+
timer.unref?.();
|
|
8569
|
+
this.summaryStatsTimers.set(row.id, timer);
|
|
8570
|
+
}
|
|
8571
|
+
return;
|
|
8572
|
+
}
|
|
8573
|
+
const pending = this.summaryStatsTimers.get(row.id);
|
|
8574
|
+
if (pending !== void 0) {
|
|
8575
|
+
clearTimeout(pending);
|
|
8576
|
+
this.summaryStatsTimers.delete(row.id);
|
|
8577
|
+
}
|
|
8578
|
+
this.summaryPushed.set(row.id, {
|
|
8579
|
+
full,
|
|
8580
|
+
withoutStats,
|
|
8581
|
+
at: now
|
|
8582
|
+
});
|
|
8121
8583
|
this.record("s2c.sessions.delta", {
|
|
8122
8584
|
upserted: [row],
|
|
8123
8585
|
removedIds: []
|
|
8124
8586
|
});
|
|
8125
8587
|
}
|
|
8588
|
+
noteSummaryPushed(row) {
|
|
8589
|
+
const pending = this.summaryStatsTimers.get(row.id);
|
|
8590
|
+
if (pending !== void 0) {
|
|
8591
|
+
clearTimeout(pending);
|
|
8592
|
+
this.summaryStatsTimers.delete(row.id);
|
|
8593
|
+
}
|
|
8594
|
+
this.summaryPushed.set(row.id, {
|
|
8595
|
+
full: JSON.stringify(row),
|
|
8596
|
+
withoutStats: JSON.stringify({
|
|
8597
|
+
...row,
|
|
8598
|
+
stats: void 0
|
|
8599
|
+
}),
|
|
8600
|
+
at: Date.now()
|
|
8601
|
+
});
|
|
8602
|
+
}
|
|
8603
|
+
forgetSummaryPush(sessionId) {
|
|
8604
|
+
const pending = this.summaryStatsTimers.get(sessionId);
|
|
8605
|
+
if (pending !== void 0) clearTimeout(pending);
|
|
8606
|
+
this.summaryStatsTimers.delete(sessionId);
|
|
8607
|
+
this.summaryPushed.delete(sessionId);
|
|
8608
|
+
}
|
|
8126
8609
|
listSessions() {
|
|
8127
8610
|
return [...this.summaries.values()].sort((a, b) => b.lastActivityTs - a.lastActivityTs);
|
|
8128
8611
|
}
|
|
@@ -8186,7 +8669,7 @@ var HostBridge = class {
|
|
|
8186
8669
|
} catch {}
|
|
8187
8670
|
}
|
|
8188
8671
|
/** Tail history for an opened session; pushes s2c.session.tail to the sink. */
|
|
8189
|
-
async openSession(sink, sessionId, tailCount) {
|
|
8672
|
+
async openSession(sink, sessionId, tailCount, sinceSeq) {
|
|
8190
8673
|
try {
|
|
8191
8674
|
const response = await this.apiProxy.sessions.history({
|
|
8192
8675
|
rpcId: randomUUID(),
|
|
@@ -8197,14 +8680,27 @@ var HostBridge = class {
|
|
|
8197
8680
|
});
|
|
8198
8681
|
if (!response.result || !response.result.ok) return false;
|
|
8199
8682
|
const result = response.result.value;
|
|
8200
|
-
const
|
|
8683
|
+
const events = result.events ?? [];
|
|
8684
|
+
this.imageRefs.remember(sessionId, events.map((row) => row.event));
|
|
8685
|
+
const touched = /* @__PURE__ */ new Map();
|
|
8686
|
+
const projected = projectHistory(events, touched);
|
|
8687
|
+
const page = limitSessionPageMessages(projected);
|
|
8201
8688
|
const messages = page.messages;
|
|
8202
8689
|
const oldestSeq = messages.length > 0 ? messages[0].seq : 0;
|
|
8203
|
-
|
|
8690
|
+
const hasMore = Boolean(result.hasMore) || page.dropped > 0;
|
|
8691
|
+
const delta = sinceSeq === void 0 ? void 0 : incrementalTail(events, projected, touched, sinceSeq);
|
|
8692
|
+
sink.push("s2c.session.tail", delta === void 0 ? {
|
|
8204
8693
|
sessionId,
|
|
8205
8694
|
messages,
|
|
8206
8695
|
oldestSeq,
|
|
8207
|
-
hasMore
|
|
8696
|
+
hasMore
|
|
8697
|
+
} : {
|
|
8698
|
+
sessionId,
|
|
8699
|
+
messages: delta,
|
|
8700
|
+
oldestSeq,
|
|
8701
|
+
hasMore,
|
|
8702
|
+
incremental: true,
|
|
8703
|
+
sinceSeq
|
|
8208
8704
|
});
|
|
8209
8705
|
this.deriveTitleFallback(sessionId, messages);
|
|
8210
8706
|
this.refreshProjections(sessionId).then(() => this.pushQueueSnapshot(sink, sessionId));
|
|
@@ -8257,6 +8753,7 @@ var HostBridge = class {
|
|
|
8257
8753
|
});
|
|
8258
8754
|
if (!response.result || !response.result.ok) return null;
|
|
8259
8755
|
const result = response.result.value;
|
|
8756
|
+
this.imageRefs.remember(sessionId, (result.events ?? []).map((row) => row.event));
|
|
8260
8757
|
const page = limitSessionPageMessages(projectHistory(result.events ?? []).filter((message) => message.seq < beforeSeq));
|
|
8261
8758
|
return {
|
|
8262
8759
|
sessionId,
|
|
@@ -8267,16 +8764,27 @@ var HostBridge = class {
|
|
|
8267
8764
|
return null;
|
|
8268
8765
|
}
|
|
8269
8766
|
}
|
|
8270
|
-
/**
|
|
8767
|
+
/**
|
|
8768
|
+
* Result of one attachment read-back for the phone.
|
|
8769
|
+
*
|
|
8770
|
+
* 宿主读图参数有两代:DSH ≤ 0.2.1 收 `{sessionId, attachmentId}` 并自己在会话日志里
|
|
8771
|
+
* 找引用;DSH 0.2.2 起改收 `{sessionId, attachment: 完整引用}`,且不再核对会话。
|
|
8772
|
+
* 控制器是直接注入的实例、不经参数 schema 校验,所以两代字段一起传:旧宿主读前者,
|
|
8773
|
+
* 新宿主读后者。完整引用来自 imageRefs(本会话投影过的图片);找不到时不带
|
|
8774
|
+
* `attachment`,旧宿主照常自查,新宿主读不到即回 null(E_NOT_FOUND)——手机因此
|
|
8775
|
+
* 读不到不属于该会话的图片。
|
|
8776
|
+
*/
|
|
8271
8777
|
async attachmentData(sessionId, attachmentId) {
|
|
8272
8778
|
const read = this.apiProxy.sessions.attachment;
|
|
8273
8779
|
if (typeof read !== "function") return null;
|
|
8274
8780
|
try {
|
|
8781
|
+
const ref = await this.imageRefs.resolve(sessionId, attachmentId, (id, beforeSeq) => this.readHistoryEvents(id, beforeSeq));
|
|
8275
8782
|
const response = await read.call(this.apiProxy.sessions, {
|
|
8276
8783
|
rpcId: randomUUID(),
|
|
8277
8784
|
payload: {
|
|
8278
8785
|
sessionId,
|
|
8279
|
-
attachmentId
|
|
8786
|
+
attachmentId,
|
|
8787
|
+
...ref ? { attachment: ref } : {}
|
|
8280
8788
|
}
|
|
8281
8789
|
});
|
|
8282
8790
|
if (!response.result || !response.result.ok) return null;
|
|
@@ -8290,6 +8798,27 @@ var HostBridge = class {
|
|
|
8290
8798
|
return null;
|
|
8291
8799
|
}
|
|
8292
8800
|
}
|
|
8801
|
+
/** 图片引用回查用的原始历史页:只取事件,不投影、不推给手机。 */
|
|
8802
|
+
async readHistoryEvents(sessionId, beforeSeq) {
|
|
8803
|
+
try {
|
|
8804
|
+
const response = await this.apiProxy.sessions.history({
|
|
8805
|
+
rpcId: randomUUID(),
|
|
8806
|
+
payload: {
|
|
8807
|
+
sessionId,
|
|
8808
|
+
...beforeSeq === void 0 ? {} : { beforeSeq },
|
|
8809
|
+
maxMessages: 500
|
|
8810
|
+
}
|
|
8811
|
+
});
|
|
8812
|
+
if (!response.result || !response.result.ok) return null;
|
|
8813
|
+
const result = response.result.value;
|
|
8814
|
+
return {
|
|
8815
|
+
events: (result.events ?? []).map((row) => row.event),
|
|
8816
|
+
hasMore: Boolean(result.hasMore)
|
|
8817
|
+
};
|
|
8818
|
+
} catch {
|
|
8819
|
+
return null;
|
|
8820
|
+
}
|
|
8821
|
+
}
|
|
8293
8822
|
async commandOperation(operation, payload) {
|
|
8294
8823
|
const api = this.apiProxy.commands;
|
|
8295
8824
|
if (!api) return {
|
|
@@ -8311,6 +8840,42 @@ var HostBridge = class {
|
|
|
8311
8840
|
return wireErrorOf("command", hostFailureOf(error, "command/invalid-response"));
|
|
8312
8841
|
}
|
|
8313
8842
|
}
|
|
8843
|
+
/**
|
|
8844
|
+
* 输入框 `@` 引用候选。两类候选的单类失败已在 reference-api 内吸收为空列表,
|
|
8845
|
+
* 这里只处理「两类都不可用」与代理层意外异常。
|
|
8846
|
+
*/
|
|
8847
|
+
async referenceCandidates(sessionId, query, signal) {
|
|
8848
|
+
const api = this.apiProxy.references;
|
|
8849
|
+
if (!api) return {
|
|
8850
|
+
ok: false,
|
|
8851
|
+
code: "E_UNSUPPORTED",
|
|
8852
|
+
message: "当前主机不支持引用文件或会话"
|
|
8853
|
+
};
|
|
8854
|
+
try {
|
|
8855
|
+
const response = await api.list({
|
|
8856
|
+
rpcId: randomUUID(),
|
|
8857
|
+
payload: {
|
|
8858
|
+
sessionId,
|
|
8859
|
+
query
|
|
8860
|
+
}
|
|
8861
|
+
}, signal);
|
|
8862
|
+
if (response.result?.ok && response.result.value) return {
|
|
8863
|
+
ok: true,
|
|
8864
|
+
value: response.result.value
|
|
8865
|
+
};
|
|
8866
|
+
return {
|
|
8867
|
+
ok: false,
|
|
8868
|
+
code: "E_INTERNAL",
|
|
8869
|
+
message: "无法读取引用候选"
|
|
8870
|
+
};
|
|
8871
|
+
} catch {
|
|
8872
|
+
return {
|
|
8873
|
+
ok: false,
|
|
8874
|
+
code: "E_INTERNAL",
|
|
8875
|
+
message: "无法读取引用候选"
|
|
8876
|
+
};
|
|
8877
|
+
}
|
|
8878
|
+
}
|
|
8314
8879
|
async exportSession(sessionId, signal, format) {
|
|
8315
8880
|
const api = this.apiProxy.commands;
|
|
8316
8881
|
if (!api) throw Object.assign(/* @__PURE__ */ new Error("当前主机不支持日志导出"), { code: "command/export-unsupported" });
|
|
@@ -8487,6 +9052,7 @@ var HostBridge = class {
|
|
|
8487
9052
|
this.archivedSessionIds = new Set((response.result.value.archivedSessionIds ?? []).map(String));
|
|
8488
9053
|
this.summaries.delete(sessionId);
|
|
8489
9054
|
this.lastAssistantText.delete(sessionId);
|
|
9055
|
+
this.forgetSummaryPush(sessionId);
|
|
8490
9056
|
this.record("s2c.sessions.delta", {
|
|
8491
9057
|
upserted: [],
|
|
8492
9058
|
removedIds: [sessionId]
|
|
@@ -9769,6 +10335,25 @@ function receiptFailureReason(receipt) {
|
|
|
9769
10335
|
* 区间与 wire 行校验(tailCount 1..10000、limit 1..500)共用投影 module 的
|
|
9770
10336
|
* 常量——「一页能要多少条消息」本是一条规则,原先散在三处。
|
|
9771
10337
|
*/
|
|
10338
|
+
/**
|
|
10339
|
+
* 增量 tail(能力 `incrementalTail`):返回 sinceSeq 之后改动过的行;不能保证完整时
|
|
10340
|
+
* 返回 undefined,调用方回退全量。三种回退:
|
|
10341
|
+
* - 窗口没覆盖到 sinceSeq(最旧事件比它新):中间可能有事件没取到;
|
|
10342
|
+
* - sinceSeq 比窗口里最新事件还新:客户端的视图和主机对不上(例如历史被改写);
|
|
10343
|
+
* - 增量本身超过单页 900 KB 预算。
|
|
10344
|
+
*/
|
|
10345
|
+
function incrementalTail(events, projected, touched, sinceSeq) {
|
|
10346
|
+
if (events.length === 0) return void 0;
|
|
10347
|
+
let oldest = Infinity;
|
|
10348
|
+
let newest = -Infinity;
|
|
10349
|
+
for (const { event } of events) {
|
|
10350
|
+
if (event.seq < oldest) oldest = event.seq;
|
|
10351
|
+
if (event.seq > newest) newest = event.seq;
|
|
10352
|
+
}
|
|
10353
|
+
if (oldest > sinceSeq || sinceSeq > newest) return void 0;
|
|
10354
|
+
const limited = limitSessionPageMessages(projected.filter((message) => (touched.get(message.seq) ?? message.seq) > sinceSeq));
|
|
10355
|
+
return limited.dropped > 0 ? void 0 : limited.messages;
|
|
10356
|
+
}
|
|
9772
10357
|
function clampTail(n) {
|
|
9773
10358
|
if (!Number.isFinite(n)) return 100;
|
|
9774
10359
|
return Math.max(10, Math.min(500, Math.floor(n)));
|
|
@@ -10504,6 +11089,115 @@ var DshCommandApi = class {
|
|
|
10504
11089
|
return response;
|
|
10505
11090
|
}
|
|
10506
11091
|
};
|
|
11092
|
+
/** 补全是交互式请求,慢于此值的结果对用户已无意义。 */
|
|
11093
|
+
const REFERENCE_TIMEOUT_MS = 5e3;
|
|
11094
|
+
const SESSION_MENTION = /^@\[[^\n]*\]\(dsh-session:[A-Za-z0-9_-]+\)$/;
|
|
11095
|
+
var DshReferenceApi = class {
|
|
11096
|
+
ctx;
|
|
11097
|
+
peer;
|
|
11098
|
+
constructor(ctx) {
|
|
11099
|
+
this.ctx = ctx;
|
|
11100
|
+
}
|
|
11101
|
+
get gatewayReady() {
|
|
11102
|
+
return typeof this.ctx.get("typertGateway")?.invoke === "function";
|
|
11103
|
+
}
|
|
11104
|
+
get filesAvailable() {
|
|
11105
|
+
return this.gatewayReady && this.ctx.get("fileReferences") !== void 0;
|
|
11106
|
+
}
|
|
11107
|
+
get sessionsAvailable() {
|
|
11108
|
+
return this.gatewayReady && this.ctx.get("sessionReferenceResolver") !== void 0;
|
|
11109
|
+
}
|
|
11110
|
+
get available() {
|
|
11111
|
+
return this.filesAvailable || this.sessionsAvailable;
|
|
11112
|
+
}
|
|
11113
|
+
owner() {
|
|
11114
|
+
const connection = this.ctx.get("connection");
|
|
11115
|
+
return this.peer ??= connection?.createPeer?.({
|
|
11116
|
+
principal: { kind: "owner" },
|
|
11117
|
+
interactions: "none"
|
|
11118
|
+
}) ?? connection?.operator;
|
|
11119
|
+
}
|
|
11120
|
+
async invoke(namespace, method, args, signal) {
|
|
11121
|
+
return unwrapRemote(await this.ctx.get("typertGateway").invoke({
|
|
11122
|
+
namespace,
|
|
11123
|
+
method,
|
|
11124
|
+
args,
|
|
11125
|
+
peer: this.owner(),
|
|
11126
|
+
signal
|
|
11127
|
+
}));
|
|
11128
|
+
}
|
|
11129
|
+
/**
|
|
11130
|
+
* 并行读两类候选。单类失败(超时、旧主机没有对应 Remote)只让该类为空,
|
|
11131
|
+
* 不拖垮另一类;两类都不可用由调用方在能力位上拦下。
|
|
11132
|
+
*/
|
|
11133
|
+
async list(sessionId, query, signal) {
|
|
11134
|
+
const bounded = AbortSignal.any([signal ?? new AbortController().signal, AbortSignal.timeout(REFERENCE_TIMEOUT_MS)]);
|
|
11135
|
+
const filesAvailable = this.filesAvailable;
|
|
11136
|
+
const sessionsAvailable = this.sessionsAvailable;
|
|
11137
|
+
const [files, sessions] = await Promise.all([filesAvailable ? this.invoke("fileReferences", "list", {
|
|
11138
|
+
agentId: sessionId,
|
|
11139
|
+
query
|
|
11140
|
+
}, bounded).then(fileItems, () => []) : Promise.resolve([]), sessionsAvailable ? this.invoke("sessionReferenceResolver", "candidates", {
|
|
11141
|
+
agentId: sessionId,
|
|
11142
|
+
query
|
|
11143
|
+
}, bounded).then(sessionItems, () => []) : Promise.resolve([])]);
|
|
11144
|
+
return {
|
|
11145
|
+
files: {
|
|
11146
|
+
available: filesAvailable,
|
|
11147
|
+
items: files
|
|
11148
|
+
},
|
|
11149
|
+
sessions: {
|
|
11150
|
+
available: sessionsAvailable,
|
|
11151
|
+
items: sessions
|
|
11152
|
+
}
|
|
11153
|
+
};
|
|
11154
|
+
}
|
|
11155
|
+
};
|
|
11156
|
+
/**
|
|
11157
|
+
* 网关对不同 Remote 的返回形态不一:有的直接给值(如 `commands/list`),有的给
|
|
11158
|
+
* `RemoteResult {ok, value | error}`(如 `sessionFeedback/record`)。两种都接受,失败抛出。
|
|
11159
|
+
*/
|
|
11160
|
+
function unwrapRemote(result) {
|
|
11161
|
+
if (result && typeof result === "object" && !Array.isArray(result) && typeof result.ok === "boolean") {
|
|
11162
|
+
if (result.ok) return result.value;
|
|
11163
|
+
throw Object.assign(new Error(result.error?.message ?? "reference lookup failed"), { code: result.error?.code });
|
|
11164
|
+
}
|
|
11165
|
+
return result;
|
|
11166
|
+
}
|
|
11167
|
+
/** 只保留形状合法的行;主机排序原样保留。 */
|
|
11168
|
+
function fileItems(rows) {
|
|
11169
|
+
if (!Array.isArray(rows)) return [];
|
|
11170
|
+
const items = [];
|
|
11171
|
+
for (const row of rows) {
|
|
11172
|
+
if (items.length >= 30) break;
|
|
11173
|
+
const path = row?.path;
|
|
11174
|
+
const kind = row?.kind;
|
|
11175
|
+
if (typeof path !== "string" || !path || path.length > 4096 || kind !== "file" && kind !== "directory") continue;
|
|
11176
|
+
items.push({
|
|
11177
|
+
path,
|
|
11178
|
+
kind
|
|
11179
|
+
});
|
|
11180
|
+
}
|
|
11181
|
+
return items;
|
|
11182
|
+
}
|
|
11183
|
+
/** 标签优先用主机给的显示标题(子代理的创建标签),与规范 mention 里的文字一致。 */
|
|
11184
|
+
function sessionItems(rows) {
|
|
11185
|
+
if (!Array.isArray(rows)) return [];
|
|
11186
|
+
const items = [];
|
|
11187
|
+
for (const row of rows) {
|
|
11188
|
+
if (items.length >= 10) break;
|
|
11189
|
+
const r = row;
|
|
11190
|
+
const label = typeof r?.displayTitle === "string" && r.displayTitle ? r.displayTitle : r?.label;
|
|
11191
|
+
if (typeof r?.sessionId !== "string" || !r.sessionId || typeof label !== "string" || !label || typeof r.mention !== "string" || r.mention.length > 4096 || !SESSION_MENTION.test(r.mention)) continue;
|
|
11192
|
+
items.push({
|
|
11193
|
+
sessionId: r.sessionId,
|
|
11194
|
+
label,
|
|
11195
|
+
mention: r.mention,
|
|
11196
|
+
sameWorkspace: r.sameWorkspace === true
|
|
11197
|
+
});
|
|
11198
|
+
}
|
|
11199
|
+
return items;
|
|
11200
|
+
}
|
|
10507
11201
|
// src/dsh-api-proxy.ts
|
|
10508
11202
|
/**
|
|
10509
11203
|
* Adapter for the DSH rc.2 controller API.
|
|
@@ -10545,6 +11239,12 @@ var DshApiProxy = class {
|
|
|
10545
11239
|
export: (request, signal) => this.commandApi.export(request.payload.sessionId, signal, request.payload.format)
|
|
10546
11240
|
};
|
|
10547
11241
|
}
|
|
11242
|
+
referenceApi;
|
|
11243
|
+
/** 引用服务可能晚于桥接层挂载:每次读取都按当前服务判定,不缓存缺席。 */
|
|
11244
|
+
get references() {
|
|
11245
|
+
if (!this.referenceApi.available) return void 0;
|
|
11246
|
+
return { list: (request, signal) => this.call(() => this.referenceApi.list(request.payload.sessionId, request.payload.query, signal)) };
|
|
11247
|
+
}
|
|
10548
11248
|
session;
|
|
10549
11249
|
workspaceController;
|
|
10550
11250
|
scheduleController;
|
|
@@ -10555,6 +11255,7 @@ var DshApiProxy = class {
|
|
|
10555
11255
|
constructor(ctx, options = {}) {
|
|
10556
11256
|
this.ctx = ctx;
|
|
10557
11257
|
this.commandApi = new DshCommandApi(ctx);
|
|
11258
|
+
this.referenceApi = new DshReferenceApi(ctx);
|
|
10558
11259
|
const session = ctx.get("sessionController");
|
|
10559
11260
|
if (session === void 0) throw new Error("DSH sessionController is unavailable");
|
|
10560
11261
|
this.session = session;
|
|
@@ -12091,6 +12792,25 @@ function requestClientIdentity(req) {
|
|
|
12091
12792
|
}
|
|
12092
12793
|
return normalizedAddress(req.socket.remoteAddress) ?? "unknown";
|
|
12093
12794
|
}
|
|
12795
|
+
// src/welcome-routes.ts
|
|
12796
|
+
/**
|
|
12797
|
+
* welcome.routes(能力 routeHints)的纯计算:局域网监听在线且有指纹时给 RFC1918 地址
|
|
12798
|
+
* (顺序沿用 localLANIPv4Addresses 的物理网卡优先),Funnel 在线时给公网地址;两者都没有
|
|
12799
|
+
* 时返回 undefined,welcome 不带该字段。
|
|
12800
|
+
*/
|
|
12801
|
+
function welcomeRoutes(local, remote, addresses) {
|
|
12802
|
+
const lanAddresses = addresses.filter(isPrivateIPv4);
|
|
12803
|
+
const lan = local.phase === "online" && local.tlsFingerprint && lanAddresses.length > 0 ? {
|
|
12804
|
+
urls: localEndpointURLs(lanAddresses, local.port),
|
|
12805
|
+
tlsFingerprint: local.tlsFingerprint
|
|
12806
|
+
} : void 0;
|
|
12807
|
+
const remoteURL = remote.phase === "online" ? remote.publicURL : void 0;
|
|
12808
|
+
if (lan === void 0 && remoteURL === void 0) return void 0;
|
|
12809
|
+
return {
|
|
12810
|
+
...lan ? { lan } : {},
|
|
12811
|
+
...remoteURL ? { remote: { url: remoteURL } } : {}
|
|
12812
|
+
};
|
|
12813
|
+
}
|
|
12094
12814
|
// src/lan-tls.ts
|
|
12095
12815
|
const LAN_TLS_COMMON_NAME = "dsh-deeppilot";
|
|
12096
12816
|
const LAN_TLS_KEY_FILE = "key.pem";
|
|
@@ -12733,6 +13453,7 @@ function apply(ctx, options) {
|
|
|
12733
13453
|
try {
|
|
12734
13454
|
const connection = new BridgeConnection(ws, {
|
|
12735
13455
|
bridge,
|
|
13456
|
+
routes: phoneRoutes,
|
|
12736
13457
|
devices,
|
|
12737
13458
|
serverVersion: SERVER_VERSION,
|
|
12738
13459
|
audience,
|
|
@@ -12821,6 +13542,12 @@ function apply(ctx, options) {
|
|
|
12821
13542
|
dataDir,
|
|
12822
13543
|
log
|
|
12823
13544
|
});
|
|
13545
|
+
/**
|
|
13546
|
+
* welcome.routes(能力 routeHints):手机据此在同一 Wi-Fi 下先走局域网、在外走 Funnel。
|
|
13547
|
+
* 局域网只给 RFC1918 地址(不含 tailnet 的 100.64/10:手机未入 tailnet 时只会白等超时),
|
|
13548
|
+
* 物理网卡优先;Funnel 只在 online 时给。每次 welcome 现算:地址与 Funnel 状态都会变。
|
|
13549
|
+
*/
|
|
13550
|
+
const phoneRoutes = () => welcomeRoutes(localTransport.status(), remoteTransport.status(), localLANIPv4Addresses());
|
|
12824
13551
|
const localStatus = (addresses) => {
|
|
12825
13552
|
const state = localTransport.status();
|
|
12826
13553
|
return {
|