dsh-deeppilot 0.9.7 → 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/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
  }