dsh-lark-bot 0.9.2 → 0.10.0

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/dist/cli.js CHANGED
@@ -667,7 +667,7 @@ var init_acp_adapter = __esm({
667
667
  });
668
668
 
669
669
  // src/cli.ts
670
- import { readFileSync as readFileSync4, realpathSync as realpathSync4 } from "fs";
670
+ import { readFileSync as readFileSync5, realpathSync as realpathSync4 } from "fs";
671
671
  import { fileURLToPath as fileURLToPath2 } from "url";
672
672
  import { Command } from "commander";
673
673
 
@@ -1277,6 +1277,7 @@ var SDK_SERVER_PACKAGE = "@deepseek-ai/dsh-sdk-jsonrpc-server";
1277
1277
  var SDK_SERVER_VERSION = DSH_COMPATIBILITY.sdkServer;
1278
1278
  var SDK_BASE_BUNDLE = "@deepseek-ai/dsh-base";
1279
1279
  var DEFAULT_SDK_PROFILE = "dsh-lark-sdk";
1280
+ var DEFAULT_SAFE_SDK_PROFILE = "dsh-lark-safe-sdk";
1280
1281
  function sdkProfileRoot(home, profile, env) {
1281
1282
  return join3(resolveDshHome(home, env), "profiles", profile);
1282
1283
  }
@@ -1301,9 +1302,10 @@ function packageJsonFor(profile) {
1301
1302
  )}
1302
1303
  `;
1303
1304
  }
1304
- function patchYamlFor() {
1305
+ function patchYamlFor(options) {
1305
1306
  const own = ownPackageInfo();
1306
- return [
1307
+ const bridgeTools = options?.bridgeTools ?? true;
1308
+ const lines = [
1307
1309
  "# dsh-lark SDK JSON-RPC runtime overlay (managed by dsh-lark-bot).",
1308
1310
  "# stdout is reserved for SDK JSON-RPC frames; no console logger may load.",
1309
1311
  "- insert:",
@@ -1324,26 +1326,31 @@ function patchYamlFor() {
1324
1326
  "",
1325
1327
  "- id: hmr",
1326
1328
  " disabled: true",
1327
- "",
1328
- // In-process bridge callback tool: lets the agent mention users and push
1329
- // messages to other chats/topics through the running bridge process.
1330
- "- insert:",
1331
- " - id: lark-notify",
1332
- ` name: '${own.name}/notify'`,
1333
- " config:",
1334
- " endpoint: !!js process.env.DSH_LARK_NOTIFY_URL",
1335
- " token: !!js process.env.DSH_LARK_NOTIFY_TOKEN",
1336
- "",
1337
- // Question-card tool: the agent asks the user for decisions / missing
1338
- // information; the bridge shows a card and returns the answer.
1339
- "- insert:",
1340
- " - id: lark-ask",
1341
- ` name: '${own.name}/ask'`,
1342
- " config:",
1343
- " endpoint: !!js process.env.DSH_LARK_ASK_URL",
1344
- " token: !!js process.env.DSH_LARK_NOTIFY_TOKEN",
1345
1329
  ""
1346
- ].join("\n");
1330
+ ];
1331
+ if (bridgeTools) {
1332
+ lines.push(
1333
+ // In-process bridge callback tool: lets the agent mention users and push
1334
+ // messages to other chats/topics through the running bridge process.
1335
+ "- insert:",
1336
+ " - id: lark-notify",
1337
+ ` name: '${own.name}/notify'`,
1338
+ " config:",
1339
+ " endpoint: !!js process.env.DSH_LARK_NOTIFY_URL",
1340
+ " token: !!js process.env.DSH_LARK_NOTIFY_TOKEN",
1341
+ "",
1342
+ // Question-card tool: the agent asks the user for decisions / missing
1343
+ // information; the bridge shows a card and returns the answer.
1344
+ "- insert:",
1345
+ " - id: lark-ask",
1346
+ ` name: '${own.name}/ask'`,
1347
+ " config:",
1348
+ " endpoint: !!js process.env.DSH_LARK_ASK_URL",
1349
+ " token: !!js process.env.DSH_LARK_NOTIFY_TOKEN",
1350
+ ""
1351
+ );
1352
+ }
1353
+ return lines.join("\n");
1347
1354
  }
1348
1355
  function sdkServerInstalled(profileRoot) {
1349
1356
  const candidates = [
@@ -1401,7 +1408,13 @@ async function ensureSdkProfile(options) {
1401
1408
  await mkdir(root, { recursive: true });
1402
1409
  await writeFile(join3(root, "package.json"), packageJsonFor(profile), "utf8");
1403
1410
  await writeFile(join3(root, "cordis.yml"), "[]\n", "utf8");
1404
- await writeFile(join3(root, "cordis.patch.yml"), patchYamlFor(), "utf8");
1411
+ await writeFile(
1412
+ join3(root, "cordis.patch.yml"),
1413
+ patchYamlFor(
1414
+ options.bridgeTools === void 0 ? {} : { bridgeTools: options.bridgeTools }
1415
+ ),
1416
+ "utf8"
1417
+ );
1405
1418
  const install = options.install ?? runPnpmInstall;
1406
1419
  if (!isSdkProfileReady(root)) {
1407
1420
  await install(root);
@@ -2179,6 +2192,10 @@ var PendingQueue = class {
2179
2192
  hasPending(scope) {
2180
2193
  return (this.pending.get(scope)?.length ?? 0) > 0;
2181
2194
  }
2195
+ /** Number of messages waiting in the queue for a scope (not yet flushing). */
2196
+ size(scope) {
2197
+ return this.pending.get(scope)?.length ?? 0;
2198
+ }
2182
2199
  isBlocked(scope) {
2183
2200
  return this.blocked.has(scope);
2184
2201
  }
@@ -2442,7 +2459,9 @@ var initialState = {
2442
2459
  footer: "thinking",
2443
2460
  terminal: "running",
2444
2461
  errorMsg: void 0,
2445
- idleTimeoutMinutes: void 0
2462
+ idleTimeoutMinutes: void 0,
2463
+ startedAtMs: void 0,
2464
+ lastActivityMs: void 0
2446
2465
  };
2447
2466
  function closeStreamingText(blocks) {
2448
2467
  return blocks.map(
@@ -2588,8 +2607,16 @@ function markdown(content) {
2588
2607
  function noteMd(content) {
2589
2608
  return { tag: "markdown", content, text_size: "notation" };
2590
2609
  }
2591
- function footerStatus(status) {
2592
- const text = status === "thinking" ? "\u{1F9E0} \u6B63\u5728\u601D\u8003" : status === "tool_running" ? "\u{1F9F0} \u6B63\u5728\u8C03\u7528\u5DE5\u5177" : "\u270D\uFE0F \u6B63\u5728\u8F93\u51FA";
2610
+ function footerStatus(status, state, now) {
2611
+ let text = status === "thinking" ? "\u{1F9E0} \u6B63\u5728\u601D\u8003" : status === "tool_running" ? "\u{1F9F0} \u6B63\u5728\u8C03\u7528\u5DE5\u5177" : "\u270D\uFE0F \u6B63\u5728\u8F93\u51FA";
2612
+ if (state.startedAtMs !== void 0) {
2613
+ const elapsed = Math.max(0, Math.round((now - state.startedAtMs) / 1e3));
2614
+ text += ` \u23F1 ${elapsed}s`;
2615
+ }
2616
+ if (state.lastActivityMs !== void 0) {
2617
+ const idle = Math.max(0, Math.round((now - state.lastActivityMs) / 1e3));
2618
+ if (idle >= 60) text += ` \xB7 \u23F8 \u65E0\u54CD\u5E94 ${idle}s`;
2619
+ }
2593
2620
  return noteMd(text);
2594
2621
  }
2595
2622
  function summaryText(state) {
@@ -2623,7 +2650,7 @@ function usageLine(state) {
2623
2650
  if (state.usage.outputTokens !== void 0) parts.push(`out ${state.usage.outputTokens}`);
2624
2651
  return parts.length ? `\uFF08tokens ${parts.join(" \xB7 ")}\uFF09` : "";
2625
2652
  }
2626
- function renderStandard(state) {
2653
+ function renderStandard(state, now) {
2627
2654
  const elements = [];
2628
2655
  if (state.reasoning.content) {
2629
2656
  elements.push(
@@ -2652,7 +2679,7 @@ function renderStandard(state) {
2652
2679
  if (usage) elements.push(noteMd(usage));
2653
2680
  }
2654
2681
  if (state.terminal === "running") {
2655
- if (state.footer) elements.push(footerStatus(state.footer));
2682
+ if (state.footer) elements.push(footerStatus(state.footer, state, now));
2656
2683
  elements.push(stopButton());
2657
2684
  }
2658
2685
  return {
@@ -2664,10 +2691,10 @@ function renderStandard(state) {
2664
2691
  body: { elements }
2665
2692
  };
2666
2693
  }
2667
- function renderCompact(state) {
2694
+ function renderCompact(state, now) {
2668
2695
  const elements = [noteMd(summaryText(state))];
2669
2696
  if (state.terminal === "running" && state.footer) {
2670
- elements.push(footerStatus(state.footer));
2697
+ elements.push(footerStatus(state.footer, state, now));
2671
2698
  }
2672
2699
  if (state.terminal === "running") {
2673
2700
  elements.push(stopButton());
@@ -2681,7 +2708,7 @@ function renderCompact(state) {
2681
2708
  body: { elements }
2682
2709
  };
2683
2710
  }
2684
- function renderDetailed(state) {
2711
+ function renderDetailed(state, now) {
2685
2712
  const elements = [];
2686
2713
  if (state.reasoning.content) {
2687
2714
  elements.push(
@@ -2720,7 +2747,7 @@ ${safeJsonPreview(tool.input)}
2720
2747
  if (usage) elements.push(noteMd(usage));
2721
2748
  }
2722
2749
  if (state.terminal === "running") {
2723
- if (state.footer) elements.push(footerStatus(state.footer));
2750
+ if (state.footer) elements.push(footerStatus(state.footer, state, now));
2724
2751
  elements.push(stopButton());
2725
2752
  }
2726
2753
  return {
@@ -2740,10 +2767,10 @@ function safeJsonPreview(value) {
2740
2767
  return String(value).slice(0, 300);
2741
2768
  }
2742
2769
  }
2743
- function renderCard(state, density = "standard") {
2744
- if (density === "compact") return renderCompact(state);
2745
- if (density === "detailed") return renderDetailed(state);
2746
- return renderStandard(state);
2770
+ function renderCard(state, density = "standard", now = Date.now()) {
2771
+ if (density === "compact") return renderCompact(state, now);
2772
+ if (density === "detailed") return renderDetailed(state, now);
2773
+ return renderStandard(state, now);
2747
2774
  }
2748
2775
 
2749
2776
  // src/card/approval-card.ts
@@ -2895,7 +2922,12 @@ async function runAgentBatch(input) {
2895
2922
  } : {}
2896
2923
  });
2897
2924
  input.activeRuns.set(input.scope, { runId, stop: run.stop });
2898
- let state = initialState;
2925
+ const now = Date.now();
2926
+ let state = {
2927
+ ...initialState,
2928
+ startedAtMs: now,
2929
+ lastActivityMs: now
2930
+ };
2899
2931
  const stopRequested = { value: false };
2900
2932
  const timeoutMs = input.runPolicies?.get(input.scope) ?? input.runTimeoutMs ?? 0;
2901
2933
  let timedOut = false;
@@ -2906,10 +2938,16 @@ async function runAgentBatch(input) {
2906
2938
  input.chatId,
2907
2939
  renderCard(state, density),
2908
2940
  async (controller) => {
2941
+ const ticker = setInterval(() => {
2942
+ void controller.update(renderCard(state, density, Date.now())).catch(() => {
2943
+ });
2944
+ }, 5e3);
2945
+ ticker.unref?.();
2909
2946
  const consume = async () => {
2910
2947
  for await (const event of run.events) {
2911
2948
  if (timedOut) return;
2912
2949
  state = applyEvent(state, event, stopRequested);
2950
+ state = { ...state, lastActivityMs: Date.now() };
2913
2951
  if (event.type === "final_text") {
2914
2952
  assistantOutput = event.content;
2915
2953
  } else if (event.type === "text") {
@@ -2950,8 +2988,9 @@ async function runAgentBatch(input) {
2950
2988
  await consume();
2951
2989
  }
2952
2990
  state = timedOut ? markIdleTimeout(state, timeoutMs / 6e4) : finalizeIfRunning(state);
2953
- await controller.update(renderCard(state, density));
2991
+ await controller.update(renderCard(state, density, Date.now()));
2954
2992
  } finally {
2993
+ clearInterval(ticker);
2955
2994
  if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
2956
2995
  unsubscribeSettled?.();
2957
2996
  }
@@ -4578,7 +4617,18 @@ async function startChannel(deps) {
4578
4617
  log.fail("channel-command", error, { scope });
4579
4618
  return false;
4580
4619
  });
4581
- if (!handled) deps.pending.push(scope, msg);
4620
+ if (!handled) {
4621
+ const queued = deps.pending.size(scope);
4622
+ const busy = queued > 0 || deps.pending.isFlushing(scope) || deps.pending.isBlocked(scope);
4623
+ if (busy) {
4624
+ await commandChannel.sendMarkdown(
4625
+ msg.chatId,
4626
+ `\u23F3 \u5DF2\u6536\u5230\uFF0C\u5F53\u524D\u6392\u961F\u4E2D\uFF08\u961F\u5217 ${queued + 1} \u6761\uFF09\u3002\u4EFB\u52A1\u5F00\u59CB\u540E\u4F1A\u5728\u8FD9\u91CC\u5B9E\u65F6\u663E\u793A\u8FDB\u5EA6\u3002`,
4627
+ { replyTo: msg.messageId }
4628
+ );
4629
+ }
4630
+ deps.pending.push(scope, msg);
4631
+ }
4582
4632
  },
4583
4633
  cardAction: async (event) => {
4584
4634
  const value = event.action.value && typeof event.action.value === "object" ? event.action.value : void 0;
@@ -6383,9 +6433,13 @@ import { join as join16 } from "path";
6383
6433
 
6384
6434
  // src/guardian/service.ts
6385
6435
  import { randomUUID as randomUUID5 } from "crypto";
6436
+ import { readFileSync as readFileSync4 } from "fs";
6386
6437
  import { homedir as homedir8 } from "os";
6387
6438
  import { join as join15 } from "path";
6388
- import { createLarkChannel as createLarkChannel2 } from "@larksuite/channel";
6439
+ import {
6440
+ createLarkChannel as createLarkChannel2
6441
+ } from "@larksuite/channel";
6442
+ import { parse } from "yaml";
6389
6443
  init_dsh_runtime();
6390
6444
 
6391
6445
  // src/guardian/control.ts
@@ -6410,6 +6464,11 @@ function parseGuardianCommand(text) {
6410
6464
  case "leave":
6411
6465
  case "restore":
6412
6466
  return { kind: "safemode-exit", argument };
6467
+ case "stop":
6468
+ case "cancel":
6469
+ case "abort":
6470
+ case "kill":
6471
+ return { kind: "safemode-stop", argument };
6413
6472
  case "help":
6414
6473
  case "?":
6415
6474
  return { kind: "safemode-help", argument };
@@ -6422,6 +6481,7 @@ var SAFEMODE_HELP = [
6422
6481
  "/safemode status \u2014 \u67E5\u770B\u5B88\u62A4\u72B6\u6001",
6423
6482
  "/safemode plugins \u2014 \u5217\u51FA\u6545\u969C profile \u5DF2\u5B89\u88C5\u7684\u63D2\u4EF6",
6424
6483
  "/safemode exit \u2014 \u9000\u51FA\u5B89\u5168\u6A21\u5F0F\uFF0C\u91CD\u542F\u5B8C\u6574 profile \u5E76\u4EA4\u8FD8\u98DE\u4E66\u901A\u9053",
6484
+ "/safemode stop \u2014 \u7EC8\u6B62\u5F53\u524D\u6B63\u5728\u8FD0\u884C\u7684\u5B89\u5168\u6A21\u5F0F\u4EFB\u52A1",
6425
6485
  "",
6426
6486
  "\u5B89\u5168\u6A21\u5F0F\u4E0B\u53D1\u9001\u666E\u901A\u6D88\u606F\u5373\u53EF\u4E0E dsh \u6838\u5FC3\u5BF9\u8BDD\uFF0C\u8FDB\u884C\u5B9A\u4F4D / \u4FEE\u590D / \u7981\u7528\u635F\u574F\u63D2\u4EF6\u7684\u81EA\u6108\u64CD\u4F5C\u3002"
6427
6487
  ].join("\n");
@@ -6430,6 +6490,7 @@ var SAFEMODE_HELP_EN = [
6430
6490
  "/safemode status \u2014 show guardian state",
6431
6491
  "/safemode plugins \u2014 list plugins installed into the broken profile",
6432
6492
  "/safemode exit \u2014 leave safe mode, relaunch the full profile and hand the channel back",
6493
+ "/safemode stop \u2014 interrupt the currently running safe-mode task",
6433
6494
  "",
6434
6495
  "In safe mode, send normal messages to converse with the dsh core for self-healing."
6435
6496
  ].join("\n");
@@ -6555,11 +6616,15 @@ async function probeSafeProfile(options) {
6555
6616
  // src/guardian/service.ts
6556
6617
  var MAX_TRANSCRIPT_ENTRIES = 30;
6557
6618
  var SAFE_RUN_TIMEOUT_MS = 10 * 6e4;
6619
+ var SAFE_SDK_PROVISION_TIMEOUT_MS = 3 * 6e4;
6620
+ var SAFE_CARD_TICK_MS = 5e3;
6558
6621
  var GuardianService = class {
6559
6622
  options;
6560
6623
  state;
6561
6624
  channel;
6562
- safeAdapter;
6625
+ safeEngine;
6626
+ safeRuns = /* @__PURE__ */ new Map();
6627
+ sessionIds = /* @__PURE__ */ new Map();
6563
6628
  transcripts = /* @__PURE__ */ new Map();
6564
6629
  downStreak = 0;
6565
6630
  lastHeartbeatFreshAt;
@@ -6577,6 +6642,9 @@ var GuardianService = class {
6577
6642
  engineDeadMs: options.engineDeadMs ?? 12e4,
6578
6643
  takeoverGracePolls: options.takeoverGracePolls ?? 2,
6579
6644
  sendDelayMs: options.sendDelayMs ?? 600,
6645
+ safeAdapterMode: options.safeAdapterMode ?? "auto",
6646
+ safeTimeoutMs: options.safeTimeoutMs ?? SAFE_RUN_TIMEOUT_MS,
6647
+ safeDensity: options.safeDensity ?? "detailed",
6580
6648
  stateFile: options.stateFile,
6581
6649
  configFile: options.configFile,
6582
6650
  heartbeatFile: options.heartbeatFile,
@@ -6591,6 +6659,7 @@ var GuardianService = class {
6591
6659
  findProcess: options.findProcess,
6592
6660
  spawnDetachedFn: options.spawnDetachedFn,
6593
6661
  probeSafeProfileFn: options.probeSafeProfileFn,
6662
+ ensureSdkProfile: options.ensureSdkProfile,
6594
6663
  runPluginList: options.runPluginList,
6595
6664
  dshBin: options.dshBin,
6596
6665
  saveState: options.saveState,
@@ -6633,9 +6702,11 @@ var GuardianService = class {
6633
6702
  this.stopped = true;
6634
6703
  if (this.timer) clearInterval(this.timer);
6635
6704
  this.timer = void 0;
6705
+ await this.stopAllSafeRuns();
6706
+ this.sessionIds.clear();
6636
6707
  await Promise.allSettled([...this.activeMessages]);
6637
6708
  await this.disconnectChannel();
6638
- await this.safeAdapter?.dispose?.();
6709
+ await this.disposeSafeEngine();
6639
6710
  this.log().info("guardian", "stopped", {});
6640
6711
  }
6641
6712
  snapshot() {
@@ -6648,6 +6719,8 @@ var GuardianService = class {
6648
6719
  dshUp: this.dshUp,
6649
6720
  heartbeatAgeMs: this.lastHeartbeatAgeMs,
6650
6721
  channelConnected: this.channel !== void 0,
6722
+ safeEngine: this.safeEngine?.kind,
6723
+ safeRuns: this.safeRuns.size,
6651
6724
  pid: process.pid,
6652
6725
  dshBin: this.dshBin,
6653
6726
  relaunchedPid: this.state.relaunchedPid,
@@ -6702,8 +6775,11 @@ var GuardianService = class {
6702
6775
  this.downStreak = 0;
6703
6776
  await this.setSeenUp();
6704
6777
  if (this.state.mode !== "standby" || this.channel !== void 0) {
6778
+ await this.stopAllSafeRuns();
6779
+ this.sessionIds.clear();
6705
6780
  await this.disconnectChannel();
6706
6781
  this.transcripts.clear();
6782
+ await this.disposeSafeEngine();
6707
6783
  await this.setMode("standby");
6708
6784
  }
6709
6785
  return;
@@ -6754,6 +6830,7 @@ var GuardianService = class {
6754
6830
  });
6755
6831
  channel.on({
6756
6832
  message: (msg) => this.track(this.handleMessage(msg)),
6833
+ cardAction: (event) => this.track(this.handleCardAction(event)),
6757
6834
  error: (error) => {
6758
6835
  this.log().fail("guardian-channel", error);
6759
6836
  },
@@ -6870,13 +6947,16 @@ var GuardianService = class {
6870
6947
  case "safemode-exit":
6871
6948
  await this.exitSafeMode(msg);
6872
6949
  return;
6950
+ case "safemode-stop":
6951
+ await this.handleStop(msg);
6952
+ return;
6873
6953
  case "safemode-help":
6874
6954
  await this.sendMarkdown(msg.chatId, safemodeHelpText(), msg.messageId);
6875
6955
  return;
6876
6956
  }
6877
6957
  }
6878
6958
  async enterSafeMode(msg) {
6879
- if (this.state.mode === "safe") {
6959
+ if (this.state.mode === "safe" && this.safeEngine) {
6880
6960
  await this.sendMarkdown(
6881
6961
  msg.chatId,
6882
6962
  "\u5DF2\u5728\u5B89\u5168\u6A21\u5F0F\u4E2D\u3002\u53D1\u9001\u666E\u901A\u6D88\u606F\u4E0E dsh \u6838\u5FC3\u5BF9\u8BDD\uFF1B`/safemode exit` \u9000\u51FA\u3002",
@@ -6924,16 +7004,26 @@ var GuardianService = class {
6924
7004
  );
6925
7005
  return;
6926
7006
  }
6927
- this.safeAdapter ??= this.options.adapter ?? new DshAdapter({
6928
- command: "node",
6929
- args: [bin, "--profile", this.state.safeProfile],
6930
- stopGraceMs: 5e3
6931
- });
7007
+ const engine = await this.resolveSafeEngine();
7008
+ if (!engine) {
7009
+ await this.sendMarkdown(
7010
+ msg.chatId,
7011
+ [
7012
+ "\u5B89\u5168\u6A21\u5F0F\u5F15\u64CE\u4E0D\u53EF\u7528\uFF1ASDK runtime \u672A\u5C31\u7EEA\u4E14 headless \u4E5F\u65E0\u6CD5\u542F\u52A8\u3002",
7013
+ "\u8BF7\u68C0\u67E5\u672C\u673A dsh \u5B89\u88C5\u4E0E pnpm \u53EF\u7528\u6027\u540E\u91CD\u8BD5\u3002"
7014
+ ].join("\n"),
7015
+ msg.messageId
7016
+ );
7017
+ return;
7018
+ }
7019
+ this.safeEngine = engine;
6932
7020
  await this.setMode("safe");
7021
+ const engineLabel = engine.kind === "sdk" ? "SDK \u6D41\u5F0F\u5F15\u64CE\uFF08\u5B9E\u65F6\u601D\u8003 / \u5DE5\u5177\u8C03\u7528 / \u6587\u5B57\u8F93\u51FA\uFF09" : engine.kind === "headless" ? "headless \u56DE\u9000\u5F15\u64CE\uFF08\u5B8C\u6210\u540E\u4E00\u6B21\u6027\u8F93\u51FA\uFF0C\u4EFB\u52A1\u671F\u95F4\u5361\u7247\u5B9E\u65F6\u663E\u793A\u6D3B\u52A8\u72B6\u6001\uFF09" : "\u6D4B\u8BD5\u5F15\u64CE";
6933
7022
  await this.sendMarkdown(
6934
7023
  msg.chatId,
6935
7024
  [
6936
- "\u5B89\u5168\u6A21\u5F0F\u5DF2\u5C31\u7EEA\uFF1Adsh \u4E3B\u6838\u5FC3\u8FD0\u884C\u4E2D\uFF0C\u7B2C\u4E09\u65B9\u63D2\u4EF6\u672A\u52A0\u8F7D\u3002",
7025
+ `\u5B89\u5168\u6A21\u5F0F\u5DF2\u5C31\u7EEA\uFF1Adsh \u4E3B\u6838\u5FC3\u8FD0\u884C\u4E2D\uFF0C\u7B2C\u4E09\u65B9\u63D2\u4EF6\u672A\u52A0\u8F7D\u3002`,
7026
+ `\u5F15\u64CE\uFF1A${engineLabel}`,
6937
7027
  "",
6938
7028
  "\u73B0\u5728\u53EF\u4EE5\u76F4\u63A5\u5BF9\u8BDD\u8FDB\u884C\u81EA\u6108\uFF08\u5B9A\u4F4D / \u4FEE\u590D / \u7981\u7528\u635F\u574F\u63D2\u4EF6\uFF09\uFF0C\u4F8B\u5982\uFF1A",
6939
7029
  "- \u201C\u5217\u51FA\u5F53\u524D profile \u5B89\u88C5\u7684\u63D2\u4EF6\u5E76\u68C0\u67E5\u54EA\u4E2A\u6700\u8FD1\u53D8\u574F\u201D",
@@ -6961,6 +7051,8 @@ var GuardianService = class {
6961
7051
  `dsh \u662F\u5426\u5728\u7EBF\uFF1A${snapshot.dshUp ? "\u662F" : "\u5426"}`,
6962
7052
  `\u5FC3\u8DF3\u9F84\uFF1A${snapshot.heartbeatAgeMs === void 0 ? "\u65E0" : `${snapshot.heartbeatAgeMs}ms`}`,
6963
7053
  `\u98DE\u4E66\u901A\u9053\uFF1A${snapshot.channelConnected ? "\u5B88\u62A4\u5DF2\u63A5\u7BA1" : "\u672A\u8FDE\u63A5"}`,
7054
+ `\u5B89\u5168\u5F15\u64CE\uFF1A${snapshot.safeEngine ?? "\u672A\u5C31\u7EEA"}`,
7055
+ `\u5B89\u5168\u6A21\u5F0F\u8FD0\u884C\u4E2D\u4EFB\u52A1\uFF1A${snapshot.safeRuns}`,
6964
7056
  `dsh bin\uFF1A${snapshot.dshBin ?? "\u672A\u53D1\u73B0"}`,
6965
7057
  `\u5B88\u62A4 pid\uFF1A${snapshot.pid}`,
6966
7058
  `\u5DF2\u89C2\u5BDF\u8FC7 dsh \u8FD0\u884C\uFF1A${snapshot.profileSeenUp ? "\u662F" : "\u5426"}`
@@ -7024,12 +7116,15 @@ var GuardianService = class {
7024
7116
  msg.messageId
7025
7117
  );
7026
7118
  await delay(this.options.sendDelayMs);
7119
+ await this.stopAllSafeRuns();
7120
+ this.sessionIds.clear();
7027
7121
  this.transcripts.clear();
7122
+ await this.disposeSafeEngine();
7028
7123
  await this.disconnectChannel();
7029
7124
  }
7030
7125
  async runSafeTask(scope, msg) {
7031
- const adapter = this.safeAdapter;
7032
- if (!adapter) {
7126
+ const engine = this.safeEngine;
7127
+ if (!engine) {
7033
7128
  await this.sendMarkdown(
7034
7129
  msg.chatId,
7035
7130
  "\u5B89\u5168\u6A21\u5F0F\u672A\u5C31\u7EEA\uFF0C\u8BF7\u5148\u53D1\u9001 `/safemode`\u3002",
@@ -7037,67 +7132,259 @@ var GuardianService = class {
7037
7132
  );
7038
7133
  return;
7039
7134
  }
7135
+ const active = this.safeRuns.get(scope);
7136
+ if (active) {
7137
+ const elapsed = Math.round((Date.now() - active.startedAt) / 1e3);
7138
+ await this.sendMarkdown(
7139
+ msg.chatId,
7140
+ `\uFF08\u5B89\u5168\u6A21\u5F0F\uFF09\u4E0A\u4E00\u6761\u4EFB\u52A1\u4ECD\u5728\u5904\u7406\u4E2D\uFF0C\u5DF2\u8FD0\u884C ${elapsed}s\u3002\u53D1\u9001 \`/safemode stop\` \u6216\u70B9\u51FB\u5361\u7247 \u23F9 \u6309\u94AE\u53EF\u7EC8\u6B62\u3002`,
7141
+ msg.messageId
7142
+ );
7143
+ return;
7144
+ }
7040
7145
  const transcript = this.transcripts.get(scope) ?? [];
7041
7146
  const prompt = buildSafePrompt(transcript, msg.content);
7042
- await this.sendMarkdown(
7043
- msg.chatId,
7044
- "\uFF08\u5B89\u5168\u6A21\u5F0F \xB7 dsh \u6838\u5FC3\u5904\u7406\u4E2D\u2026\uFF09",
7045
- msg.messageId
7046
- );
7047
- const run = adapter.run({
7048
- runId: randomUUID5(),
7147
+ const runId = randomUUID5();
7148
+ const density = this.options.safeDensity;
7149
+ const timeoutMs = this.options.safeTimeoutMs;
7150
+ const now = Date.now();
7151
+ let state = {
7152
+ ...initialState,
7153
+ startedAtMs: now,
7154
+ lastActivityMs: now
7155
+ };
7156
+ let assistantOutput = "";
7157
+ let finalText;
7158
+ let errorText;
7159
+ let timedOut = false;
7160
+ let events = 0;
7161
+ const run = engine.adapter.run({
7162
+ runId,
7049
7163
  prompt,
7050
7164
  cwd: this.config?.workspaces.default,
7051
- sessionId: void 0,
7165
+ sessionId: this.sessionIds.get(scope),
7052
7166
  model: void 0,
7053
7167
  images: void 0,
7054
7168
  stopGraceMs: 5e3
7055
7169
  });
7056
- const textParts = [];
7057
- let finalText;
7058
- let errorText;
7059
- let terminal = false;
7170
+ this.safeRuns.set(scope, { stop: () => run.stop(), startedAt: now });
7171
+ this.log().info("guardian-safe", "task-start", {
7172
+ runId,
7173
+ scope,
7174
+ engine: engine.kind,
7175
+ timeoutMs
7176
+ });
7060
7177
  try {
7061
- for await (const event of run.events) {
7062
- if (event.type === "text" && event.delta) textParts.push(event.delta);
7063
- if (event.type === "final_text") finalText = event.content;
7064
- if (event.type === "error") {
7065
- errorText = event.message;
7066
- terminal = true;
7067
- }
7068
- if (event.type === "done") terminal = true;
7069
- }
7070
- } catch (error) {
7071
- errorText = error instanceof Error ? error.message : String(error);
7072
- terminal = true;
7073
- }
7074
- if (!terminal) {
7075
- await run.waitForExit(SAFE_RUN_TIMEOUT_MS);
7076
- }
7077
- if (errorText) {
7078
- await this.sendMarkdown(
7178
+ await this.streamCard(
7079
7179
  msg.chatId,
7080
- `\uFF08\u5B89\u5168\u6A21\u5F0F \xB7 dsh \u6838\u5FC3\u9519\u8BEF\uFF09
7081
- ${errorText.slice(0, 2e3)}`,
7082
- msg.messageId
7180
+ renderCard(state, density, Date.now()),
7181
+ async (controller) => {
7182
+ const ticker = setInterval(() => {
7183
+ void controller.update(renderCard(state, density, Date.now())).catch(() => {
7184
+ });
7185
+ }, SAFE_CARD_TICK_MS);
7186
+ ticker.unref?.();
7187
+ let timeoutTimer;
7188
+ try {
7189
+ const consume = async () => {
7190
+ for await (const event of run.events) {
7191
+ if (timedOut) return;
7192
+ events += 1;
7193
+ state = reduce(state, event);
7194
+ state = { ...state, lastActivityMs: Date.now() };
7195
+ if (event.type === "final_text") {
7196
+ finalText = event.content;
7197
+ } else if (event.type === "text") {
7198
+ assistantOutput += event.delta;
7199
+ }
7200
+ if (event.type === "system" && event.sessionId) {
7201
+ this.sessionIds.set(scope, event.sessionId);
7202
+ }
7203
+ if (event.type === "error") errorText = event.message;
7204
+ await controller.update(renderCard(state, density, Date.now()));
7205
+ }
7206
+ };
7207
+ const timeoutPromise = timeoutMs > 0 ? new Promise((resolve5) => {
7208
+ timeoutTimer = setTimeout(() => {
7209
+ if (timedOut) return;
7210
+ timedOut = true;
7211
+ state = markIdleTimeout(
7212
+ state,
7213
+ Math.round(timeoutMs / 6e4)
7214
+ );
7215
+ void run.stop();
7216
+ resolve5();
7217
+ }, timeoutMs);
7218
+ }) : void 0;
7219
+ if (timeoutPromise) {
7220
+ await Promise.race([consume(), timeoutPromise]);
7221
+ } else {
7222
+ await consume();
7223
+ }
7224
+ if (!timedOut && state.terminal === "running") {
7225
+ state = finalizeIfRunning(state);
7226
+ }
7227
+ await controller.update(renderCard(state, density, Date.now()));
7228
+ } finally {
7229
+ clearInterval(ticker);
7230
+ if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
7231
+ }
7232
+ },
7233
+ { replyTo: msg.messageId }
7083
7234
  );
7235
+ } catch (error) {
7236
+ this.log().fail("guardian-safe", error, { runId, scope });
7237
+ state = markInterrupted(state);
7238
+ try {
7239
+ await this.sendMarkdown(
7240
+ msg.chatId,
7241
+ `\u26A0\uFE0F \u5B89\u5168\u6A21\u5F0F\u4EFB\u52A1\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`,
7242
+ msg.messageId
7243
+ );
7244
+ } catch {
7245
+ }
7084
7246
  return;
7247
+ } finally {
7248
+ this.safeRuns.delete(scope);
7085
7249
  }
7086
- const answer = finalText ?? textParts.join("").trim();
7250
+ const durationMs = Date.now() - now;
7251
+ const terminal = state.terminal;
7252
+ this.log().info("guardian-safe", "task-end", {
7253
+ runId,
7254
+ scope,
7255
+ engine: engine.kind,
7256
+ terminal,
7257
+ events,
7258
+ durationMs,
7259
+ ...errorText === void 0 ? {} : { errorText },
7260
+ outputLength: (finalText ?? assistantOutput).trim().length
7261
+ });
7262
+ if (timedOut || terminal === "interrupted" || terminal === "error" || terminal === "idle_timeout") {
7263
+ return;
7264
+ }
7265
+ const answer = finalText ?? assistantOutput.trim();
7087
7266
  if (!answer) {
7088
- await this.sendMarkdown(
7089
- msg.chatId,
7090
- "\uFF08\u5B89\u5168\u6A21\u5F0F \xB7 dsh \u6838\u5FC3\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09",
7091
- msg.messageId
7092
- );
7267
+ this.log().warn("guardian-safe", "task-empty", { runId, scope });
7093
7268
  return;
7094
7269
  }
7095
- await this.sendMarkdown(msg.chatId, answer, msg.messageId);
7096
7270
  this.transcripts.set(scope, pushTranscript(transcript, [
7097
7271
  { role: "user", content: msg.content },
7098
7272
  { role: "assistant", content: answer }
7099
7273
  ]));
7100
7274
  }
7275
+ async streamCard(chatId, initial, producer, options) {
7276
+ if (!this.channel) return;
7277
+ await this.channel.stream(chatId, { card: { initial, producer } }, options);
7278
+ }
7279
+ async handleCardAction(event) {
7280
+ const value = isRecord4(event.action.value) ? event.action.value : void 0;
7281
+ if (value?.cmd !== "stop") return;
7282
+ const scope = safeScopeForAction(event);
7283
+ const run = this.safeRuns.get(scope);
7284
+ if (!run) return;
7285
+ this.log().info("guardian-safe", "stop-requested", { scope });
7286
+ await run.stop();
7287
+ }
7288
+ async handleStop(msg) {
7289
+ const scope = this.scopeFor(msg);
7290
+ const run = this.safeRuns.get(scope);
7291
+ if (!run) {
7292
+ await this.sendMarkdown(
7293
+ msg.chatId,
7294
+ "\u5F53\u524D\u6CA1\u6709\u6B63\u5728\u8FD0\u884C\u7684\u5B89\u5168\u6A21\u5F0F\u4EFB\u52A1\u3002",
7295
+ msg.messageId
7296
+ );
7297
+ return;
7298
+ }
7299
+ await this.sendMarkdown(
7300
+ msg.chatId,
7301
+ "\u5DF2\u8BF7\u6C42\u7EC8\u6B62\u5F53\u524D\u4EFB\u52A1\uFF0C\u6B63\u5728\u505C\u6B62 dsh \u8FDB\u7A0B\u2026",
7302
+ msg.messageId
7303
+ );
7304
+ await run.stop();
7305
+ }
7306
+ async resolveSafeEngine() {
7307
+ if (this.options.adapter) {
7308
+ return { adapter: this.options.adapter, kind: "test" };
7309
+ }
7310
+ const mode = this.options.safeAdapterMode;
7311
+ if (mode === "sdk" || mode === "auto") {
7312
+ const launch = await this.provisionSafeSdk();
7313
+ if (launch) {
7314
+ this.log().info("guardian-safe", "engine-selected", { engine: "sdk" });
7315
+ return {
7316
+ adapter: new SdkDshAdapter({
7317
+ launch,
7318
+ provider: this.options.env?.DSH_LARK_PROVIDER ?? "deepseek-official",
7319
+ model: this.safeModel()
7320
+ }),
7321
+ kind: "sdk"
7322
+ };
7323
+ }
7324
+ if (mode === "sdk") return void 0;
7325
+ }
7326
+ if (!this.dshBin) return void 0;
7327
+ this.log().info("guardian-safe", "engine-selected", { engine: "headless" });
7328
+ return {
7329
+ adapter: new DshAdapter({
7330
+ command: "node",
7331
+ args: [this.dshBin, "--profile", this.state.safeProfile],
7332
+ stopGraceMs: 5e3
7333
+ }),
7334
+ kind: "headless"
7335
+ };
7336
+ }
7337
+ async provisionSafeSdk() {
7338
+ if (!this.dshBin) return void 0;
7339
+ const ensure = this.options.ensureSdkProfile ?? ensureSdkProfile;
7340
+ const result = await withTimeout2(
7341
+ ensure({
7342
+ home: this.options.home,
7343
+ env: this.options.env ?? process.env,
7344
+ profile: DEFAULT_SAFE_SDK_PROFILE,
7345
+ bridgeTools: false
7346
+ }),
7347
+ SAFE_SDK_PROVISION_TIMEOUT_MS
7348
+ );
7349
+ if (!result) {
7350
+ this.log().warn("guardian-safe", "sdk-provision-timeout", {
7351
+ profile: DEFAULT_SAFE_SDK_PROFILE
7352
+ });
7353
+ return void 0;
7354
+ }
7355
+ if (!result.ok) {
7356
+ this.log().warn("guardian-safe", "sdk-provision-failed", {
7357
+ profile: DEFAULT_SAFE_SDK_PROFILE,
7358
+ error: result.error
7359
+ });
7360
+ return void 0;
7361
+ }
7362
+ return resolveSdkLaunch({
7363
+ home: this.options.home,
7364
+ env: this.options.env ?? process.env,
7365
+ profile: DEFAULT_SAFE_SDK_PROFILE
7366
+ });
7367
+ }
7368
+ safeModel() {
7369
+ const explicit = this.options.env?.DSH_LARK_MODEL;
7370
+ if (explicit) return explicit;
7371
+ return readDshDefaultModel(this.options.home, this.options.env ?? process.env) ?? "deepseek-v4-flash";
7372
+ }
7373
+ async stopAllSafeRuns() {
7374
+ const runs = [...this.safeRuns.values()];
7375
+ this.safeRuns.clear();
7376
+ await Promise.allSettled(runs.map((run) => run.stop()));
7377
+ }
7378
+ async disposeSafeEngine() {
7379
+ const engine = this.safeEngine;
7380
+ this.safeEngine = void 0;
7381
+ if (!engine) return;
7382
+ try {
7383
+ await engine.adapter.dispose?.();
7384
+ } catch (error) {
7385
+ this.log().fail("guardian-safe", error);
7386
+ }
7387
+ }
7101
7388
  };
7102
7389
  function buildSafePrompt(transcript, current) {
7103
7390
  if (transcript.length === 0) return current;
@@ -7119,12 +7406,58 @@ async function defaultRunPluginList(bin, dshProfile) {
7119
7406
  function delay(ms) {
7120
7407
  return new Promise((resolve5) => setTimeout(resolve5, ms));
7121
7408
  }
7409
+ function withTimeout2(promise, ms) {
7410
+ return new Promise((resolve5) => {
7411
+ const timer = setTimeout(() => resolve5(void 0), ms);
7412
+ timer.unref?.();
7413
+ promise.then(
7414
+ (value) => {
7415
+ clearTimeout(timer);
7416
+ resolve5(value);
7417
+ },
7418
+ () => {
7419
+ clearTimeout(timer);
7420
+ resolve5(void 0);
7421
+ }
7422
+ );
7423
+ });
7424
+ }
7425
+ function isRecord4(value) {
7426
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7427
+ }
7428
+ function safeScopeForAction(event) {
7429
+ const raw = isRecord4(event.raw) ? event.raw : void 0;
7430
+ const message = isRecord4(raw?.message) ? raw.message : void 0;
7431
+ const threadId = typeof message?.thread_id === "string" ? message.thread_id : void 0;
7432
+ return threadId ? `${event.chatId}:${threadId}` : event.chatId;
7433
+ }
7434
+ function readDshDefaultModel(home, env) {
7435
+ try {
7436
+ const file = join15(resolveDshHome(home, env), "settings.yaml");
7437
+ const doc = parse(readFileSync4(file, "utf8"));
7438
+ const value = doc?.["agent-default-model"];
7439
+ return typeof value === "string" && value ? value : void 0;
7440
+ } catch {
7441
+ return void 0;
7442
+ }
7443
+ }
7122
7444
  function parseFreshness2(value) {
7123
7445
  const raw = value?.trim();
7124
7446
  if (!raw) return 6e5;
7125
7447
  const parsed = Number(raw);
7126
7448
  return Number.isInteger(parsed) && parsed >= 0 ? parsed : 6e5;
7127
7449
  }
7450
+ function parseSafeAdapterMode(value) {
7451
+ const normalized = value?.trim().toLowerCase();
7452
+ if (normalized === "sdk" || normalized === "headless") return normalized;
7453
+ return "auto";
7454
+ }
7455
+ function parsePositiveInt2(value, fallback) {
7456
+ const raw = value?.trim();
7457
+ if (!raw) return fallback;
7458
+ const parsed = Number(raw);
7459
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
7460
+ }
7128
7461
  function defined(input) {
7129
7462
  const output = {};
7130
7463
  for (const [key, value] of Object.entries(input)) {
@@ -7161,6 +7494,12 @@ async function buildGuardianService(env, overrides = {}) {
7161
7494
  pollMs: env.guardianPollMs,
7162
7495
  staleMs: env.guardianStaleMs,
7163
7496
  engineDeadMs: env.guardianEngineDeadMs,
7497
+ safeAdapterMode: parseSafeAdapterMode(process.env.DSH_LARK_GUARDIAN_SAFE_ADAPTER),
7498
+ safeTimeoutMs: parsePositiveInt2(
7499
+ process.env.DSH_LARK_GUARDIAN_SAFE_TIMEOUT_MS,
7500
+ SAFE_RUN_TIMEOUT_MS
7501
+ ),
7502
+ safeDensity: parseCardDensity(process.env.DSH_LARK_GUARDIAN_CARD_DENSITY) ?? "detailed",
7164
7503
  ...overrides
7165
7504
  });
7166
7505
  }
@@ -7252,7 +7591,7 @@ function waitForShutdown2() {
7252
7591
 
7253
7592
  // src/cli.ts
7254
7593
  function packageVersion() {
7255
- const raw = readFileSync4(new URL("../package.json", import.meta.url), "utf8");
7594
+ const raw = readFileSync5(new URL("../package.json", import.meta.url), "utf8");
7256
7595
  const pkg = JSON.parse(raw);
7257
7596
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
7258
7597
  }