dsh-deeppilot 0.9.16 → 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/docs/DSH_RELEASE_MEMORY.md +60 -0
- package/docs/NPM_PUBLISHING.md +10 -1
- package/lib/index.d.ts +96 -4
- package/lib/index.js +596 -125
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1968,7 +1968,8 @@ function projectEvent(sessionId, event) {
|
|
|
1968
1968
|
state: "running",
|
|
1969
1969
|
summary: summarizeArgs(data?.arguments),
|
|
1970
1970
|
...data?.callId ? { callId: String(data.callId) } : {},
|
|
1971
|
-
...editField(data?.name, data?.arguments)
|
|
1971
|
+
...editField(data?.name, data?.arguments),
|
|
1972
|
+
...targetField(data?.name, data?.arguments)
|
|
1972
1973
|
},
|
|
1973
1974
|
ts: tsOf(event)
|
|
1974
1975
|
}
|
|
@@ -2139,6 +2140,40 @@ function attachmentProjection(data) {
|
|
|
2139
2140
|
const attachments = messageAttachments(data);
|
|
2140
2141
|
return attachments.length > 0 ? { attachments } : {};
|
|
2141
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
|
+
}
|
|
2142
2177
|
/** Extract reasoning ("thinking") text from assistant message payloads. */
|
|
2143
2178
|
function messageThinking(data) {
|
|
2144
2179
|
if (!data || typeof data !== "object") return "";
|
|
@@ -2178,6 +2213,125 @@ function editField(name, raw) {
|
|
|
2178
2213
|
const edit = toolEditOf(name, raw);
|
|
2179
2214
|
return edit ? { edit } : {};
|
|
2180
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
|
+
}
|
|
2181
2335
|
function summarizeArgs(raw) {
|
|
2182
2336
|
if (typeof raw !== "string" || raw.length === 0) return "";
|
|
2183
2337
|
try {
|
|
@@ -2191,15 +2345,22 @@ function summarizeArgs(raw) {
|
|
|
2191
2345
|
}
|
|
2192
2346
|
/** 宿主的文件编辑工具名(DSH `dsh-tool-str-replace-editor`)。 */
|
|
2193
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";
|
|
2194
2352
|
/**
|
|
2195
2353
|
* 把编辑类工具调用的原始参数归一化成 `ToolEditProjection`。
|
|
2196
2354
|
*
|
|
2197
|
-
*
|
|
2198
|
-
*
|
|
2199
|
-
*
|
|
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` 的解析口径一致:新文本缺失视为删除(空串)。
|
|
2200
2360
|
*/
|
|
2201
2361
|
function toolEditOf(name, raw) {
|
|
2202
|
-
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;
|
|
2203
2364
|
let args;
|
|
2204
2365
|
try {
|
|
2205
2366
|
const parsed = JSON.parse(raw);
|
|
@@ -2208,6 +2369,10 @@ function toolEditOf(name, raw) {
|
|
|
2208
2369
|
} catch {
|
|
2209
2370
|
return;
|
|
2210
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
|
+
}
|
|
2211
2376
|
const path = args.path;
|
|
2212
2377
|
if (typeof path !== "string" || path.length === 0) return void 0;
|
|
2213
2378
|
let edit;
|
|
@@ -2240,6 +2405,32 @@ function toolEditOf(name, raw) {
|
|
|
2240
2405
|
break;
|
|
2241
2406
|
default: return;
|
|
2242
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) {
|
|
2243
2434
|
return [edit.oldText, edit.newText].some((text) => text !== void 0 && Buffer.byteLength(text, "utf8") > 16384) ? withoutEditBodies(edit) : edit;
|
|
2244
2435
|
}
|
|
2245
2436
|
/** 去掉改动正文,只留路径、操作与 truncated 标记(超限或整条消息超限时用)。 */
|
|
@@ -2301,7 +2492,8 @@ function projectHistory(events, touched) {
|
|
|
2301
2492
|
name: String(data?.name ?? "tool"),
|
|
2302
2493
|
state: "running",
|
|
2303
2494
|
summary: summarizeArgs(data?.arguments),
|
|
2304
|
-
...editField(data?.name, data?.arguments)
|
|
2495
|
+
...editField(data?.name, data?.arguments),
|
|
2496
|
+
...targetField(data?.name, data?.arguments)
|
|
2305
2497
|
}
|
|
2306
2498
|
};
|
|
2307
2499
|
messages.push(row);
|
|
@@ -3112,128 +3304,153 @@ const categories = [
|
|
|
3112
3304
|
"security-privacy-permission",
|
|
3113
3305
|
"other"
|
|
3114
3306
|
];
|
|
3115
|
-
const commandRows = [
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
if (
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
"image
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
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
|
+
});
|
|
3148
3376
|
}
|
|
3149
|
-
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", "反馈内容无效");
|
|
3150
|
-
return result;
|
|
3151
|
-
},
|
|
3152
|
-
handle: async (ctx, payload) => {
|
|
3153
|
-
const operation = name === "commands" ? "list" : name === "command" ? "execute" : name;
|
|
3154
|
-
const result = await ctx.bridge.commandOperation(operation, payload);
|
|
3155
|
-
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
3156
|
-
const type = name === "commands" ? "s2c.session.commands" : name === "permissions" ? "s2c.session.permissions" : name === "command" ? "s2c.session.commandResult" : "s2c.ack";
|
|
3157
|
-
ctx.send(type, {
|
|
3158
|
-
sessionId: payload.sessionId,
|
|
3159
|
-
...result.value
|
|
3160
|
-
});
|
|
3161
|
-
}
|
|
3162
|
-
})), {
|
|
3163
|
-
type: "c2s.session.export",
|
|
3164
|
-
stage: "authenticated",
|
|
3165
|
-
scopes: ["sessions.read"],
|
|
3166
|
-
capability: "commands",
|
|
3167
|
-
conditionalCapabilities: ["sessionMarkdown"],
|
|
3168
|
-
doc: "PROTOCOL.md c2s.session.export",
|
|
3169
|
-
validate: (value) => {
|
|
3170
|
-
const result = sessionPayload(value);
|
|
3171
|
-
if (!result.ok) return result;
|
|
3172
|
-
if (result.value.format !== void 0 && result.value.format !== "zip" && result.value.format !== "markdown") return reject$1("E_PROTOCOL", "导出格式无效");
|
|
3173
|
-
if (result.value.binary !== void 0 && typeof result.value.binary !== "boolean") return reject$1("E_PROTOCOL", "传输方式无效");
|
|
3174
|
-
return result;
|
|
3175
3377
|
},
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
});
|
|
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(() => {});
|
|
3197
3399
|
};
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
signal.
|
|
3201
|
-
const
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
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
|
+
}
|
|
3211
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();
|
|
3212
3450
|
}
|
|
3213
|
-
signal.throwIfAborted();
|
|
3214
|
-
if (carry !== void 0 && carry.length > 0) await sendChunk(carry);
|
|
3215
|
-
ctx.send("s2c.ack", {
|
|
3216
|
-
sessionId: payload.sessionId,
|
|
3217
|
-
fileName: format === "markdown" ? "dsh-session.md" : "dsh-session-log.zip",
|
|
3218
|
-
chunks: index,
|
|
3219
|
-
bytes,
|
|
3220
|
-
...format === void 0 ? {} : { mediaType: format === "markdown" ? "text/markdown; charset=utf-8" : "application/zip" }
|
|
3221
|
-
});
|
|
3222
|
-
} catch (error) {
|
|
3223
|
-
const failure = hostFailureOf(error, "command/export-cancelled");
|
|
3224
|
-
const message = failure.code === "command/export-cancelled" ? "会话日志导出失败,请重试" : failure.message;
|
|
3225
|
-
const mapped = wireErrorOf("command", {
|
|
3226
|
-
...failure,
|
|
3227
|
-
message
|
|
3228
|
-
});
|
|
3229
|
-
ctx.fail(mapped.code, mapped.message);
|
|
3230
|
-
} finally {
|
|
3231
|
-
signal.removeEventListener("abort", cancel);
|
|
3232
|
-
await reader?.cancel().catch(() => {});
|
|
3233
|
-
reader?.releaseLock();
|
|
3234
3451
|
}
|
|
3235
3452
|
}
|
|
3236
|
-
|
|
3453
|
+
];
|
|
3237
3454
|
// src/plugin-update.ts
|
|
3238
3455
|
const PACKAGE = "dsh-deeppilot";
|
|
3239
3456
|
const PHASES = [
|
|
@@ -3639,9 +3856,8 @@ var PluginUpdater = class {
|
|
|
3639
3856
|
record.restartPhase = "rolled-back";
|
|
3640
3857
|
return this.transition(record, "restart-required", "目标安装失败,旧版已重新安装;请在电脑端重启 DSH 后确认恢复");
|
|
3641
3858
|
}
|
|
3642
|
-
if (restored.
|
|
3859
|
+
if (restored.version === void 0 || restored.version === record.fromVersion) {
|
|
3643
3860
|
if ((await this.listBundles()).find((bundle) => bundle.name === PACKAGE)?.version === record.fromVersion) return this.transition(record, "rolled-back", "目标安装失败,宿主已自动恢复旧版本");
|
|
3644
|
-
return this.transition(record, "failed", "宿主已尝试回滚,但无法核对实际旧版本,请在电脑端检查插件");
|
|
3645
3861
|
}
|
|
3646
3862
|
return this.transition(record, "failed", "目标安装失败,宿主恢复旧版也未完成,请在电脑端检查插件");
|
|
3647
3863
|
}
|
|
@@ -7255,6 +7471,7 @@ const HOST_CAPABILITY_PROBES = {
|
|
|
7255
7471
|
models: (proxy) => typeof proxy.sessions.models === "function",
|
|
7256
7472
|
skills: (proxy) => typeof proxy.sessions.skills === "function",
|
|
7257
7473
|
commands: (proxy) => typeof proxy.commands?.list === "function" && typeof proxy.commands?.execute === "function",
|
|
7474
|
+
references: (proxy) => typeof proxy.references?.list === "function",
|
|
7258
7475
|
sessionManagement: (proxy) => typeof proxy.sessions.rename === "function" && typeof proxy.workspace?.archiveSession === "function",
|
|
7259
7476
|
sessionFork: (proxy) => typeof proxy.sessions.fork === "function",
|
|
7260
7477
|
sessionRestore: (proxy) => typeof proxy.workspace?.unarchiveSession === "function",
|
|
@@ -7287,6 +7504,7 @@ function capabilityBits(proxy, pushOutlet) {
|
|
|
7287
7504
|
models: HOST_CAPABILITY_PROBES.models(proxy),
|
|
7288
7505
|
skills: HOST_CAPABILITY_PROBES.skills(proxy),
|
|
7289
7506
|
commands: HOST_CAPABILITY_PROBES.commands(proxy),
|
|
7507
|
+
references: HOST_CAPABILITY_PROBES.references(proxy),
|
|
7290
7508
|
sessionManagement: HOST_CAPABILITY_PROBES.sessionManagement(proxy),
|
|
7291
7509
|
sessionFork: HOST_CAPABILITY_PROBES.sessionFork(proxy),
|
|
7292
7510
|
sessionRestore: HOST_CAPABILITY_PROBES.sessionRestore(proxy),
|
|
@@ -7554,6 +7772,69 @@ var AssistantStream = class {
|
|
|
7554
7772
|
this.attempts.clear();
|
|
7555
7773
|
}
|
|
7556
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
|
+
}
|
|
7557
7838
|
// src/host-bridge.ts
|
|
7558
7839
|
const MAX_RING_DEFAULT = 2e3;
|
|
7559
7840
|
/** 补发量超过它就改发 s2c.resync(经 Funnel 实测吞吐 40~80 KB/s,约 6~12 秒)。 */
|
|
@@ -7602,6 +7883,8 @@ var HostBridge = class {
|
|
|
7602
7883
|
abort = new AbortController();
|
|
7603
7884
|
started = false;
|
|
7604
7885
|
disposed = false;
|
|
7886
|
+
/** attachmentId → 宿主完整图片引用,供 attachmentData 补全 DSH 0.2.2+ 的读图参数。 */
|
|
7887
|
+
imageRefs = new ImageRefIndex();
|
|
7605
7888
|
promptDeliveries;
|
|
7606
7889
|
scheduleMutations;
|
|
7607
7890
|
forkMutations;
|
|
@@ -7978,6 +8261,7 @@ var HostBridge = class {
|
|
|
7978
8261
|
this.noteActivity(sessionId, event);
|
|
7979
8262
|
if (event.type === "user/message") this.quietCompletion.noteUserMessage(sessionId, userMessageSourceKind(event.data), event.seq);
|
|
7980
8263
|
this.captureAssistantText(sessionId, event);
|
|
8264
|
+
this.imageRefs.remember(sessionId, [event]);
|
|
7981
8265
|
const projection = projectEvent(sessionId, event);
|
|
7982
8266
|
this.captureActivity(sessionId, event, projection?.data.tool);
|
|
7983
8267
|
if (projection) this.record("s2c.session.event", {
|
|
@@ -8001,6 +8285,7 @@ var HostBridge = class {
|
|
|
8001
8285
|
const p = frame;
|
|
8002
8286
|
if (!p.sessionId) break;
|
|
8003
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 });
|
|
8004
8289
|
if (p.key === "modelSelection") {
|
|
8005
8290
|
const next = p.value?.next;
|
|
8006
8291
|
const current = next && typeof next.provider === "string" && typeof next.model === "string" ? {
|
|
@@ -8396,6 +8681,7 @@ var HostBridge = class {
|
|
|
8396
8681
|
if (!response.result || !response.result.ok) return false;
|
|
8397
8682
|
const result = response.result.value;
|
|
8398
8683
|
const events = result.events ?? [];
|
|
8684
|
+
this.imageRefs.remember(sessionId, events.map((row) => row.event));
|
|
8399
8685
|
const touched = /* @__PURE__ */ new Map();
|
|
8400
8686
|
const projected = projectHistory(events, touched);
|
|
8401
8687
|
const page = limitSessionPageMessages(projected);
|
|
@@ -8467,6 +8753,7 @@ var HostBridge = class {
|
|
|
8467
8753
|
});
|
|
8468
8754
|
if (!response.result || !response.result.ok) return null;
|
|
8469
8755
|
const result = response.result.value;
|
|
8756
|
+
this.imageRefs.remember(sessionId, (result.events ?? []).map((row) => row.event));
|
|
8470
8757
|
const page = limitSessionPageMessages(projectHistory(result.events ?? []).filter((message) => message.seq < beforeSeq));
|
|
8471
8758
|
return {
|
|
8472
8759
|
sessionId,
|
|
@@ -8477,16 +8764,27 @@ var HostBridge = class {
|
|
|
8477
8764
|
return null;
|
|
8478
8765
|
}
|
|
8479
8766
|
}
|
|
8480
|
-
/**
|
|
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
|
+
*/
|
|
8481
8777
|
async attachmentData(sessionId, attachmentId) {
|
|
8482
8778
|
const read = this.apiProxy.sessions.attachment;
|
|
8483
8779
|
if (typeof read !== "function") return null;
|
|
8484
8780
|
try {
|
|
8781
|
+
const ref = await this.imageRefs.resolve(sessionId, attachmentId, (id, beforeSeq) => this.readHistoryEvents(id, beforeSeq));
|
|
8485
8782
|
const response = await read.call(this.apiProxy.sessions, {
|
|
8486
8783
|
rpcId: randomUUID(),
|
|
8487
8784
|
payload: {
|
|
8488
8785
|
sessionId,
|
|
8489
|
-
attachmentId
|
|
8786
|
+
attachmentId,
|
|
8787
|
+
...ref ? { attachment: ref } : {}
|
|
8490
8788
|
}
|
|
8491
8789
|
});
|
|
8492
8790
|
if (!response.result || !response.result.ok) return null;
|
|
@@ -8500,6 +8798,27 @@ var HostBridge = class {
|
|
|
8500
8798
|
return null;
|
|
8501
8799
|
}
|
|
8502
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
|
+
}
|
|
8503
8822
|
async commandOperation(operation, payload) {
|
|
8504
8823
|
const api = this.apiProxy.commands;
|
|
8505
8824
|
if (!api) return {
|
|
@@ -8521,6 +8840,42 @@ var HostBridge = class {
|
|
|
8521
8840
|
return wireErrorOf("command", hostFailureOf(error, "command/invalid-response"));
|
|
8522
8841
|
}
|
|
8523
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
|
+
}
|
|
8524
8879
|
async exportSession(sessionId, signal, format) {
|
|
8525
8880
|
const api = this.apiProxy.commands;
|
|
8526
8881
|
if (!api) throw Object.assign(/* @__PURE__ */ new Error("当前主机不支持日志导出"), { code: "command/export-unsupported" });
|
|
@@ -10734,6 +11089,115 @@ var DshCommandApi = class {
|
|
|
10734
11089
|
return response;
|
|
10735
11090
|
}
|
|
10736
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
|
+
}
|
|
10737
11201
|
// src/dsh-api-proxy.ts
|
|
10738
11202
|
/**
|
|
10739
11203
|
* Adapter for the DSH rc.2 controller API.
|
|
@@ -10775,6 +11239,12 @@ var DshApiProxy = class {
|
|
|
10775
11239
|
export: (request, signal) => this.commandApi.export(request.payload.sessionId, signal, request.payload.format)
|
|
10776
11240
|
};
|
|
10777
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
|
+
}
|
|
10778
11248
|
session;
|
|
10779
11249
|
workspaceController;
|
|
10780
11250
|
scheduleController;
|
|
@@ -10785,6 +11255,7 @@ var DshApiProxy = class {
|
|
|
10785
11255
|
constructor(ctx, options = {}) {
|
|
10786
11256
|
this.ctx = ctx;
|
|
10787
11257
|
this.commandApi = new DshCommandApi(ctx);
|
|
11258
|
+
this.referenceApi = new DshReferenceApi(ctx);
|
|
10788
11259
|
const session = ctx.get("sessionController");
|
|
10789
11260
|
if (session === void 0) throw new Error("DSH sessionController is unavailable");
|
|
10790
11261
|
this.session = session;
|