dsh-deeppilot 0.9.6 → 0.9.8
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/COMPATIBILITY.md +119 -18
- package/README.md +13 -11
- package/README.zh-CN.md +8 -7
- package/docs/DSH_RELEASE_MEMORY.md +344 -8
- package/docs/NPM_PUBLISHING.md +26 -0
- package/lib/index.d.ts +87 -3
- package/lib/index.js +443 -0
- package/lib/index.js.map +1 -1
- package/package.json +12 -12
package/lib/index.js
CHANGED
|
@@ -98,6 +98,24 @@ const DOMAIN_TABLES = {
|
|
|
98
98
|
"frequency_too_high": "E_PROTOCOL",
|
|
99
99
|
"schedule_ended": "E_PROTOCOL",
|
|
100
100
|
"subagent_session": "E_PROTOCOL"
|
|
101
|
+
},
|
|
102
|
+
command: {
|
|
103
|
+
"command/not-found": "E_NOT_FOUND",
|
|
104
|
+
"command/definition-changed": "E_NOT_FOUND",
|
|
105
|
+
"command/attachments-unsupported": "E_PROTOCOL",
|
|
106
|
+
"command/export-unavailable": "E_INTERNAL",
|
|
107
|
+
"command/export-cancelled": "E_INTERNAL",
|
|
108
|
+
"command/export-unsupported": "E_UNSUPPORTED",
|
|
109
|
+
"command/invalid-response": "E_INTERNAL",
|
|
110
|
+
"command/permissions-unsupported": "E_UNSUPPORTED",
|
|
111
|
+
"gateway/bad-request": "E_PROTOCOL",
|
|
112
|
+
"session/not-found": "E_NOT_FOUND",
|
|
113
|
+
"session/agent-busy": "E_BUSY",
|
|
114
|
+
"session/writer-held": "E_BUSY",
|
|
115
|
+
"session/conflict": "E_BUSY",
|
|
116
|
+
"session-not-found": "E_NOT_FOUND",
|
|
117
|
+
"agent-busy": "E_BUSY",
|
|
118
|
+
"session-conflict": "E_BUSY"
|
|
101
119
|
}
|
|
102
120
|
};
|
|
103
121
|
/**
|
|
@@ -127,6 +145,24 @@ function wireErrorOf(domain, error) {
|
|
|
127
145
|
};
|
|
128
146
|
}
|
|
129
147
|
/**
|
|
148
|
+
* 把任意抛出物收敛成 `HostFailure`,供 `catch` 分支复用同一张域表。
|
|
149
|
+
*
|
|
150
|
+
* 指令链路里有两种失败来源:`DshApiProxy.call` 把插件的失败码放在
|
|
151
|
+
* `{result:{ok:false,error}}` 里正常返回,而导出的归档流是直接抛出的
|
|
152
|
+
* (`AbortError`、主机侧 `Error` 都算)。把「取形状」和「查表」分开,是为了让
|
|
153
|
+
* 两条路都只经一次翻译;没有 code 的抛出物用调用方给的 fallback,避免把
|
|
154
|
+
* `undefined` 当码送进 lookupCode 而落进无法解释的兜底。
|
|
155
|
+
*/
|
|
156
|
+
function hostFailureOf(error, fallbackCode) {
|
|
157
|
+
const value = error;
|
|
158
|
+
const code = typeof value?.code === "string" && value.code.length > 0 ? value.code : fallbackCode;
|
|
159
|
+
const message = typeof value?.message === "string" && value.message.length > 0 ? value.message : void 0;
|
|
160
|
+
return message === void 0 ? { code } : {
|
|
161
|
+
code,
|
|
162
|
+
message
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
130
166
|
* 组装给手机看的那句话。
|
|
131
167
|
*
|
|
132
168
|
* DSH 把 prompt 准入的**任意**内部失败包成 `session/agent-busy`,真实原因只留在
|
|
@@ -213,6 +249,9 @@ function isEnvelope(value) {
|
|
|
213
249
|
*/
|
|
214
250
|
const PUSH_SCOPE_BY_TYPE = {
|
|
215
251
|
"s2c.session.event": "sessions.read",
|
|
252
|
+
"s2c.skills.changed": "sessions.read",
|
|
253
|
+
"s2c.commands.changed": "sessions.read",
|
|
254
|
+
"s2c.models.changed": "sessions.read",
|
|
216
255
|
"s2c.sessions.delta": "sessions.read",
|
|
217
256
|
"s2c.session.tail": "sessions.read",
|
|
218
257
|
"s2c.history.page": "sessions.read",
|
|
@@ -2067,6 +2106,26 @@ const sessionRows = [
|
|
|
2067
2106
|
ctx.send("s2c.ack", image);
|
|
2068
2107
|
}
|
|
2069
2108
|
},
|
|
2109
|
+
{
|
|
2110
|
+
type: "c2s.session.skills",
|
|
2111
|
+
stage: "authenticated",
|
|
2112
|
+
scopes: READ,
|
|
2113
|
+
capability: "skills",
|
|
2114
|
+
doc: "PROTOCOL.md c2s.session.skills",
|
|
2115
|
+
validate: (payload) => {
|
|
2116
|
+
const checked = payloadObject(payload);
|
|
2117
|
+
if (!checked.ok) return checked;
|
|
2118
|
+
return requireSessionId(checked.value) ?? checked;
|
|
2119
|
+
},
|
|
2120
|
+
handle: async (ctx, payload) => {
|
|
2121
|
+
const result = await ctx.bridge.sessionSkills(payload.sessionId);
|
|
2122
|
+
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
2123
|
+
ctx.send("s2c.session.skills", {
|
|
2124
|
+
sessionId: payload.sessionId,
|
|
2125
|
+
...result.value
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
},
|
|
2070
2129
|
{
|
|
2071
2130
|
type: "c2s.session.models",
|
|
2072
2131
|
stage: "authenticated",
|
|
@@ -2219,6 +2278,121 @@ const sessionRows = [
|
|
|
2219
2278
|
}
|
|
2220
2279
|
}
|
|
2221
2280
|
];
|
|
2281
|
+
//#endregion
|
|
2282
|
+
//#region src/wire-command.ts
|
|
2283
|
+
function sessionPayload(value) {
|
|
2284
|
+
const result = payloadObject(value);
|
|
2285
|
+
return result.ok && !isText(result.value.sessionId) ? reject$1("E_PROTOCOL", "会话标识无效") : result;
|
|
2286
|
+
}
|
|
2287
|
+
const categories = [
|
|
2288
|
+
"task-result",
|
|
2289
|
+
"instruction-following",
|
|
2290
|
+
"product-interaction",
|
|
2291
|
+
"service-stability",
|
|
2292
|
+
"resource-cost",
|
|
2293
|
+
"security-privacy-permission",
|
|
2294
|
+
"other"
|
|
2295
|
+
];
|
|
2296
|
+
const commandRows = [...[
|
|
2297
|
+
"commands",
|
|
2298
|
+
"permissions",
|
|
2299
|
+
"feedback",
|
|
2300
|
+
"command"
|
|
2301
|
+
].map((name) => ({
|
|
2302
|
+
type: `c2s.session.${name}`,
|
|
2303
|
+
stage: "authenticated",
|
|
2304
|
+
capability: "commands",
|
|
2305
|
+
scopes: name === "commands" || name === "permissions" ? ["sessions.read"] : ["prompt.send"],
|
|
2306
|
+
doc: `PROTOCOL.md c2s.session.${name}`,
|
|
2307
|
+
validate: (value) => {
|
|
2308
|
+
const result = sessionPayload(value);
|
|
2309
|
+
if (!result.ok) return result;
|
|
2310
|
+
const p = result.value;
|
|
2311
|
+
if (name === "command") {
|
|
2312
|
+
if (typeof p.line !== "string" || p.line.length > 65536 || !/^\/[a-z][a-z0-9_-]*(?:\s|$)/.test(p.line)) return reject$1("E_PROTOCOL", "指令格式无效");
|
|
2313
|
+
if (p.expectedDefinitionId !== void 0 && (typeof p.expectedDefinitionId !== "string" || !p.expectedDefinitionId || p.expectedDefinitionId.length > 512)) return reject$1("E_PROTOCOL", "指令定义标识无效");
|
|
2314
|
+
const attachments = p.attachments ?? [];
|
|
2315
|
+
if (!Array.isArray(attachments) || attachments.length > 4) return reject$1("E_PROTOCOL", "附件数量无效");
|
|
2316
|
+
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" && ![
|
|
2317
|
+
"image/png",
|
|
2318
|
+
"image/jpeg",
|
|
2319
|
+
"image/webp",
|
|
2320
|
+
"image/gif"
|
|
2321
|
+
].includes(item.mediaType)) return reject$1("E_PROTOCOL", "附件格式无效");
|
|
2322
|
+
return {
|
|
2323
|
+
ok: true,
|
|
2324
|
+
value: {
|
|
2325
|
+
...p,
|
|
2326
|
+
attachments
|
|
2327
|
+
}
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
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", "反馈内容无效");
|
|
2331
|
+
return result;
|
|
2332
|
+
},
|
|
2333
|
+
handle: async (ctx, payload) => {
|
|
2334
|
+
const operation = name === "commands" ? "list" : name === "command" ? "execute" : name;
|
|
2335
|
+
const result = await ctx.bridge.commandOperation(operation, payload);
|
|
2336
|
+
if (!result.ok) return ctx.fail(result.code, result.message);
|
|
2337
|
+
const type = name === "commands" ? "s2c.session.commands" : name === "permissions" ? "s2c.session.permissions" : name === "command" ? "s2c.session.commandResult" : "s2c.ack";
|
|
2338
|
+
ctx.send(type, {
|
|
2339
|
+
sessionId: payload.sessionId,
|
|
2340
|
+
...result.value
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
})), {
|
|
2344
|
+
type: "c2s.session.export",
|
|
2345
|
+
stage: "authenticated",
|
|
2346
|
+
scopes: ["sessions.read"],
|
|
2347
|
+
capability: "commands",
|
|
2348
|
+
doc: "PROTOCOL.md c2s.session.export",
|
|
2349
|
+
validate: sessionPayload,
|
|
2350
|
+
handle: async (ctx, payload) => {
|
|
2351
|
+
const signal = AbortSignal.any([ctx.signal ?? new AbortController().signal, AbortSignal.timeout(6e5)]);
|
|
2352
|
+
let reader;
|
|
2353
|
+
const cancel = () => {
|
|
2354
|
+
reader?.cancel().catch(() => {});
|
|
2355
|
+
};
|
|
2356
|
+
try {
|
|
2357
|
+
reader = (await ctx.bridge.exportSession(payload.sessionId, signal)).body.getReader();
|
|
2358
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
2359
|
+
let index = 0, bytes = 0;
|
|
2360
|
+
while (true) {
|
|
2361
|
+
signal.throwIfAborted();
|
|
2362
|
+
const next = await reader.read();
|
|
2363
|
+
if (next.done) break;
|
|
2364
|
+
for (let offset = 0; offset < next.value.length; offset += 49152) {
|
|
2365
|
+
const chunk = next.value.subarray(offset, offset + 49152);
|
|
2366
|
+
bytes += chunk.length;
|
|
2367
|
+
await ctx.waitForWritable?.();
|
|
2368
|
+
ctx.send("s2c.session.export.chunk", {
|
|
2369
|
+
index: index++,
|
|
2370
|
+
data: Buffer.from(chunk).toString("base64")
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
signal.throwIfAborted();
|
|
2375
|
+
ctx.send("s2c.ack", {
|
|
2376
|
+
sessionId: payload.sessionId,
|
|
2377
|
+
fileName: "dsh-session-log.zip",
|
|
2378
|
+
chunks: index,
|
|
2379
|
+
bytes
|
|
2380
|
+
});
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
const failure = hostFailureOf(error, "command/export-cancelled");
|
|
2383
|
+
const message = failure.code === "command/export-cancelled" ? "会话日志导出失败,请重试" : failure.message;
|
|
2384
|
+
const mapped = wireErrorOf("command", {
|
|
2385
|
+
...failure,
|
|
2386
|
+
message
|
|
2387
|
+
});
|
|
2388
|
+
ctx.fail(mapped.code, mapped.message);
|
|
2389
|
+
} finally {
|
|
2390
|
+
signal.removeEventListener("abort", cancel);
|
|
2391
|
+
await reader?.cancel().catch(() => {});
|
|
2392
|
+
reader?.releaseLock();
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}];
|
|
2222
2396
|
/** 第一个缺失的 scope;消息与迁移前一致(`scope <name> required`)。 */
|
|
2223
2397
|
function scopeRejection(row, scopes) {
|
|
2224
2398
|
for (const scope of row.scopes) if (!scopes.has(scope)) return {
|
|
@@ -2297,6 +2471,7 @@ function accept(value) {
|
|
|
2297
2471
|
/** 全部入站行。装配是静态的:新增特性 module 必须在这里显式出现。 */
|
|
2298
2472
|
const WIRE_FRAME_ROWS = [
|
|
2299
2473
|
...sessionRows,
|
|
2474
|
+
...commandRows,
|
|
2300
2475
|
...scheduleRows,
|
|
2301
2476
|
...interactionRows,
|
|
2302
2477
|
...pushRows,
|
|
@@ -2545,6 +2720,8 @@ var ConnectionGate = class {
|
|
|
2545
2720
|
const host = this.host;
|
|
2546
2721
|
return {
|
|
2547
2722
|
frame: env,
|
|
2723
|
+
signal: host.signal,
|
|
2724
|
+
waitForWritable: host.waitForWritable?.bind(host),
|
|
2548
2725
|
deviceId: this.deviceId,
|
|
2549
2726
|
widgetClient: this.widgetClient,
|
|
2550
2727
|
bridge: host.bridge,
|
|
@@ -2604,6 +2781,7 @@ var BridgeConnection = class {
|
|
|
2604
2781
|
liveActivityRegistrationGeneration = 0;
|
|
2605
2782
|
pendingLiveActivityId;
|
|
2606
2783
|
lastActivity = Date.now();
|
|
2784
|
+
streamAbort = new AbortController();
|
|
2607
2785
|
/** True when no inbound frame arrived within maxIdleMs. */
|
|
2608
2786
|
isStale(now, maxIdleMs) {
|
|
2609
2787
|
return now - this.lastActivity > maxIdleMs;
|
|
@@ -2624,6 +2802,14 @@ var BridgeConnection = class {
|
|
|
2624
2802
|
this.deps = deps;
|
|
2625
2803
|
const connection = this;
|
|
2626
2804
|
const host = {
|
|
2805
|
+
signal: this.streamAbort.signal,
|
|
2806
|
+
waitForWritable: async () => {
|
|
2807
|
+
while (this.ws.bufferedAmount > 262144) {
|
|
2808
|
+
this.streamAbort.signal.throwIfAborted();
|
|
2809
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2810
|
+
}
|
|
2811
|
+
this.streamAbort.signal.throwIfAborted();
|
|
2812
|
+
},
|
|
2627
2813
|
get bridge() {
|
|
2628
2814
|
return connection.deps.bridge;
|
|
2629
2815
|
},
|
|
@@ -2769,6 +2955,7 @@ var BridgeConnection = class {
|
|
|
2769
2955
|
onClose() {
|
|
2770
2956
|
if (this.closed) return;
|
|
2771
2957
|
this.closed = true;
|
|
2958
|
+
this.streamAbort.abort();
|
|
2772
2959
|
this.gate.onClose();
|
|
2773
2960
|
for (const id of this.openSessions) this.deps.bridge.markSinkClosed(this, id);
|
|
2774
2961
|
this.openSessions.clear();
|
|
@@ -5225,6 +5412,8 @@ function unwrapStreamItem(item) {
|
|
|
5225
5412
|
/** 每位一行:Host 上必须存在的依赖。缺失即该位为 false,相关帧回 E_UNSUPPORTED。 */
|
|
5226
5413
|
const HOST_CAPABILITY_PROBES = {
|
|
5227
5414
|
models: (proxy) => typeof proxy.sessions.models === "function",
|
|
5415
|
+
skills: (proxy) => typeof proxy.sessions.skills === "function",
|
|
5416
|
+
commands: (proxy) => typeof proxy.commands?.list === "function" && typeof proxy.commands?.execute === "function",
|
|
5228
5417
|
sessionManagement: (proxy) => typeof proxy.sessions.rename === "function" && typeof proxy.workspace?.archiveSession === "function",
|
|
5229
5418
|
sessionFork: (proxy) => typeof proxy.sessions.fork === "function",
|
|
5230
5419
|
sessionRestore: (proxy) => typeof proxy.workspace?.unarchiveSession === "function",
|
|
@@ -5245,6 +5434,8 @@ function capabilityBits(proxy, pushOutlet) {
|
|
|
5245
5434
|
promptDelivery: true,
|
|
5246
5435
|
notifyAllCategories: true,
|
|
5247
5436
|
models: HOST_CAPABILITY_PROBES.models(proxy),
|
|
5437
|
+
skills: HOST_CAPABILITY_PROBES.skills(proxy),
|
|
5438
|
+
commands: HOST_CAPABILITY_PROBES.commands(proxy),
|
|
5248
5439
|
sessionManagement: HOST_CAPABILITY_PROBES.sessionManagement(proxy),
|
|
5249
5440
|
sessionFork: HOST_CAPABILITY_PROBES.sessionFork(proxy),
|
|
5250
5441
|
sessionRestore: HOST_CAPABILITY_PROBES.sessionRestore(proxy),
|
|
@@ -5550,12 +5741,19 @@ var HostBridge = class {
|
|
|
5550
5741
|
}
|
|
5551
5742
|
onMuxFrame(frame) {
|
|
5552
5743
|
switch (frame.type) {
|
|
5744
|
+
case "skills/changed":
|
|
5745
|
+
this.record("s2c.skills.changed", {});
|
|
5746
|
+
break;
|
|
5747
|
+
case "commands/changed":
|
|
5748
|
+
this.record("s2c.commands.changed", {});
|
|
5749
|
+
break;
|
|
5553
5750
|
case "session/event": {
|
|
5554
5751
|
const event = frame.event;
|
|
5555
5752
|
const sessionId = String(frame.sessionId ?? "");
|
|
5556
5753
|
if (!event || !sessionId) break;
|
|
5557
5754
|
if (frame.isSubagent === true) this.subagentSessionIds.add(sessionId);
|
|
5558
5755
|
if (this.subagentSessionIds.has(sessionId)) break;
|
|
5756
|
+
if (event.type === "agent-preset/selected") this.record("s2c.skills.changed", { sessionId });
|
|
5559
5757
|
this.noteActivity(sessionId, event);
|
|
5560
5758
|
this.captureAssistantText(sessionId, event);
|
|
5561
5759
|
const projection = projectEvent(sessionId, event);
|
|
@@ -5572,6 +5770,19 @@ var HostBridge = class {
|
|
|
5572
5770
|
case "session/projection": {
|
|
5573
5771
|
const p = frame;
|
|
5574
5772
|
if (!p.sessionId) break;
|
|
5773
|
+
if (p.key === "workingDirectory" || p.key === "agentPreset") this.record("s2c.skills.changed", { sessionId: p.sessionId });
|
|
5774
|
+
if (p.key === "modelSelection") {
|
|
5775
|
+
const next = p.value?.next;
|
|
5776
|
+
const current = next && typeof next.provider === "string" && typeof next.model === "string" ? {
|
|
5777
|
+
provider: next.provider,
|
|
5778
|
+
model: next.model,
|
|
5779
|
+
...typeof next.reasoningEffort === "string" ? { reasoningEffort: next.reasoningEffort } : {}
|
|
5780
|
+
} : void 0;
|
|
5781
|
+
this.record("s2c.models.changed", {
|
|
5782
|
+
sessionId: p.sessionId,
|
|
5783
|
+
...current ? { current } : {}
|
|
5784
|
+
});
|
|
5785
|
+
}
|
|
5575
5786
|
this.applyProjection(p.sessionId, String(p.key ?? ""), p.value);
|
|
5576
5787
|
break;
|
|
5577
5788
|
}
|
|
@@ -5948,6 +6159,75 @@ var HostBridge = class {
|
|
|
5948
6159
|
return null;
|
|
5949
6160
|
}
|
|
5950
6161
|
}
|
|
6162
|
+
async commandOperation(operation, payload) {
|
|
6163
|
+
const api = this.apiProxy.commands;
|
|
6164
|
+
if (!api) return {
|
|
6165
|
+
ok: false,
|
|
6166
|
+
code: "E_UNSUPPORTED",
|
|
6167
|
+
message: "当前主机不支持手机指令"
|
|
6168
|
+
};
|
|
6169
|
+
try {
|
|
6170
|
+
const response = await api[operation].call(api, {
|
|
6171
|
+
rpcId: randomUUID(),
|
|
6172
|
+
payload
|
|
6173
|
+
});
|
|
6174
|
+
if (response.result?.ok) return {
|
|
6175
|
+
ok: true,
|
|
6176
|
+
value: response.result.value ?? {}
|
|
6177
|
+
};
|
|
6178
|
+
return wireErrorOf("command", response.result?.error ?? { code: "command/invalid-response" });
|
|
6179
|
+
} catch (error) {
|
|
6180
|
+
return wireErrorOf("command", hostFailureOf(error, "command/invalid-response"));
|
|
6181
|
+
}
|
|
6182
|
+
}
|
|
6183
|
+
async exportSession(sessionId, signal) {
|
|
6184
|
+
const api = this.apiProxy.commands;
|
|
6185
|
+
if (!api) throw Object.assign(/* @__PURE__ */ new Error("当前主机不支持日志导出"), { code: "command/export-unsupported" });
|
|
6186
|
+
return api.export({
|
|
6187
|
+
rpcId: randomUUID(),
|
|
6188
|
+
payload: { sessionId }
|
|
6189
|
+
}, signal);
|
|
6190
|
+
}
|
|
6191
|
+
/** 由会话目录决定技能可见性;不读取正文,也不把本机路径发给移动端。 */
|
|
6192
|
+
async sessionSkills(sessionId) {
|
|
6193
|
+
const read = this.apiProxy.sessions.skills;
|
|
6194
|
+
if (!read) return {
|
|
6195
|
+
ok: false,
|
|
6196
|
+
code: "E_UNSUPPORTED",
|
|
6197
|
+
message: "当前主机不支持技能目录"
|
|
6198
|
+
};
|
|
6199
|
+
try {
|
|
6200
|
+
const response = await read.call(this.apiProxy.sessions, {
|
|
6201
|
+
rpcId: randomUUID(),
|
|
6202
|
+
payload: { sessionId }
|
|
6203
|
+
});
|
|
6204
|
+
if (!response.result) return {
|
|
6205
|
+
ok: false,
|
|
6206
|
+
code: "E_INTERNAL",
|
|
6207
|
+
message: "技能目录响应无效"
|
|
6208
|
+
};
|
|
6209
|
+
if (!response.result.ok) return wireErrorOf("session", response.result.error);
|
|
6210
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6211
|
+
return {
|
|
6212
|
+
ok: true,
|
|
6213
|
+
value: { skills: response.result.value.skills.map((skill) => {
|
|
6214
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(skill.name) || seen.has(skill.name) || typeof skill.description !== "string" || typeof skill.modelInvocable !== "boolean") throw new Error("技能目录条目无效");
|
|
6215
|
+
seen.add(skill.name);
|
|
6216
|
+
return {
|
|
6217
|
+
name: skill.name,
|
|
6218
|
+
description: skill.description,
|
|
6219
|
+
modelInvocable: skill.modelInvocable
|
|
6220
|
+
};
|
|
6221
|
+
}) }
|
|
6222
|
+
};
|
|
6223
|
+
} catch {
|
|
6224
|
+
return {
|
|
6225
|
+
ok: false,
|
|
6226
|
+
code: "E_INTERNAL",
|
|
6227
|
+
message: "无法读取当前会话的技能目录"
|
|
6228
|
+
};
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
5951
6231
|
async sessionModels(sessionId) {
|
|
5952
6232
|
const models = this.apiProxy.sessions.models;
|
|
5953
6233
|
if (typeof models !== "function") return {
|
|
@@ -7083,6 +7363,145 @@ async function startDshRemoteInteractions(hostCtx, handlers) {
|
|
|
7083
7363
|
}
|
|
7084
7364
|
}
|
|
7085
7365
|
//#endregion
|
|
7366
|
+
//#region src/command-api.ts
|
|
7367
|
+
/**
|
|
7368
|
+
* 给本域自造的失败带上 Host 风格的码。
|
|
7369
|
+
*
|
|
7370
|
+
* 这些失败不是上游返回的,而是插件在这里判定出来的,所以必须自己声明语义:
|
|
7371
|
+
* 没有码的抛出物在 wire-errors 的域表里查不到,一律落进 `E_INTERNAL`,手机端
|
|
7372
|
+
* 便无法区分「指令不存在」「定义已变化」「这个指令不收附件」——它们对用户的
|
|
7373
|
+
* 下一步动作要求完全不同。码的取值见 `src/wire-errors.ts` 的 `command` 域表。
|
|
7374
|
+
*/
|
|
7375
|
+
function fail(code, message) {
|
|
7376
|
+
return Object.assign(new Error(message), { code });
|
|
7377
|
+
}
|
|
7378
|
+
/** 只经官方 Gateway 调用命令,保留其 Agent 解析、作用域覆盖和生命周期记录。 */
|
|
7379
|
+
var DshCommandApi = class {
|
|
7380
|
+
ctx;
|
|
7381
|
+
peer;
|
|
7382
|
+
constructor(ctx) {
|
|
7383
|
+
this.ctx = ctx;
|
|
7384
|
+
}
|
|
7385
|
+
get available() {
|
|
7386
|
+
return this.ctx.get("commands") !== void 0 && typeof this.ctx.get("typertGateway")?.invoke === "function";
|
|
7387
|
+
}
|
|
7388
|
+
connection() {
|
|
7389
|
+
return this.ctx.get("connection");
|
|
7390
|
+
}
|
|
7391
|
+
owner() {
|
|
7392
|
+
const connection = this.connection();
|
|
7393
|
+
return this.peer ??= connection?.createPeer?.({
|
|
7394
|
+
principal: { kind: "owner" },
|
|
7395
|
+
interactions: "none"
|
|
7396
|
+
}) ?? connection?.operator;
|
|
7397
|
+
}
|
|
7398
|
+
invoke(namespace, method, args) {
|
|
7399
|
+
return this.ctx.get("typertGateway").invoke({
|
|
7400
|
+
namespace,
|
|
7401
|
+
method,
|
|
7402
|
+
args,
|
|
7403
|
+
peer: this.owner(),
|
|
7404
|
+
signal: AbortSignal.timeout(6e5)
|
|
7405
|
+
});
|
|
7406
|
+
}
|
|
7407
|
+
async list(sessionId) {
|
|
7408
|
+
const rows = await this.invoke("commands", "list", { agentId: sessionId });
|
|
7409
|
+
const names = /* @__PURE__ */ new Set();
|
|
7410
|
+
return {
|
|
7411
|
+
commands: rows.map((row) => {
|
|
7412
|
+
if (!/^[a-z][a-z0-9_-]{0,127}$/.test(row.name) || names.has(row.name) || typeof row.description !== "string") throw fail("command/invalid-response", "命令目录无效");
|
|
7413
|
+
names.add(row.name);
|
|
7414
|
+
return {
|
|
7415
|
+
name: row.name,
|
|
7416
|
+
description: row.description,
|
|
7417
|
+
...typeof row.definitionId === "string" ? { definitionId: row.definitionId } : {},
|
|
7418
|
+
...row.input && typeof row.input.hint === "string" ? { input: {
|
|
7419
|
+
hint: row.input.hint,
|
|
7420
|
+
attachments: row.input.attachments === true
|
|
7421
|
+
} } : {}
|
|
7422
|
+
};
|
|
7423
|
+
}),
|
|
7424
|
+
permissions: this.ctx.get("permissionPresets") !== void 0,
|
|
7425
|
+
export: typeof this.connection()?.createSharedFetchHandler === "function"
|
|
7426
|
+
};
|
|
7427
|
+
}
|
|
7428
|
+
async execute(sessionId, line, attachments, expectedDefinitionId) {
|
|
7429
|
+
const catalog = await this.list(sessionId);
|
|
7430
|
+
const name = /^\/([a-z][a-z0-9_-]*)(?=\s|$)/.exec(line)?.[1];
|
|
7431
|
+
const command = catalog.commands.find((row) => row.name === name);
|
|
7432
|
+
if (!command) throw fail("command/not-found", "当前会话没有这个指令");
|
|
7433
|
+
if (expectedDefinitionId !== void 0 && command.definitionId !== expectedDefinitionId) throw fail("command/definition-changed", "指令定义已变化,请重新选择");
|
|
7434
|
+
if (attachments.length && command.input?.attachments !== true) throw fail("command/attachments-unsupported", "这个指令不接受附件");
|
|
7435
|
+
const submittedAttachments = [];
|
|
7436
|
+
for (const item of attachments) if (item.type === "image") submittedAttachments.push({
|
|
7437
|
+
type: "image",
|
|
7438
|
+
mediaType: item.mediaType,
|
|
7439
|
+
data: item.data
|
|
7440
|
+
});
|
|
7441
|
+
else {
|
|
7442
|
+
const uploaded = await this.invoke("fileUploads", "upload", {
|
|
7443
|
+
agentId: sessionId,
|
|
7444
|
+
request: {
|
|
7445
|
+
name: item.name + ".txt",
|
|
7446
|
+
data: item.data
|
|
7447
|
+
}
|
|
7448
|
+
});
|
|
7449
|
+
submittedAttachments.push({
|
|
7450
|
+
type: "file",
|
|
7451
|
+
receiptId: uploaded.receiptId
|
|
7452
|
+
});
|
|
7453
|
+
}
|
|
7454
|
+
const result = await this.invoke("commands", "execute", {
|
|
7455
|
+
agentId: sessionId,
|
|
7456
|
+
line,
|
|
7457
|
+
submittedAttachments
|
|
7458
|
+
});
|
|
7459
|
+
if (!result || typeof result.commandId !== "string" || !["success", "error"].includes(result.result?.kind)) throw fail("command/invalid-response", "指令执行响应无效");
|
|
7460
|
+
return {
|
|
7461
|
+
commandId: result.commandId,
|
|
7462
|
+
result: {
|
|
7463
|
+
kind: result.result.kind,
|
|
7464
|
+
...typeof result.result.text === "string" ? { text: result.result.text } : {},
|
|
7465
|
+
...Number.isSafeInteger(result.result.sourceEventSeq) && result.result.sourceEventSeq >= 0 ? { sourceEventSeq: result.result.sourceEventSeq } : {}
|
|
7466
|
+
}
|
|
7467
|
+
};
|
|
7468
|
+
}
|
|
7469
|
+
async permissions(sessionId) {
|
|
7470
|
+
const service = this.ctx.get("permissionPresets");
|
|
7471
|
+
if (!service) throw fail("command/permissions-unsupported", "当前主机不支持权限预设");
|
|
7472
|
+
const catalog = await this.invoke("permissionPresets", "catalog", {});
|
|
7473
|
+
const currentValue = (await this.ctx.get("sessionController").projections({ sessionId }, new AbortController().signal))?.values?.permissions?.currentValue;
|
|
7474
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7475
|
+
const options = catalog.options.map((option) => {
|
|
7476
|
+
if (typeof option.value !== "string" || !option.value.trim() || option.value.length > 512 || /[\r\n]/.test(option.value) || seen.has(option.value) || typeof option.name !== "string") throw fail("command/invalid-response", "权限目录无效");
|
|
7477
|
+
seen.add(option.value);
|
|
7478
|
+
return {
|
|
7479
|
+
value: option.value,
|
|
7480
|
+
name: option.name,
|
|
7481
|
+
...typeof option.description === "string" ? { description: option.description } : {},
|
|
7482
|
+
confirmation: option.value === "auto" || service.resolve(option.value).sandbox === "danger-full-access"
|
|
7483
|
+
};
|
|
7484
|
+
});
|
|
7485
|
+
return {
|
|
7486
|
+
...typeof currentValue === "string" ? { currentValue } : {},
|
|
7487
|
+
options
|
|
7488
|
+
};
|
|
7489
|
+
}
|
|
7490
|
+
async feedback(sessionId, text, category) {
|
|
7491
|
+
const result = await this.invoke("sessionFeedback", "record", { request: {
|
|
7492
|
+
sessionId,
|
|
7493
|
+
...text.trim() ? { text: text.trim() } : {},
|
|
7494
|
+
...category ? { category } : {}
|
|
7495
|
+
} });
|
|
7496
|
+
if (result?.ok !== true) throw fail(typeof result?.error?.code === "string" ? result.error.code : "command/invalid-response", typeof result?.error?.message === "string" ? result.error.message : "反馈未能保存");
|
|
7497
|
+
}
|
|
7498
|
+
async export(sessionId, signal) {
|
|
7499
|
+
const response = await this.connection().createSharedFetchHandler("/api").fetch(new Request("http://localhost/api/session.export?includeDescendants=true&sessionId=" + encodeURIComponent(sessionId), { signal }), this.owner());
|
|
7500
|
+
if (!response.ok || !response.body || !response.headers.get("content-type")?.includes("application/zip")) throw fail("command/export-unavailable", "主机未能生成会话日志归档");
|
|
7501
|
+
return response;
|
|
7502
|
+
}
|
|
7503
|
+
};
|
|
7504
|
+
//#endregion
|
|
7086
7505
|
//#region src/dsh-api-proxy.ts
|
|
7087
7506
|
/**
|
|
7088
7507
|
* Adapter for the DSH rc.2 controller API.
|
|
@@ -7095,6 +7514,17 @@ async function startDshRemoteInteractions(hostCtx, handlers) {
|
|
|
7095
7514
|
/** Direct-controller adapter with the shape HostBridge consumes. */
|
|
7096
7515
|
var DshApiProxy = class {
|
|
7097
7516
|
ctx;
|
|
7517
|
+
commandApi;
|
|
7518
|
+
get commands() {
|
|
7519
|
+
if (!this.commandApi.available) return void 0;
|
|
7520
|
+
return {
|
|
7521
|
+
list: (request) => this.call(() => this.commandApi.list(request.payload.sessionId)),
|
|
7522
|
+
execute: (request) => this.call(() => this.commandApi.execute(request.payload.sessionId, request.payload.line, request.payload.attachments, request.payload.expectedDefinitionId)),
|
|
7523
|
+
permissions: (request) => this.call(() => this.commandApi.permissions(request.payload.sessionId)),
|
|
7524
|
+
feedback: (request) => this.call(() => this.commandApi.feedback(request.payload.sessionId, request.payload.text, request.payload.category)),
|
|
7525
|
+
export: (request, signal) => this.commandApi.export(request.payload.sessionId, signal)
|
|
7526
|
+
};
|
|
7527
|
+
}
|
|
7098
7528
|
session;
|
|
7099
7529
|
workspaceController;
|
|
7100
7530
|
scheduleController;
|
|
@@ -7104,6 +7534,7 @@ var DshApiProxy = class {
|
|
|
7104
7534
|
shouldSurfaceInteraction;
|
|
7105
7535
|
constructor(ctx, options = {}) {
|
|
7106
7536
|
this.ctx = ctx;
|
|
7537
|
+
this.commandApi = new DshCommandApi(ctx);
|
|
7107
7538
|
const session = ctx.get("sessionController");
|
|
7108
7539
|
if (session === void 0) throw new Error("DSH sessionController is unavailable");
|
|
7109
7540
|
this.session = session;
|
|
@@ -7112,6 +7543,12 @@ var DshApiProxy = class {
|
|
|
7112
7543
|
this.directoryPicker = ctx.get("directoryPickerController");
|
|
7113
7544
|
this.shouldSurfaceInteraction = options.shouldSurfaceInteraction ?? (() => true);
|
|
7114
7545
|
this.resolveSchedule();
|
|
7546
|
+
Object.defineProperty(this.sessions, "skills", { get: () => {
|
|
7547
|
+
const catalog = ctx.get("sessionSkillCatalog");
|
|
7548
|
+
if (typeof catalog?.list !== "function") return void 0;
|
|
7549
|
+
const read = async (request) => this.call(() => catalog.list({ sessionId: request.payload.sessionId }, new AbortController().signal));
|
|
7550
|
+
return read;
|
|
7551
|
+
} });
|
|
7115
7552
|
}
|
|
7116
7553
|
/**
|
|
7117
7554
|
* Resolve the optional Schedule service lazily, on every read.
|
|
@@ -7307,6 +7744,10 @@ var DshApiProxy = class {
|
|
|
7307
7744
|
value
|
|
7308
7745
|
});
|
|
7309
7746
|
});
|
|
7747
|
+
const offSkills = this.ctx.on("skills/change", (() => {
|
|
7748
|
+
queue.push({ type: "skills/changed" });
|
|
7749
|
+
}), { global: true });
|
|
7750
|
+
const offCommands = ["commands/change", "permission-presets/catalog-changed"].map((event) => this.ctx.on(event, (() => queue.push({ type: "commands/changed" })), { global: true }));
|
|
7310
7751
|
let disposeInteractions;
|
|
7311
7752
|
try {
|
|
7312
7753
|
disposeInteractions = await startDshRemoteInteractions(this.ctx, {
|
|
@@ -7321,6 +7762,8 @@ var DshApiProxy = class {
|
|
|
7321
7762
|
} finally {
|
|
7322
7763
|
offEvent();
|
|
7323
7764
|
offProjection?.();
|
|
7765
|
+
offSkills();
|
|
7766
|
+
offCommands.forEach((dispose) => dispose());
|
|
7324
7767
|
await disposeInteractions?.();
|
|
7325
7768
|
queue.close();
|
|
7326
7769
|
}
|