dsh-lark-bot 0.7.0 → 0.8.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
@@ -64,7 +64,7 @@ var init_dsh_compat = __esm({
64
64
  sdkServer: "0.1.0-rc.6",
65
65
  acp: "0.1.0-rc.6",
66
66
  node: ">=22.19.0",
67
- verifiedAt: "2026-08-14"
67
+ verifiedAt: "2026-08-15"
68
68
  };
69
69
  }
70
70
  });
@@ -1498,7 +1498,13 @@ var DEFAULTS = {
1498
1498
  scopeConcurrency: 2,
1499
1499
  retentionMsgs: 40,
1500
1500
  archiveMax: 50,
1501
- archiveMaxAgeDays: 90
1501
+ archiveMaxAgeDays: 90,
1502
+ heartbeatMs: 5e3,
1503
+ guardianPollMs: 2e3,
1504
+ guardianStaleMs: 15e3,
1505
+ guardianEngineDeadMs: 12e4,
1506
+ guardianProfile: "dsh-lark",
1507
+ guardianBridgeProfile: "default"
1502
1508
  };
1503
1509
  function nonEmpty(value) {
1504
1510
  const trimmed = value?.trim();
@@ -1584,6 +1590,15 @@ function parseMinOneInt(value, fallback, name) {
1584
1590
  }
1585
1591
  return parsed;
1586
1592
  }
1593
+ function parsePositiveIntMin(value, fallback, name) {
1594
+ const raw = value?.trim();
1595
+ if (!raw) return fallback;
1596
+ const parsed = Number(raw);
1597
+ if (!Number.isInteger(parsed) || parsed < 1) {
1598
+ throw new Error(`${name} must be a positive integer, got "${raw}"`);
1599
+ }
1600
+ return parsed;
1601
+ }
1587
1602
  function loadRuntimeEnv(source = process.env) {
1588
1603
  const homeOverride = nonEmpty(source.DSH_LARK_HOME);
1589
1604
  const workspace = nonEmpty(source.DSH_LARK_WORKSPACE);
@@ -1634,7 +1649,30 @@ function loadRuntimeEnv(source = process.env) {
1634
1649
  "DSH_LARK_ARCHIVE_MAX_AGE_DAYS"
1635
1650
  ),
1636
1651
  accessDefaultDeny: parseBoolean(source.DSH_LARK_ACCESS_DEFAULT_DENY, false),
1637
- eventFreshnessMs: parseFreshness(source.DSH_LARK_EVENT_FRESHNESS_MS)
1652
+ eventFreshnessMs: parseFreshness(source.DSH_LARK_EVENT_FRESHNESS_MS),
1653
+ heartbeatMs: parsePositiveIntMin(
1654
+ source.DSH_LARK_HEARTBEAT_MS,
1655
+ DEFAULTS.heartbeatMs,
1656
+ "DSH_LARK_HEARTBEAT_MS"
1657
+ ),
1658
+ guardianDisabled: parseBoolean(source.DSH_LARK_GUARDIAN_DISABLED, false),
1659
+ guardianProfile: nonEmpty(source.DSH_LARK_GUARDIAN_PROFILE) ?? DEFAULTS.guardianProfile,
1660
+ guardianBridgeProfile: nonEmpty(source.DSH_LARK_GUARDIAN_BRIDGE_PROFILE) ?? DEFAULTS.guardianBridgeProfile,
1661
+ guardianPollMs: parsePositiveIntMin(
1662
+ source.DSH_LARK_GUARDIAN_POLL_MS,
1663
+ DEFAULTS.guardianPollMs,
1664
+ "DSH_LARK_GUARDIAN_POLL_MS"
1665
+ ),
1666
+ guardianStaleMs: parsePositiveIntMin(
1667
+ source.DSH_LARK_GUARDIAN_STALE_MS,
1668
+ DEFAULTS.guardianStaleMs,
1669
+ "DSH_LARK_GUARDIAN_STALE_MS"
1670
+ ),
1671
+ guardianEngineDeadMs: parsePositiveIntMin(
1672
+ source.DSH_LARK_GUARDIAN_ENGINE_DEAD_MS,
1673
+ DEFAULTS.guardianEngineDeadMs,
1674
+ "DSH_LARK_GUARDIAN_ENGINE_DEAD_MS"
1675
+ )
1638
1676
  };
1639
1677
  }
1640
1678
 
@@ -1660,9 +1698,9 @@ async function writeFileAtomic(target, data, options = {}) {
1660
1698
  }
1661
1699
  }
1662
1700
  async function rmSilently(path) {
1663
- const { rm: rm2 } = await import("fs/promises");
1701
+ const { rm: rm3 } = await import("fs/promises");
1664
1702
  try {
1665
- await rm2(path, { force: true });
1703
+ await rm3(path, { force: true });
1666
1704
  } catch {
1667
1705
  }
1668
1706
  }
@@ -5391,6 +5429,59 @@ var WorkspaceStore = class {
5391
5429
  }
5392
5430
  };
5393
5431
 
5432
+ // src/guardian/heartbeat.ts
5433
+ import { readFile as readFile9 } from "fs/promises";
5434
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 5e3;
5435
+ async function readHeartbeat(file) {
5436
+ try {
5437
+ const raw = await readFile9(file, "utf8");
5438
+ const parsed = JSON.parse(raw);
5439
+ if (typeof parsed.pid !== "number" || typeof parsed.startedAt !== "string" || typeof parsed.ts !== "number") {
5440
+ return void 0;
5441
+ }
5442
+ return {
5443
+ pid: parsed.pid,
5444
+ startedAt: parsed.startedAt,
5445
+ ts: parsed.ts
5446
+ };
5447
+ } catch {
5448
+ return void 0;
5449
+ }
5450
+ }
5451
+ function heartbeatAgeMs(payload, now = Date.now()) {
5452
+ return Math.max(0, now - payload.ts);
5453
+ }
5454
+ function isHeartbeatFresh(payload, maxAgeMs, now = Date.now()) {
5455
+ if (payload === void 0) return false;
5456
+ return heartbeatAgeMs(payload, now) < maxAgeMs;
5457
+ }
5458
+ function startHeartbeat(file, pid, intervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS) {
5459
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
5460
+ let stopped = false;
5461
+ const beat = async () => {
5462
+ if (stopped) return;
5463
+ const payload = { pid, startedAt, ts: Date.now() };
5464
+ try {
5465
+ await writeFileAtomic(file, `${JSON.stringify(payload)}
5466
+ `, {
5467
+ mode: 384
5468
+ });
5469
+ } catch {
5470
+ }
5471
+ };
5472
+ void beat();
5473
+ const timer = setInterval(() => {
5474
+ void beat();
5475
+ }, intervalMs);
5476
+ timer.unref?.();
5477
+ return {
5478
+ stop() {
5479
+ stopped = true;
5480
+ clearInterval(timer);
5481
+ }
5482
+ };
5483
+ }
5484
+
5394
5485
  // src/cli/commands/run.ts
5395
5486
  var DEBOUNCE_MS = 600;
5396
5487
  async function ensureBotProfile(store, options) {
@@ -5605,6 +5696,11 @@ async function startBridgeEngine(options) {
5605
5696
  larkChannel = bridge.channel;
5606
5697
  await notifyServer.start();
5607
5698
  process.env.DSH_LARK_NOTIFY_URL = notifyServer.url ?? "";
5699
+ const heartbeat = startHeartbeat(
5700
+ paths.profilePath(profileName, "guardian", "heartbeat.json"),
5701
+ process.pid,
5702
+ env.heartbeatMs
5703
+ );
5608
5704
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
5609
5705
  log.info("cli", "started", {
5610
5706
  profile: profileName,
@@ -5629,6 +5725,7 @@ async function startBridgeEngine(options) {
5629
5725
  stop: async () => {
5630
5726
  if (stopped) return;
5631
5727
  stopped = true;
5728
+ heartbeat.stop();
5632
5729
  await notifyServer.stop();
5633
5730
  await bridge.disconnect();
5634
5731
  await adapter.dispose?.();
@@ -5672,29 +5769,396 @@ function waitForShutdown() {
5672
5769
  // src/cli/commands/setup.ts
5673
5770
  init_dsh_runtime();
5674
5771
  init_own_package();
5675
- import { spawn as spawn5 } from "child_process";
5676
- import { mkdir as mkdir10, readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
5772
+ import { spawn as spawn6 } from "child_process";
5773
+ import { mkdir as mkdir12, readFile as readFile12, writeFile as writeFile7 } from "fs/promises";
5774
+ import { homedir as homedir7 } from "os";
5775
+ import { join as join13 } from "path";
5776
+
5777
+ // src/guardian/install.ts
5778
+ init_own_package();
5779
+ import { mkdir as mkdir11, readFile as readFile11, rm as rm2, writeFile as writeFile6 } from "fs/promises";
5780
+ import { existsSync as existsSync5 } from "fs";
5677
5781
  import { homedir as homedir6 } from "os";
5678
5782
  import { join as join12 } from "path";
5783
+
5784
+ // src/guardian/process.ts
5785
+ import { spawn as spawn5 } from "child_process";
5786
+ function hasProfileFlag(cmdline, dshProfile) {
5787
+ const pattern = new RegExp(
5788
+ `(?:^|\\s)--profile(?:\\s+|=)${escapeRegExp(dshProfile)}(?:\\s|$)`
5789
+ );
5790
+ return pattern.test(cmdline);
5791
+ }
5792
+ function escapeRegExp(value) {
5793
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5794
+ }
5795
+ function looksLikeDshProcess(cmdline) {
5796
+ return cmdline.includes("@deepseek-ai/dsh") || /(?:^|\s)dsh(?:\.exe)?(?:\s|$)/.test(cmdline);
5797
+ }
5798
+ function matchProfileProcess(cmdline, dshProfile) {
5799
+ return hasProfileFlag(cmdline, dshProfile) && looksLikeDshProcess(cmdline);
5800
+ }
5801
+ async function listProcesses() {
5802
+ if (process.platform === "win32") {
5803
+ return listProcessesWindows();
5804
+ }
5805
+ return listProcessesPosix();
5806
+ }
5807
+ async function listProcessesPosix() {
5808
+ const { stdout } = await captureOutput("ps", ["-axo", "pid=,args="], 1e4);
5809
+ const result = [];
5810
+ for (const line of stdout.split("\n")) {
5811
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
5812
+ if (match) {
5813
+ result.push({ pid: Number(match[1]), cmdline: match[2] ?? "" });
5814
+ }
5815
+ }
5816
+ return result;
5817
+ }
5818
+ async function listProcessesWindows() {
5819
+ const { stdout } = await captureOutput(
5820
+ "powershell.exe",
5821
+ [
5822
+ "-NoProfile",
5823
+ "-NonInteractive",
5824
+ "-Command",
5825
+ "Get-CimInstance Win32_Process | Select-Object ProcessId, CommandLine | ConvertTo-Csv -NoTypeInformation"
5826
+ ],
5827
+ 1e4
5828
+ );
5829
+ const result = [];
5830
+ for (const line of stdout.split("\n").slice(1)) {
5831
+ const [pid, ...rest] = line.trim().split(",");
5832
+ const numeric = Number(pid?.replace(/"/g, ""));
5833
+ if (Number.isInteger(numeric) && rest.length > 0) {
5834
+ result.push({ pid: numeric, cmdline: rest.join(",").replace(/^"|"$/g, "") });
5835
+ }
5836
+ }
5837
+ return result;
5838
+ }
5839
+ async function captureOutput(command, args, timeoutMs = 3e4) {
5840
+ return new Promise((resolve5) => {
5841
+ const child = spawn5(command, [...args], {
5842
+ stdio: ["ignore", "pipe", "pipe"],
5843
+ windowsHide: true
5844
+ });
5845
+ let stdout = "";
5846
+ let stderr = "";
5847
+ child.stdout?.on("data", (chunk) => {
5848
+ stdout += chunk.toString("utf8");
5849
+ });
5850
+ child.stderr?.on("data", (chunk) => {
5851
+ stderr += chunk.toString("utf8");
5852
+ });
5853
+ child.on("error", () => {
5854
+ resolve5({ code: 1, stdout: "", stderr });
5855
+ });
5856
+ child.on("close", (code) => {
5857
+ resolve5({ code: code ?? 1, stdout, stderr });
5858
+ });
5859
+ const timer = setTimeout(() => {
5860
+ child.kill("SIGKILL");
5861
+ resolve5({ code: 1, stdout, stderr });
5862
+ }, timeoutMs);
5863
+ child.once("close", () => clearTimeout(timer));
5864
+ });
5865
+ }
5866
+ async function findProfileProcess(dshProfile) {
5867
+ const processes = await listProcesses();
5868
+ return processes.find((entry) => matchProfileProcess(entry.cmdline, dshProfile));
5869
+ }
5870
+ function spawnDetached(command, args, env = process.env) {
5871
+ const child = spawn5(command, [...args], {
5872
+ detached: process.platform !== "win32",
5873
+ stdio: "ignore",
5874
+ windowsHide: true,
5875
+ env: { ...env }
5876
+ });
5877
+ if (child.pid !== void 0 && process.platform !== "win32") {
5878
+ child.unref();
5879
+ }
5880
+ return { pid: child.pid };
5881
+ }
5882
+
5883
+ // src/guardian/state.ts
5884
+ import { readFile as readFile10 } from "fs/promises";
5885
+ import { dirname as dirname6 } from "path";
5886
+ import { mkdir as mkdir10 } from "fs/promises";
5887
+ var DEFAULT_DSH_PROFILE = "dsh-lark";
5888
+ var DEFAULT_BRIDGE_PROFILE = "default";
5889
+ function defaultSafeProfile(dshProfile) {
5890
+ return `${dshProfile}-safe`;
5891
+ }
5892
+ function newGuardianState(input) {
5893
+ const dshProfile = input.dshProfile ?? DEFAULT_DSH_PROFILE;
5894
+ return {
5895
+ schemaVersion: 1,
5896
+ dshProfile,
5897
+ bridgeProfile: input.bridgeProfile ?? DEFAULT_BRIDGE_PROFILE,
5898
+ safeProfile: defaultSafeProfile(dshProfile),
5899
+ profileSeenUp: false,
5900
+ mode: "standby",
5901
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
5902
+ };
5903
+ }
5904
+ async function loadGuardianState(file, fallback) {
5905
+ try {
5906
+ const raw = await readFile10(file, "utf8");
5907
+ const parsed = JSON.parse(raw);
5908
+ if (parsed.schemaVersion !== 1) return fallback;
5909
+ const dshProfile = typeof parsed.dshProfile === "string" ? parsed.dshProfile : fallback.dshProfile;
5910
+ const bridgeProfile = typeof parsed.bridgeProfile === "string" ? parsed.bridgeProfile : fallback.bridgeProfile;
5911
+ return {
5912
+ schemaVersion: 1,
5913
+ dshProfile,
5914
+ bridgeProfile,
5915
+ safeProfile: typeof parsed.safeProfile === "string" ? parsed.safeProfile : defaultSafeProfile(dshProfile),
5916
+ profileSeenUp: parsed.profileSeenUp === true || fallback.profileSeenUp === true,
5917
+ mode: parsed.mode === "takeover" || parsed.mode === "safe" ? parsed.mode : "standby",
5918
+ relaunchedPid: typeof parsed.relaunchedPid === "number" ? parsed.relaunchedPid : void 0,
5919
+ updatedAt: parsed.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
5920
+ };
5921
+ } catch {
5922
+ return fallback;
5923
+ }
5924
+ }
5925
+ async function saveGuardianState(file, state) {
5926
+ const next = { ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
5927
+ await mkdir10(dirname6(file), { recursive: true });
5928
+ await writeFileAtomic(file, `${JSON.stringify(next, null, 2)}
5929
+ `, { mode: 384 });
5930
+ }
5931
+
5932
+ // src/guardian/install.ts
5933
+ var GUARDIAN_LABEL = "dsh-lark-guardian";
5934
+ function guardianCliEntry() {
5935
+ return join12(ownPackageInfo().root, "dist", "cli.js");
5936
+ }
5937
+ function serviceDirectory(platform, root) {
5938
+ switch (platform) {
5939
+ case "linux":
5940
+ return join12(root, ".config", "systemd", "user");
5941
+ case "darwin":
5942
+ return join12(root, "Library", "LaunchAgents");
5943
+ case "win32":
5944
+ return join12(
5945
+ process.env.APPDATA ?? join12(root, "AppData", "Roaming"),
5946
+ "Microsoft",
5947
+ "Windows",
5948
+ "Start Menu",
5949
+ "Programs",
5950
+ "Startup"
5951
+ );
5952
+ default:
5953
+ return join12(root, ".dsh-lark", "guardian");
5954
+ }
5955
+ }
5956
+ function systemdUnit(nodeBin, cliEntry, env = {}) {
5957
+ const envLines = Object.entries(env).map(([key, value]) => `Environment=${key}=${value}`).join("\n");
5958
+ return [
5959
+ "[Unit]",
5960
+ "Description=dsh-lark-bot safety-net guardian (Feishu rescue when dsh is down)",
5961
+ "After=network-online.target",
5962
+ "Wants=network-online.target",
5963
+ "",
5964
+ "[Service]",
5965
+ "Type=simple",
5966
+ `ExecStart=${nodeBin} ${cliEntry} guardian run`,
5967
+ "Restart=on-failure",
5968
+ "RestartSec=5",
5969
+ ...envLines ? [envLines] : [],
5970
+ "",
5971
+ "[Install]",
5972
+ "WantedBy=default.target",
5973
+ ""
5974
+ ].join("\n");
5975
+ }
5976
+ function launchdPlist(nodeBin, cliEntry, label = `io.dsh-lark.${GUARDIAN_LABEL}`, logPath = "/tmp/dsh-lark-guardian.log") {
5977
+ return [
5978
+ '<?xml version="1.0" encoding="UTF-8"?>',
5979
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
5980
+ '<plist version="1.0">',
5981
+ "<dict>",
5982
+ ` <key>Label</key><string>${label}</string>`,
5983
+ " <key>ProgramArguments</key>",
5984
+ " <array>",
5985
+ ` <string>${nodeBin}</string>`,
5986
+ ` <string>${cliEntry}</string>`,
5987
+ " <string>guardian</string>",
5988
+ " <string>run</string>",
5989
+ " </array>",
5990
+ " <key>RunAtLoad</key><true/>",
5991
+ " <key>KeepAlive</key><true/>",
5992
+ ` <key>StandardOutPath</key><string>${logPath}</string>`,
5993
+ ` <key>StandardErrorPath</key><string>${logPath}</string>`,
5994
+ "</dict>",
5995
+ "</plist>",
5996
+ ""
5997
+ ].join("\n");
5998
+ }
5999
+ function windowsStartupCmd(nodeBin, cliEntry) {
6000
+ return [
6001
+ "@echo off",
6002
+ "REM dsh-lark-bot safety-net guardian - keep the Feishu rescue entrance alive.",
6003
+ `start "" /b "${nodeBin}" "${cliEntry}" guardian run`,
6004
+ ""
6005
+ ].join("\r\n");
6006
+ }
6007
+ function defaultRun(command, args) {
6008
+ return captureOutput(command, args, 3e4);
6009
+ }
6010
+ async function installGuardian(options) {
6011
+ const messages = [];
6012
+ const root = options.rootOverride ?? homedir6();
6013
+ const dshProfile = options.dshProfile ?? options.env.guardianProfile;
6014
+ const bridgeProfile = options.bridgeProfile ?? options.env.guardianBridgeProfile;
6015
+ const stateFile = join12(root, ".dsh-lark", "guardian.json");
6016
+ const fallback = newGuardianState({ dshProfile, bridgeProfile });
6017
+ const existing = await loadGuardianState(stateFile, fallback);
6018
+ const state = {
6019
+ ...existing,
6020
+ dshProfile: existing.dshProfile === dshProfile ? existing.dshProfile : dshProfile,
6021
+ bridgeProfile: existing.bridgeProfile === bridgeProfile ? existing.bridgeProfile : bridgeProfile,
6022
+ safeProfile: existing.safeProfile || `${dshProfile}-safe`
6023
+ };
6024
+ await saveGuardianState(stateFile, state);
6025
+ messages.push(`\u5B88\u62A4\u72B6\u6001\u5DF2\u5199\u5165 ${stateFile}\uFF08dsh profile=${state.dshProfile}\uFF09`);
6026
+ if (options.dryRun) {
6027
+ return { ok: true, messages };
6028
+ }
6029
+ const nodeBin = process.execPath;
6030
+ const cliEntry = guardianCliEntry();
6031
+ const directory = serviceDirectory(process.platform, root);
6032
+ await mkdir11(directory, { recursive: true });
6033
+ if (process.platform === "linux") {
6034
+ const unitPath = join12(directory, `${GUARDIAN_LABEL}.service`);
6035
+ await writeFile6(
6036
+ unitPath,
6037
+ systemdUnit(nodeBin, cliEntry, { DSH_LARK_GUARDIAN_DISABLED: "0" }),
6038
+ "utf8"
6039
+ );
6040
+ messages.push(`systemd user unit \u5DF2\u5199\u5165 ${unitPath}`);
6041
+ const run = options.run ?? defaultRun;
6042
+ const reload = await run("systemctl", ["--user", "daemon-reload"]);
6043
+ const enable = await run("systemctl", ["--user", "enable", "--now", `${GUARDIAN_LABEL}.service`]);
6044
+ if (reload.code === 0 && enable.code === 0) {
6045
+ messages.push("\u5B88\u62A4\u670D\u52A1\u5DF2\u542F\u7528\u5E76\u542F\u52A8\uFF08systemctl --user\uFF09\u3002");
6046
+ } else {
6047
+ messages.push(
6048
+ "\u672A\u80FD\u81EA\u52A8\u542F\u7528\u670D\u52A1\uFF08\u53EF\u80FD\u9700\u8981\u56FE\u5F62\u4F1A\u8BDD / systemd user \u5B9E\u4F8B\uFF09\u3002\u8BF7\u624B\u52A8\u6267\u884C\uFF1A"
6049
+ );
6050
+ messages.push(` systemctl --user daemon-reload && systemctl --user enable --now ${GUARDIAN_LABEL}.service`);
6051
+ return { ok: false, messages };
6052
+ }
6053
+ } else if (process.platform === "darwin") {
6054
+ const label = `io.dsh-lark.${GUARDIAN_LABEL}`;
6055
+ const plistPath = join12(directory, `${label}.plist`);
6056
+ await writeFile6(plistPath, launchdPlist(nodeBin, cliEntry), "utf8");
6057
+ messages.push(`launchd LaunchAgent \u5DF2\u5199\u5165 ${plistPath}`);
6058
+ const run = options.run ?? defaultRun;
6059
+ const uid = typeof options.userOverride === "string" ? options.userOverride : String(process.getuid?.() ?? 501);
6060
+ const bootstrap = await run("launchctl", ["bootstrap", `gui/${uid}`, plistPath]);
6061
+ if (bootstrap.code === 0) {
6062
+ messages.push("\u5B88\u62A4\u670D\u52A1\u5DF2\u52A0\u8F7D\uFF08launchctl bootstrap\uFF09\u3002");
6063
+ } else {
6064
+ messages.push(
6065
+ "\u672A\u80FD\u81EA\u52A8\u52A0\u8F7D LaunchAgent\u3002\u8BF7\u624B\u52A8\u6267\u884C\uFF08\u6216\u6539\u7528\u65E7\u7248 launchctl load -w\uFF09\uFF1A"
6066
+ );
6067
+ messages.push(` launchctl bootstrap gui/${uid} ${plistPath}`);
6068
+ return { ok: false, messages };
6069
+ }
6070
+ } else if (process.platform === "win32") {
6071
+ const cmdPath = join12(directory, `${GUARDIAN_LABEL}.cmd`);
6072
+ await writeFile6(cmdPath, windowsStartupCmd(nodeBin, cliEntry), "utf8");
6073
+ messages.push(`Windows \u542F\u52A8\u9879\u5DF2\u5199\u5165 ${cmdPath}\uFF08\u767B\u5F55\u540E\u81EA\u52A8\u5E38\u9A7B\uFF09\u3002`);
6074
+ } else {
6075
+ const fallbackPath = join12(directory, `${GUARDIAN_LABEL}.sh`);
6076
+ await writeFile6(
6077
+ fallbackPath,
6078
+ `#!/bin/sh
6079
+ exec "${nodeBin}" "${cliEntry}" guardian run
6080
+ `,
6081
+ "utf8"
6082
+ );
6083
+ messages.push(
6084
+ `\u5F53\u524D\u5E73\u53F0\u4E0D\u652F\u6301\u81EA\u52A8\u6CE8\u518C\u670D\u52A1\uFF1B\u5DF2\u5199\u5165\u542F\u52A8\u811A\u672C ${fallbackPath}\uFF0C\u8BF7\u6309\u9700\u914D\u7F6E\u5E38\u9A7B\u3002`
6085
+ );
6086
+ }
6087
+ return { ok: true, messages };
6088
+ }
6089
+ async function uninstallGuardian(options) {
6090
+ const messages = [];
6091
+ const root = options.rootOverride ?? homedir6();
6092
+ const run = options.run ?? defaultRun;
6093
+ if (process.platform === "linux") {
6094
+ await run("systemctl", ["--user", "disable", "--now", `${GUARDIAN_LABEL}.service`]);
6095
+ const unitPath = join12(root, ".config", "systemd", "user", `${GUARDIAN_LABEL}.service`);
6096
+ await rm2(unitPath, { force: true });
6097
+ await run("systemctl", ["--user", "daemon-reload"]);
6098
+ messages.push("systemd user unit \u5DF2\u79FB\u9664\u5E76\u505C\u6B62\u3002");
6099
+ } else if (process.platform === "darwin") {
6100
+ const label = `io.dsh-lark.${GUARDIAN_LABEL}`;
6101
+ const plistPath = join12(root, "Library", "LaunchAgents", `${label}.plist`);
6102
+ const uid = typeof options.userOverride === "string" ? options.userOverride : String(process.getuid?.() ?? 501);
6103
+ await run("launchctl", ["bootout", `gui/${uid}`, plistPath]);
6104
+ await rm2(plistPath, { force: true });
6105
+ messages.push("launchd LaunchAgent \u5DF2\u79FB\u9664\u3002");
6106
+ } else if (process.platform === "win32") {
6107
+ const startupDir = join12(
6108
+ process.env.APPDATA ?? join12(root, "AppData", "Roaming"),
6109
+ "Microsoft",
6110
+ "Windows",
6111
+ "Start Menu",
6112
+ "Programs",
6113
+ "Startup"
6114
+ );
6115
+ await rm2(join12(startupDir, `${GUARDIAN_LABEL}.cmd`), { force: true });
6116
+ messages.push("Windows \u542F\u52A8\u9879\u5DF2\u79FB\u9664\u3002");
6117
+ } else {
6118
+ const fallbackPath = join12(root, ".dsh-lark", "guardian", `${GUARDIAN_LABEL}.sh`);
6119
+ await rm2(fallbackPath, { force: true });
6120
+ messages.push("\u542F\u52A8\u811A\u672C\u5DF2\u79FB\u9664\u3002");
6121
+ }
6122
+ messages.push("\u5B88\u62A4\u72B6\u6001\u6587\u4EF6\uFF08~/.dsh-lark/guardian.json\uFF09\u5DF2\u4FDD\u7559\uFF0C\u53EF\u968F\u65F6\u91CD\u65B0\u5B89\u88C5\u3002");
6123
+ return { ok: true, messages };
6124
+ }
6125
+
6126
+ // src/cli/commands/setup.ts
5679
6127
  async function runSetup(options = {}) {
5680
6128
  const profile = options.profile ?? "dsh-lark";
5681
6129
  const own = ownPackageInfo();
5682
6130
  const packageSpec = options.packageSpec ?? process.env.DSH_LARK_SETUP_PACKAGE ?? own.name;
5683
- const dshHome = options.dshHome ?? resolveDshHome(homedir6(), process.env);
5684
- const bin = options.bin ?? discoverDshBin(homedir6(), process.env);
6131
+ const dshHome = options.dshHome ?? resolveDshHome(homedir7(), process.env);
6132
+ const bin = options.bin ?? discoverDshBin(homedir7(), process.env);
5685
6133
  if (!bin) {
5686
6134
  throw new Error(
5687
6135
  "\u672A\u627E\u5230\u672C\u673A dsh \u5B89\u88C5\uFF08`@deepseek-ai/dsh`\uFF09\u3002\u8BF7\u5148\u5B89\u88C5 DeepSeek Harness\uFF0C\u518D\u8FD0\u884C\u672C\u547D\u4EE4\u3002"
5688
6136
  );
5689
6137
  }
5690
- const profileDir = join12(dshHome, "profiles", profile);
5691
- await mkdir10(profileDir, { recursive: true });
6138
+ const profileDir = join13(dshHome, "profiles", profile);
6139
+ await mkdir12(profileDir, { recursive: true });
5692
6140
  await approveBuilds(profileDir);
5693
6141
  process.stdout.write(
5694
6142
  `\u6B63\u5728\u628A ${packageSpec} \u5B89\u88C5\u5230 dsh profile \`${profile}\`\uFF08$DSH_HOME=${dshHome}\uFF09...
5695
6143
  `
5696
6144
  );
5697
6145
  await runDshPlugin(bin, profile, packageSpec);
6146
+ if (options.guardian) {
6147
+ const install = options.installGuardianFn ?? installGuardian;
6148
+ const guardianResult = await install({
6149
+ env: loadRuntimeEnv(process.env),
6150
+ dshProfile: profile
6151
+ });
6152
+ for (const message of guardianResult.messages) {
6153
+ process.stdout.write(`${message}
6154
+ `);
6155
+ }
6156
+ if (!guardianResult.ok) {
6157
+ process.stdout.write(
6158
+ "\n\u6CE8\u610F\uFF1Aguardian \u670D\u52A1\u672A\u5B8C\u5168\u542F\u7528\uFF0C\u8BF7\u6309\u4E0A\u65B9\u63D0\u793A\u624B\u52A8\u6267\u884C\uFF1B\u4E5F\u53EF\u7A0D\u540E\u91CD\u8DD1 `dsh-lark-bot guardian install`\u3002\n"
6159
+ );
6160
+ }
6161
+ }
5698
6162
  process.stdout.write(
5699
6163
  [
5700
6164
  "",
@@ -5704,13 +6168,21 @@ async function runSetup(options = {}) {
5704
6168
  "",
5705
6169
  "\u9996\u6B21\u542F\u52A8\u4F1A\u5728\u7EC8\u7AEF\u6253\u5370\u4E8C\u7EF4\u7801\uFF0C\u7528\u98DE\u4E66 / Lark \u626B\u7801\u5B8C\u6210\u4E00\u6B21\u6027\u7ED1\u5B9A\uFF1B",
5706
6170
  "\u7ED1\u5B9A\u540E dsh \u5373\u4EE5\u6807\u51C6\u63D2\u4EF6\u65B9\u5F0F\u52A0\u8F7D dsh-lark-bot \u7684\u6865\u63A5\u5F15\u64CE\u3002",
6171
+ ...options.guardian ? [
6172
+ "",
6173
+ "\u5B89\u5168\u7F51\u5B88\u62A4\u5DF2\u5B89\u88C5\uFF1Adsh \u4E0B\u7EBF\u540E\u4ECD\u53EF\u901A\u8FC7\u98DE\u4E66\u53D1\u9001 /safemode \u8FDB\u5165\u4EC5\u6838\u5FC3\u5B89\u5168\u6A21\u5F0F\u81EA\u6551\u3002",
6174
+ "\u67E5\u770B\u72B6\u6001\uFF1Adsh-lark-bot guardian status"
6175
+ ] : [
6176
+ "",
6177
+ "\u63D0\u793A\uFF1A\u53EF\u52A0 --guardian \u540C\u65F6\u5B89\u88C5\u300C\u5B89\u5168\u7F51\u5B88\u62A4\u300D\uFF08dsh \u5168\u90E8\u4E0B\u7EBF\u540E\u7684\u98DE\u4E66\u6551\u63F4\u901A\u9053\uFF09\u3002"
6178
+ ],
5707
6179
  ""
5708
6180
  ].join("\n")
5709
6181
  );
5710
6182
  }
5711
6183
  function runDshPlugin(bin, profile, packageSpec) {
5712
6184
  return new Promise((resolve5, reject) => {
5713
- const child = spawn5(
6185
+ const child = spawn6(
5714
6186
  "node",
5715
6187
  [bin, "plugin", "--profile", profile, "add", packageSpec],
5716
6188
  { stdio: "inherit" }
@@ -5723,10 +6195,10 @@ function runDshPlugin(bin, profile, packageSpec) {
5723
6195
  });
5724
6196
  }
5725
6197
  async function approveBuilds(profileDir) {
5726
- const workspaceFile = join12(profileDir, "pnpm-workspace.yaml");
6198
+ const workspaceFile = join13(profileDir, "pnpm-workspace.yaml");
5727
6199
  let existing = "";
5728
6200
  try {
5729
- existing = await readFile9(workspaceFile, "utf8");
6201
+ existing = await readFile12(workspaceFile, "utf8");
5730
6202
  } catch {
5731
6203
  }
5732
6204
  const allowBuilds = [
@@ -5745,7 +6217,876 @@ allowBuilds:
5745
6217
  existing = existing.replace(/(allowBuilds:\s*\n)/, `$1 ${allowBuilds}
5746
6218
  `);
5747
6219
  }
5748
- await writeFile6(workspaceFile, existing, "utf8");
6220
+ await writeFile7(workspaceFile, existing, "utf8");
6221
+ }
6222
+
6223
+ // src/cli/commands/guardian.ts
6224
+ import { join as join16 } from "path";
6225
+
6226
+ // src/guardian/service.ts
6227
+ import { randomUUID as randomUUID5 } from "crypto";
6228
+ import { homedir as homedir8 } from "os";
6229
+ import { join as join15 } from "path";
6230
+ import { createLarkChannel as createLarkChannel2 } from "@larksuite/channel";
6231
+ init_dsh_runtime();
6232
+
6233
+ // src/guardian/control.ts
6234
+ var COMMAND_RE = /^\/(safemode)(?:\s+(.+))?$/i;
6235
+ function parseGuardianCommand(text) {
6236
+ const trimmed = text.trim();
6237
+ const match = COMMAND_RE.exec(trimmed);
6238
+ if (!match) return void 0;
6239
+ const argument = match[2]?.trim().toLowerCase() ?? "";
6240
+ switch (argument) {
6241
+ case "":
6242
+ case "start":
6243
+ case "enter":
6244
+ return { kind: "safemode", argument };
6245
+ case "status":
6246
+ return { kind: "safemode-status", argument };
6247
+ case "plugins":
6248
+ case "list":
6249
+ return { kind: "safemode-plugins", argument };
6250
+ case "exit":
6251
+ case "quit":
6252
+ case "leave":
6253
+ case "restore":
6254
+ return { kind: "safemode-exit", argument };
6255
+ case "help":
6256
+ case "?":
6257
+ return { kind: "safemode-help", argument };
6258
+ default:
6259
+ return { kind: "safemode-help", argument };
6260
+ }
6261
+ }
6262
+ var SAFEMODE_HELP = [
6263
+ "/safemode \u2014 \u8FDB\u5165\u4EC5\u6838\u5FC3\u5B89\u5168\u6A21\u5F0F\uFF08dsh \u4E3B\u6838\u5FC3 + \u5B98\u65B9 headless\uFF0C\u4E0D\u52A0\u8F7D\u4EFB\u4F55\u7B2C\u4E09\u65B9\u63D2\u4EF6\uFF09",
6264
+ "/safemode status \u2014 \u67E5\u770B\u5B88\u62A4\u72B6\u6001",
6265
+ "/safemode plugins \u2014 \u5217\u51FA\u6545\u969C profile \u5DF2\u5B89\u88C5\u7684\u63D2\u4EF6",
6266
+ "/safemode exit \u2014 \u9000\u51FA\u5B89\u5168\u6A21\u5F0F\uFF0C\u91CD\u542F\u5B8C\u6574 profile \u5E76\u4EA4\u8FD8\u98DE\u4E66\u901A\u9053",
6267
+ "",
6268
+ "\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"
6269
+ ].join("\n");
6270
+ var SAFEMODE_HELP_EN = [
6271
+ "/safemode \u2014 enter core-only safe mode (dsh core + official headless, no third-party plugins)",
6272
+ "/safemode status \u2014 show guardian state",
6273
+ "/safemode plugins \u2014 list plugins installed into the broken profile",
6274
+ "/safemode exit \u2014 leave safe mode, relaunch the full profile and hand the channel back",
6275
+ "",
6276
+ "In safe mode, send normal messages to converse with the dsh core for self-healing."
6277
+ ].join("\n");
6278
+ function safemodeHelpText() {
6279
+ return `${SAFEMODE_HELP}
6280
+
6281
+ ${SAFEMODE_HELP_EN}`;
6282
+ }
6283
+
6284
+ // src/guardian/safe-profile.ts
6285
+ init_dsh_runtime();
6286
+ import { mkdir as mkdir13, writeFile as writeFile8 } from "fs/promises";
6287
+ import { existsSync as existsSync6 } from "fs";
6288
+ import { join as join14 } from "path";
6289
+ var SAFE_CORE_BUNDLES = [
6290
+ "@deepseek-ai/dsh-base",
6291
+ "@deepseek-ai/dsh-headless"
6292
+ ];
6293
+ function safeProfileName(dshProfile) {
6294
+ return `${dshProfile}-safe`;
6295
+ }
6296
+ function safeProfileRoot(home, dshProfile, env) {
6297
+ return join14(resolveDshHome(home, env), "profiles", safeProfileName(dshProfile));
6298
+ }
6299
+ async function ensureSafeProfile(options) {
6300
+ const root = safeProfileRoot(options.home, options.dshProfile, options.env);
6301
+ const name = safeProfileName(options.dshProfile);
6302
+ await mkdir13(root, { recursive: true });
6303
+ let created = false;
6304
+ const manifest = join14(root, "package.json");
6305
+ if (!existsSync6(manifest)) {
6306
+ await writeFile8(
6307
+ manifest,
6308
+ `${JSON.stringify(
6309
+ {
6310
+ name: `dsh-profile-${name}`,
6311
+ private: true,
6312
+ dependencies: {},
6313
+ dsh: {
6314
+ profile: {
6315
+ bundles: [...SAFE_CORE_BUNDLES]
6316
+ }
6317
+ }
6318
+ },
6319
+ null,
6320
+ 2
6321
+ )}
6322
+ `,
6323
+ "utf8"
6324
+ );
6325
+ created = true;
6326
+ }
6327
+ const rootConfig = join14(root, "cordis.yml");
6328
+ if (!existsSync6(rootConfig)) {
6329
+ await writeFile8(
6330
+ rootConfig,
6331
+ "# dsh profile root - an empty entry list; the tree is composed as patches.\n[]\n",
6332
+ "utf8"
6333
+ );
6334
+ created = true;
6335
+ }
6336
+ const userPatch = join14(root, "cordis.patch.yml");
6337
+ if (!existsSync6(userPatch)) {
6338
+ await writeFile8(userPatch, "[]\n", "utf8");
6339
+ created = true;
6340
+ }
6341
+ const workspace = join14(root, "pnpm-workspace.yaml");
6342
+ if (!existsSync6(workspace)) {
6343
+ await writeFile8(
6344
+ workspace,
6345
+ [
6346
+ "packages:",
6347
+ " - .",
6348
+ "",
6349
+ "",
6350
+ "nodeLinker: hoisted",
6351
+ "autoInstallPeers: false",
6352
+ ""
6353
+ ].join("\n"),
6354
+ "utf8"
6355
+ );
6356
+ created = true;
6357
+ }
6358
+ return { root, created };
6359
+ }
6360
+ async function defaultRun2(command, args) {
6361
+ const { spawn: spawn7 } = await import("child_process");
6362
+ return new Promise((resolve5) => {
6363
+ const child = spawn7(command, [...args], { stdio: ["ignore", "pipe", "pipe"] });
6364
+ let stdout = "";
6365
+ let stderr = "";
6366
+ child.stdout?.on("data", (chunk) => {
6367
+ stdout += chunk.toString("utf8");
6368
+ });
6369
+ child.stderr?.on("data", (chunk) => {
6370
+ stderr += chunk.toString("utf8");
6371
+ });
6372
+ child.on("error", (error) => {
6373
+ resolve5({ code: 1, stdout, stderr: `${error.message}
6374
+ ` });
6375
+ });
6376
+ child.on("close", (code) => {
6377
+ resolve5({ code: code ?? 1, stdout, stderr });
6378
+ });
6379
+ });
6380
+ }
6381
+ async function probeSafeProfile(options) {
6382
+ const run = options.run ?? defaultRun2;
6383
+ const profile = safeProfileName(options.dshProfile);
6384
+ const result = await run(options.bin, ["--profile", profile, "--dump-config"]);
6385
+ if (result.code === 0) {
6386
+ return { ok: true, stdout: result.stdout, stderr: result.stderr };
6387
+ }
6388
+ const tail = result.stderr.trim().split("\n").slice(-5).join("\n");
6389
+ return {
6390
+ ok: false,
6391
+ stdout: result.stdout,
6392
+ stderr: result.stderr,
6393
+ error: tail || `dsh --profile ${profile} --dump-config exited with code ${result.code}`
6394
+ };
6395
+ }
6396
+
6397
+ // src/guardian/service.ts
6398
+ var MAX_TRANSCRIPT_ENTRIES = 30;
6399
+ var SAFE_RUN_TIMEOUT_MS = 10 * 6e4;
6400
+ var GuardianService = class {
6401
+ options;
6402
+ state;
6403
+ channel;
6404
+ safeAdapter;
6405
+ transcripts = /* @__PURE__ */ new Map();
6406
+ downStreak = 0;
6407
+ lastHeartbeatFreshAt;
6408
+ timer;
6409
+ ticking = false;
6410
+ stopped = false;
6411
+ dshBin;
6412
+ config;
6413
+ eventFreshnessMs;
6414
+ activeMessages = /* @__PURE__ */ new Set();
6415
+ constructor(options) {
6416
+ this.options = {
6417
+ pollMs: options.pollMs ?? 2e3,
6418
+ staleMs: options.staleMs ?? 15e3,
6419
+ engineDeadMs: options.engineDeadMs ?? 12e4,
6420
+ takeoverGracePolls: options.takeoverGracePolls ?? 2,
6421
+ sendDelayMs: options.sendDelayMs ?? 600,
6422
+ stateFile: options.stateFile,
6423
+ configFile: options.configFile,
6424
+ heartbeatFile: options.heartbeatFile,
6425
+ home: options.home,
6426
+ dshProfile: options.dshProfile,
6427
+ bridgeProfile: options.bridgeProfile,
6428
+ safeProfile: options.safeProfile,
6429
+ ...defined({
6430
+ env: options.env,
6431
+ createChannel: options.createChannel,
6432
+ adapter: options.adapter,
6433
+ findProcess: options.findProcess,
6434
+ spawnDetachedFn: options.spawnDetachedFn,
6435
+ probeSafeProfileFn: options.probeSafeProfileFn,
6436
+ runPluginList: options.runPluginList,
6437
+ dshBin: options.dshBin,
6438
+ saveState: options.saveState,
6439
+ logger: options.logger,
6440
+ now: options.now
6441
+ })
6442
+ };
6443
+ this.state = newGuardianState({
6444
+ dshProfile: options.dshProfile,
6445
+ bridgeProfile: options.bridgeProfile
6446
+ });
6447
+ this.eventFreshnessMs = parseFreshness2(this.options.env?.DSH_LARK_EVENT_FRESHNESS_MS);
6448
+ }
6449
+ get mode() {
6450
+ return this.state.mode;
6451
+ }
6452
+ async start() {
6453
+ const loaded = await loadGuardianState(this.options.stateFile, this.state);
6454
+ if (loaded.dshProfile !== this.options.dshProfile) {
6455
+ this.state = loaded;
6456
+ } else {
6457
+ this.state = loaded;
6458
+ }
6459
+ await this.loadContext();
6460
+ await this.save();
6461
+ await this.tick();
6462
+ this.timer = setInterval(() => {
6463
+ void this.tick();
6464
+ }, this.options.pollMs);
6465
+ this.timer.unref?.();
6466
+ this.log().info("guardian", "started", {
6467
+ dshProfile: this.state.dshProfile,
6468
+ bridgeProfile: this.state.bridgeProfile,
6469
+ mode: this.state.mode,
6470
+ pollMs: this.options.pollMs
6471
+ });
6472
+ }
6473
+ async stop() {
6474
+ if (this.stopped) return;
6475
+ this.stopped = true;
6476
+ if (this.timer) clearInterval(this.timer);
6477
+ this.timer = void 0;
6478
+ await Promise.allSettled([...this.activeMessages]);
6479
+ await this.disconnectChannel();
6480
+ await this.safeAdapter?.dispose?.();
6481
+ this.log().info("guardian", "stopped", {});
6482
+ }
6483
+ snapshot() {
6484
+ return {
6485
+ mode: this.state.mode,
6486
+ dshProfile: this.state.dshProfile,
6487
+ bridgeProfile: this.state.bridgeProfile,
6488
+ safeProfile: this.state.safeProfile,
6489
+ profileSeenUp: this.state.profileSeenUp,
6490
+ dshUp: this.dshUp,
6491
+ heartbeatAgeMs: this.lastHeartbeatAgeMs,
6492
+ channelConnected: this.channel !== void 0,
6493
+ pid: process.pid,
6494
+ dshBin: this.dshBin,
6495
+ relaunchedPid: this.state.relaunchedPid,
6496
+ updatedAt: this.state.updatedAt
6497
+ };
6498
+ }
6499
+ dshUp = false;
6500
+ lastHeartbeatAgeMs;
6501
+ log() {
6502
+ return this.options.logger ?? log;
6503
+ }
6504
+ async loadContext() {
6505
+ const store = new ConfigStore(this.options.configFile);
6506
+ await store.load();
6507
+ this.config = store.getProfile(this.state.bridgeProfile);
6508
+ this.dshBin = this.options.dshBin ?? discoverDshBin(this.options.home, this.options.env ?? process.env);
6509
+ }
6510
+ async save() {
6511
+ if (this.options.saveState) {
6512
+ await this.options.saveState(this.state);
6513
+ return;
6514
+ }
6515
+ await saveGuardianState(this.options.stateFile, this.state);
6516
+ }
6517
+ async setMode(mode) {
6518
+ if (this.state.mode === mode) return;
6519
+ this.state.mode = mode;
6520
+ await this.save();
6521
+ }
6522
+ async setSeenUp() {
6523
+ if (this.state.profileSeenUp) return;
6524
+ this.state.profileSeenUp = true;
6525
+ await this.save();
6526
+ }
6527
+ async tick() {
6528
+ if (this.stopped || this.ticking) return;
6529
+ this.ticking = true;
6530
+ try {
6531
+ const heartbeat = await readHeartbeat(this.options.heartbeatFile);
6532
+ const now = (this.options.now ?? Date.now)();
6533
+ const heartbeatFresh = isHeartbeatFresh(heartbeat, this.options.staleMs, now);
6534
+ this.lastHeartbeatAgeMs = heartbeat ? heartbeatAgeMs(heartbeat, now) : void 0;
6535
+ if (heartbeatFresh) this.lastHeartbeatFreshAt = now;
6536
+ const processFound = await (this.options.findProcess ?? findProfileProcess)(
6537
+ this.state.dshProfile
6538
+ );
6539
+ const processAlive = processFound !== void 0;
6540
+ const engineDead = this.lastHeartbeatFreshAt !== void 0 && now - this.lastHeartbeatFreshAt > this.options.engineDeadMs;
6541
+ const up = heartbeatFresh || processAlive && !engineDead;
6542
+ this.dshUp = up;
6543
+ if (up) {
6544
+ this.downStreak = 0;
6545
+ await this.setSeenUp();
6546
+ if (this.state.mode !== "standby" || this.channel !== void 0) {
6547
+ await this.disconnectChannel();
6548
+ this.transcripts.clear();
6549
+ await this.setMode("standby");
6550
+ }
6551
+ return;
6552
+ }
6553
+ if (!this.state.profileSeenUp) return;
6554
+ this.downStreak += 1;
6555
+ if (this.downStreak < this.options.takeoverGracePolls) return;
6556
+ await this.ensureChannel();
6557
+ } catch (error) {
6558
+ this.log().fail("guardian", error);
6559
+ } finally {
6560
+ this.ticking = false;
6561
+ }
6562
+ }
6563
+ async ensureChannel() {
6564
+ if (this.channel !== void 0) return;
6565
+ const config = this.config ?? await this.reloadConfig();
6566
+ if (!config) {
6567
+ this.log().warn("guardian", "no-bridge-profile", {
6568
+ bridgeProfile: this.state.bridgeProfile
6569
+ });
6570
+ return;
6571
+ }
6572
+ const create = this.options.createChannel ?? createLarkChannel2;
6573
+ const channel = create({
6574
+ appId: config.accounts.appId,
6575
+ appSecret: config.accounts.appSecret,
6576
+ domain: config.tenant === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn",
6577
+ source: "dsh-lark-bot-guardian",
6578
+ policy: {
6579
+ dmMode: config.access.allowedUsers.length ? "allowlist" : "open",
6580
+ requireMention: true,
6581
+ respondToMentionAll: false,
6582
+ ...config.access.allowedUsers.length ? { dmAllowlist: config.access.allowedUsers } : {},
6583
+ ...config.access.allowedChats.length ? { groupAllowlist: config.access.allowedChats } : {}
6584
+ },
6585
+ safety: {
6586
+ chatQueue: { enabled: false }
6587
+ },
6588
+ outbound: {
6589
+ streamThrottleMs: 400
6590
+ },
6591
+ includeRawEvent: true,
6592
+ resolveChatMode: true,
6593
+ handshakeTimeoutMs: 8e3,
6594
+ httpTimeoutMs: 3e4,
6595
+ respectProxyEnv: true
6596
+ });
6597
+ channel.on({
6598
+ message: (msg) => this.track(this.handleMessage(msg)),
6599
+ error: (error) => {
6600
+ this.log().fail("guardian-channel", error);
6601
+ },
6602
+ reconnecting: () => {
6603
+ this.log().warn("guardian-channel", "reconnecting", {});
6604
+ },
6605
+ reconnected: () => {
6606
+ this.log().info("guardian-channel", "reconnected", {});
6607
+ }
6608
+ });
6609
+ try {
6610
+ await channel.connect();
6611
+ this.channel = channel;
6612
+ if (this.state.mode !== "safe") await this.setMode("takeover");
6613
+ this.log().info("guardian", "channel-taken-over", {
6614
+ dshProfile: this.state.dshProfile
6615
+ });
6616
+ } catch (error) {
6617
+ this.log().fail("guardian-channel", error);
6618
+ }
6619
+ }
6620
+ async reloadConfig() {
6621
+ await this.loadContext();
6622
+ return this.config;
6623
+ }
6624
+ async disconnectChannel() {
6625
+ const channel = this.channel;
6626
+ this.channel = void 0;
6627
+ if (!channel) return;
6628
+ try {
6629
+ await channel.disconnect();
6630
+ } catch (error) {
6631
+ this.log().fail("guardian-channel", error);
6632
+ }
6633
+ if (this.state.mode !== "standby") {
6634
+ await this.setMode("standby");
6635
+ }
6636
+ }
6637
+ scopeFor(msg) {
6638
+ if (msg.chatMode === "topic" && msg.threadId) return `${msg.chatId}:${msg.threadId}`;
6639
+ return msg.chatId;
6640
+ }
6641
+ authorized(msg) {
6642
+ const access3 = this.config?.access;
6643
+ if (!access3) return false;
6644
+ const controllers = access3.admins.length > 0 ? access3.admins : access3.allowedUsers;
6645
+ return controllers.includes(msg.senderId);
6646
+ }
6647
+ async sendMarkdown(chatId, markdown2, replyTo) {
6648
+ if (!this.channel) return;
6649
+ try {
6650
+ await this.channel.send(
6651
+ chatId,
6652
+ { markdown: markdown2 },
6653
+ replyTo ? { replyTo } : void 0
6654
+ );
6655
+ } catch (error) {
6656
+ this.log().fail("guardian-send", error);
6657
+ }
6658
+ }
6659
+ async handleMessage(msg) {
6660
+ if (!this.authorized(msg)) {
6661
+ this.log().warn("guardian", "unauthorized-message-dropped", {
6662
+ senderId: msg.senderId,
6663
+ scope: this.scopeFor(msg)
6664
+ });
6665
+ return;
6666
+ }
6667
+ if (this.eventFreshnessMs > 0 && !isEventFresh(msg.createTime, this.eventFreshnessMs)) {
6668
+ this.log().warn("guardian", "stale-message-dropped", {
6669
+ ageMs: Date.now() - msg.createTime
6670
+ });
6671
+ return;
6672
+ }
6673
+ const scope = this.scopeFor(msg);
6674
+ const control = parseGuardianCommand(msg.content);
6675
+ if (control) {
6676
+ await this.handleControl(control.kind, msg);
6677
+ return;
6678
+ }
6679
+ if (this.state.mode === "safe") {
6680
+ await this.runSafeTask(scope, msg);
6681
+ return;
6682
+ }
6683
+ await this.sendMarkdown(
6684
+ msg.chatId,
6685
+ [
6686
+ "dsh \u672A\u5728\u8FD0\u884C\uFF0C\u5B88\u62A4\u8FDB\u7A0B\u5DF2\u63A5\u7BA1\u98DE\u4E66\u901A\u9053\u3002",
6687
+ "",
6688
+ "\u53D1\u9001 `/safemode` \u8FDB\u5165\u4EC5\u6838\u5FC3\u5B89\u5168\u6A21\u5F0F\uFF08dsh \u4E3B\u6838\u5FC3 + \u5B98\u65B9 headless\uFF0C\u4E0D\u52A0\u8F7D\u4EFB\u4F55\u7B2C\u4E09\u65B9\u63D2\u4EF6\uFF09\uFF0C",
6689
+ "\u6216\u53D1\u9001 `/safemode status` \u67E5\u770B\u72B6\u6001\u3002"
6690
+ ].join("\n"),
6691
+ msg.messageId
6692
+ );
6693
+ }
6694
+ track(promise) {
6695
+ this.activeMessages.add(promise);
6696
+ void promise.finally(() => {
6697
+ this.activeMessages.delete(promise);
6698
+ });
6699
+ return promise;
6700
+ }
6701
+ async handleControl(kind, msg) {
6702
+ switch (kind) {
6703
+ case "safemode":
6704
+ await this.enterSafeMode(msg);
6705
+ return;
6706
+ case "safemode-status":
6707
+ await this.sendStatus(msg);
6708
+ return;
6709
+ case "safemode-plugins":
6710
+ await this.sendPluginList(msg);
6711
+ return;
6712
+ case "safemode-exit":
6713
+ await this.exitSafeMode(msg);
6714
+ return;
6715
+ case "safemode-help":
6716
+ await this.sendMarkdown(msg.chatId, safemodeHelpText(), msg.messageId);
6717
+ return;
6718
+ }
6719
+ }
6720
+ async enterSafeMode(msg) {
6721
+ if (this.state.mode === "safe") {
6722
+ await this.sendMarkdown(
6723
+ msg.chatId,
6724
+ "\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",
6725
+ msg.messageId
6726
+ );
6727
+ return;
6728
+ }
6729
+ await this.sendMarkdown(
6730
+ msg.chatId,
6731
+ "\u6B63\u5728\u8FDB\u5165\u5B89\u5168\u6A21\u5F0F\uFF1A\u4EC5\u6302\u8F7D dsh \u4E3B\u6838\u5FC3\uFF08`dsh-base` + `dsh-headless`\uFF09\uFF0C\u4E0D\u52A0\u8F7D\u4EFB\u4F55\u7B2C\u4E09\u65B9\u63D2\u4EF6\u2026",
6732
+ msg.messageId
6733
+ );
6734
+ try {
6735
+ await ensureSafeProfile({
6736
+ home: this.options.home,
6737
+ dshProfile: this.state.dshProfile,
6738
+ env: this.options.env ?? process.env
6739
+ });
6740
+ const bin = this.dshBin;
6741
+ if (!bin) {
6742
+ await this.sendMarkdown(
6743
+ msg.chatId,
6744
+ "\u672A\u627E\u5230\u672C\u673A dsh \u5B89\u88C5\uFF0C\u65E0\u6CD5\u8FDB\u5165\u5B89\u5168\u6A21\u5F0F\u3002\u8BF7\u5148\u786E\u8BA4 `dsh` \u53EF\u7528\u3002",
6745
+ msg.messageId
6746
+ );
6747
+ return;
6748
+ }
6749
+ const probe = await (this.options.probeSafeProfileFn ?? probeSafeProfile)({
6750
+ bin,
6751
+ dshProfile: this.state.dshProfile,
6752
+ home: this.options.home,
6753
+ env: this.options.env ?? process.env
6754
+ });
6755
+ if (!probe.ok) {
6756
+ await this.sendMarkdown(
6757
+ msg.chatId,
6758
+ [
6759
+ "\u5B89\u5168\u6A21\u5F0F\u5C31\u7EEA\u68C0\u67E5\u5931\u8D25\uFF08dsh \u6838\u5FC3\u65E0\u6CD5\u89E3\u6790\uFF09\uFF1A",
6760
+ "",
6761
+ "```",
6762
+ (probe.error ?? "unknown error").slice(0, 1500),
6763
+ "```"
6764
+ ].join("\n"),
6765
+ msg.messageId
6766
+ );
6767
+ return;
6768
+ }
6769
+ this.safeAdapter ??= this.options.adapter ?? new DshAdapter({
6770
+ command: "node",
6771
+ args: [bin, "--profile", this.state.safeProfile],
6772
+ stopGraceMs: 5e3
6773
+ });
6774
+ await this.setMode("safe");
6775
+ await this.sendMarkdown(
6776
+ msg.chatId,
6777
+ [
6778
+ "\u5B89\u5168\u6A21\u5F0F\u5DF2\u5C31\u7EEA\uFF1Adsh \u4E3B\u6838\u5FC3\u8FD0\u884C\u4E2D\uFF0C\u7B2C\u4E09\u65B9\u63D2\u4EF6\u672A\u52A0\u8F7D\u3002",
6779
+ "",
6780
+ "\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",
6781
+ "- \u201C\u5217\u51FA\u5F53\u524D profile \u5B89\u88C5\u7684\u63D2\u4EF6\u5E76\u68C0\u67E5\u54EA\u4E2A\u6700\u8FD1\u53D8\u574F\u201D",
6782
+ "- \u201C\u7528 `/safemode plugins` \u67E5\u770B\u63D2\u4EF6\u6E05\u5355\u201D",
6783
+ "- \u4FEE\u590D\u5B8C\u6210\u540E\u53D1\u9001 `/safemode exit` \u91CD\u542F\u5B8C\u6574 profile\u3002"
6784
+ ].join("\n"),
6785
+ msg.messageId
6786
+ );
6787
+ } catch (error) {
6788
+ await this.sendMarkdown(
6789
+ msg.chatId,
6790
+ `\u8FDB\u5165\u5B89\u5168\u6A21\u5F0F\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`,
6791
+ msg.messageId
6792
+ );
6793
+ }
6794
+ }
6795
+ async sendStatus(msg) {
6796
+ const snapshot = this.snapshot();
6797
+ await this.sendMarkdown(
6798
+ msg.chatId,
6799
+ [
6800
+ `\u6A21\u5F0F\uFF1A${snapshot.mode}`,
6801
+ `dsh profile\uFF1A${snapshot.dshProfile}`,
6802
+ `\u5B89\u5168 profile\uFF1A${snapshot.safeProfile}`,
6803
+ `dsh \u662F\u5426\u5728\u7EBF\uFF1A${snapshot.dshUp ? "\u662F" : "\u5426"}`,
6804
+ `\u5FC3\u8DF3\u9F84\uFF1A${snapshot.heartbeatAgeMs === void 0 ? "\u65E0" : `${snapshot.heartbeatAgeMs}ms`}`,
6805
+ `\u98DE\u4E66\u901A\u9053\uFF1A${snapshot.channelConnected ? "\u5B88\u62A4\u5DF2\u63A5\u7BA1" : "\u672A\u8FDE\u63A5"}`,
6806
+ `dsh bin\uFF1A${snapshot.dshBin ?? "\u672A\u53D1\u73B0"}`,
6807
+ `\u5B88\u62A4 pid\uFF1A${snapshot.pid}`,
6808
+ `\u5DF2\u89C2\u5BDF\u8FC7 dsh \u8FD0\u884C\uFF1A${snapshot.profileSeenUp ? "\u662F" : "\u5426"}`
6809
+ ].join("\n"),
6810
+ msg.messageId
6811
+ );
6812
+ }
6813
+ async sendPluginList(msg) {
6814
+ const bin = this.dshBin;
6815
+ if (!bin) {
6816
+ await this.sendMarkdown(
6817
+ msg.chatId,
6818
+ "\u672A\u627E\u5230\u672C\u673A dsh \u5B89\u88C5\uFF0C\u65E0\u6CD5\u5217\u51FA\u63D2\u4EF6\u3002",
6819
+ msg.messageId
6820
+ );
6821
+ return;
6822
+ }
6823
+ const run = this.options.runPluginList ?? defaultRunPluginList;
6824
+ const result = await run(bin, this.state.dshProfile);
6825
+ const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
6826
+ await this.sendMarkdown(
6827
+ msg.chatId,
6828
+ [
6829
+ `profile \`${this.state.dshProfile}\` \u5DF2\u5B89\u88C5\u7684\u63D2\u4EF6\uFF08\u4F9D\u8D56\u6E05\u5355\uFF09\uFF1A`,
6830
+ "",
6831
+ "```",
6832
+ (output || "\uFF08\u7A7A\uFF09").slice(0, 3e3),
6833
+ "```"
6834
+ ].join("\n"),
6835
+ msg.messageId
6836
+ );
6837
+ }
6838
+ async exitSafeMode(msg) {
6839
+ if (this.state.mode !== "safe") {
6840
+ await this.sendMarkdown(
6841
+ msg.chatId,
6842
+ "\u5F53\u524D\u4E0D\u5728\u5B89\u5168\u6A21\u5F0F\uFF0C\u65E0\u9700\u9000\u51FA\u3002",
6843
+ msg.messageId
6844
+ );
6845
+ return;
6846
+ }
6847
+ const bin = this.dshBin;
6848
+ if (!bin) {
6849
+ await this.sendMarkdown(
6850
+ msg.chatId,
6851
+ "\u672A\u627E\u5230\u672C\u673A dsh \u5B89\u88C5\uFF0C\u65E0\u6CD5\u91CD\u542F\u5B8C\u6574 profile\u3002",
6852
+ msg.messageId
6853
+ );
6854
+ return;
6855
+ }
6856
+ const spawn7 = this.options.spawnDetachedFn ?? spawnDetached;
6857
+ const spawned = spawn7("node", [bin, "--profile", this.state.dshProfile]);
6858
+ if (spawned.pid !== void 0) this.state.relaunchedPid = spawned.pid;
6859
+ await this.save();
6860
+ await this.sendMarkdown(
6861
+ msg.chatId,
6862
+ [
6863
+ `\u6B63\u5728\u9000\u51FA\u5B89\u5168\u6A21\u5F0F\u5E76\u91CD\u542F\u5B8C\u6574 profile\uFF08\`${this.state.dshProfile}\`\uFF09\u2026`,
6864
+ "\u5B88\u62A4\u8FDB\u7A0B\u5C06\u65AD\u5F00\u98DE\u4E66\u8FDE\u63A5\u5E76\u628A\u901A\u9053\u4EA4\u8FD8\u7ED9 dsh \u6865\u63A5\u5F15\u64CE\u3002"
6865
+ ].join("\n"),
6866
+ msg.messageId
6867
+ );
6868
+ await delay(this.options.sendDelayMs);
6869
+ this.transcripts.clear();
6870
+ await this.disconnectChannel();
6871
+ }
6872
+ async runSafeTask(scope, msg) {
6873
+ const adapter = this.safeAdapter;
6874
+ if (!adapter) {
6875
+ await this.sendMarkdown(
6876
+ msg.chatId,
6877
+ "\u5B89\u5168\u6A21\u5F0F\u672A\u5C31\u7EEA\uFF0C\u8BF7\u5148\u53D1\u9001 `/safemode`\u3002",
6878
+ msg.messageId
6879
+ );
6880
+ return;
6881
+ }
6882
+ const transcript = this.transcripts.get(scope) ?? [];
6883
+ const prompt = buildSafePrompt(transcript, msg.content);
6884
+ await this.sendMarkdown(
6885
+ msg.chatId,
6886
+ "\uFF08\u5B89\u5168\u6A21\u5F0F \xB7 dsh \u6838\u5FC3\u5904\u7406\u4E2D\u2026\uFF09",
6887
+ msg.messageId
6888
+ );
6889
+ const run = adapter.run({
6890
+ runId: randomUUID5(),
6891
+ prompt,
6892
+ cwd: this.config?.workspaces.default,
6893
+ sessionId: void 0,
6894
+ model: void 0,
6895
+ images: void 0,
6896
+ stopGraceMs: 5e3
6897
+ });
6898
+ const textParts = [];
6899
+ let finalText;
6900
+ let errorText;
6901
+ let terminal = false;
6902
+ try {
6903
+ for await (const event of run.events) {
6904
+ if (event.type === "text" && event.delta) textParts.push(event.delta);
6905
+ if (event.type === "final_text") finalText = event.content;
6906
+ if (event.type === "error") {
6907
+ errorText = event.message;
6908
+ terminal = true;
6909
+ }
6910
+ if (event.type === "done") terminal = true;
6911
+ }
6912
+ } catch (error) {
6913
+ errorText = error instanceof Error ? error.message : String(error);
6914
+ terminal = true;
6915
+ }
6916
+ if (!terminal) {
6917
+ await run.waitForExit(SAFE_RUN_TIMEOUT_MS);
6918
+ }
6919
+ if (errorText) {
6920
+ await this.sendMarkdown(
6921
+ msg.chatId,
6922
+ `\uFF08\u5B89\u5168\u6A21\u5F0F \xB7 dsh \u6838\u5FC3\u9519\u8BEF\uFF09
6923
+ ${errorText.slice(0, 2e3)}`,
6924
+ msg.messageId
6925
+ );
6926
+ return;
6927
+ }
6928
+ const answer = finalText ?? textParts.join("").trim();
6929
+ if (!answer) {
6930
+ await this.sendMarkdown(
6931
+ msg.chatId,
6932
+ "\uFF08\u5B89\u5168\u6A21\u5F0F \xB7 dsh \u6838\u5FC3\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09",
6933
+ msg.messageId
6934
+ );
6935
+ return;
6936
+ }
6937
+ await this.sendMarkdown(msg.chatId, answer, msg.messageId);
6938
+ this.transcripts.set(scope, pushTranscript(transcript, [
6939
+ { role: "user", content: msg.content },
6940
+ { role: "assistant", content: answer }
6941
+ ]));
6942
+ }
6943
+ };
6944
+ function buildSafePrompt(transcript, current) {
6945
+ if (transcript.length === 0) return current;
6946
+ const history = transcript.map((entry) => `${entry.role === "user" ? "\u7528\u6237" : "\u52A9\u624B"}: ${entry.content}`).join("\n");
6947
+ return [
6948
+ "\u4EE5\u4E0B\u662F\u672C\u6B21\u5B89\u5168\u6A21\u5F0F\u5BF9\u8BDD\u7684\u4E0A\u4E0B\u6587\uFF08\u7528\u4E8E\u8FDE\u7EED\u6027\uFF0C\u4E0D\u662F\u65B0\u4EFB\u52A1\u6307\u4EE4\uFF09\uFF1A",
6949
+ "",
6950
+ history,
6951
+ "",
6952
+ `\u7528\u6237: ${current}`
6953
+ ].join("\n");
6954
+ }
6955
+ function pushTranscript(existing, entries) {
6956
+ return [...existing, ...entries].slice(-MAX_TRANSCRIPT_ENTRIES);
6957
+ }
6958
+ async function defaultRunPluginList(bin, dshProfile) {
6959
+ return captureOutput("node", [bin, "plugin", "--profile", dshProfile, "list"], 6e4);
6960
+ }
6961
+ function delay(ms) {
6962
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
6963
+ }
6964
+ function parseFreshness2(value) {
6965
+ const raw = value?.trim();
6966
+ if (!raw) return 6e5;
6967
+ const parsed = Number(raw);
6968
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : 6e5;
6969
+ }
6970
+ function defined(input) {
6971
+ const output = {};
6972
+ for (const [key, value] of Object.entries(input)) {
6973
+ if (value !== void 0) output[key] = value;
6974
+ }
6975
+ return output;
6976
+ }
6977
+ function guardianLayoutFor(env, bridgeProfile) {
6978
+ const paths = resolveAppPaths(env.home);
6979
+ return {
6980
+ paths,
6981
+ stateFile: join15(paths.root, "guardian.json"),
6982
+ heartbeatFile: paths.profilePath(bridgeProfile, "guardian", "heartbeat.json"),
6983
+ configFile: paths.configFile
6984
+ };
6985
+ }
6986
+ async function buildGuardianService(env, overrides = {}) {
6987
+ const paths = resolveAppPaths(env.home);
6988
+ const stateFile = join15(paths.root, "guardian.json");
6989
+ const fallback = newGuardianState({
6990
+ dshProfile: env.guardianProfile,
6991
+ bridgeProfile: env.guardianBridgeProfile
6992
+ });
6993
+ const state = await loadGuardianState(stateFile, fallback);
6994
+ return new GuardianService({
6995
+ stateFile,
6996
+ configFile: paths.configFile,
6997
+ heartbeatFile: paths.profilePath(state.bridgeProfile, "guardian", "heartbeat.json"),
6998
+ home: homedir8(),
6999
+ env: process.env,
7000
+ dshProfile: state.dshProfile,
7001
+ bridgeProfile: state.bridgeProfile,
7002
+ safeProfile: state.safeProfile,
7003
+ pollMs: env.guardianPollMs,
7004
+ staleMs: env.guardianStaleMs,
7005
+ engineDeadMs: env.guardianEngineDeadMs,
7006
+ ...overrides
7007
+ });
7008
+ }
7009
+
7010
+ // src/cli/commands/guardian.ts
7011
+ function envWithOverrides(options) {
7012
+ const env = loadRuntimeEnv(process.env);
7013
+ if (options.dshProfile) env.guardianProfile = options.dshProfile;
7014
+ if (options.bridgeProfile) env.guardianBridgeProfile = options.bridgeProfile;
7015
+ return env;
7016
+ }
7017
+ function printLines(lines) {
7018
+ process.stdout.write(`${lines.join("\n")}
7019
+ `);
7020
+ }
7021
+ async function runGuardian(options = {}) {
7022
+ const env = envWithOverrides(options);
7023
+ if (env.guardianDisabled) {
7024
+ printLines(["\u5B89\u5168\u7F51\u5B88\u62A4\u5DF2\u7981\u7528\uFF08DSH_LARK_GUARDIAN_DISABLED=1\uFF09\uFF0C\u8FDB\u7A0B\u9000\u51FA\u3002"]);
7025
+ return;
7026
+ }
7027
+ const service = await buildGuardianService(env);
7028
+ await service.start();
7029
+ printLines([
7030
+ `\u5B89\u5168\u7F51\u5B88\u62A4\u8FD0\u884C\u4E2D\uFF08pid=${process.pid}\uFF0Cdsh profile=${service.snapshot().dshProfile}\uFF09\u3002`,
7031
+ "dsh \u6B63\u5E38\u8FD0\u884C\u65F6\u4FDD\u6301\u9759\u9ED8\uFF1Bdsh \u4E0B\u7EBF\u540E\u81EA\u52A8\u63A5\u7BA1\u98DE\u4E66\u901A\u9053\uFF0C\u7B49\u5F85 /safemode \u63A7\u5236\u4FE1\u53F7\u3002"
7032
+ ]);
7033
+ await waitForShutdown2();
7034
+ await service.stop();
7035
+ }
7036
+ async function installGuardianCommand(options = {}) {
7037
+ const env = envWithOverrides(options);
7038
+ const result = await installGuardian({ env, ...options.dshProfile ? { dshProfile: options.dshProfile } : {} });
7039
+ printResult(result);
7040
+ if (!result.ok) process.exitCode = 1;
7041
+ }
7042
+ async function uninstallGuardianCommand(options = {}) {
7043
+ const env = envWithOverrides(options);
7044
+ const result = await uninstallGuardian({ env });
7045
+ printResult(result);
7046
+ }
7047
+ async function statusGuardianCommand(options = {}) {
7048
+ const env = envWithOverrides(options);
7049
+ const paths = resolveAppPaths(env.home);
7050
+ const fallback = newGuardianState({
7051
+ dshProfile: env.guardianProfile,
7052
+ bridgeProfile: env.guardianBridgeProfile
7053
+ });
7054
+ const state = await loadGuardianState(join16(paths.root, "guardian.json"), fallback);
7055
+ const layout = guardianLayoutFor(env, state.bridgeProfile);
7056
+ const heartbeat = await readHeartbeat(layout.heartbeatFile);
7057
+ const processFound = await findProfileProcess(state.dshProfile);
7058
+ const up = isHeartbeatFresh(heartbeat, env.guardianStaleMs) || processFound !== void 0;
7059
+ printLines([
7060
+ "\u5B89\u5168\u7F51\u5B88\u62A4\u72B6\u6001",
7061
+ "---------------",
7062
+ `\u6A21\u5F0F\uFF1A${state.mode}`,
7063
+ `dsh profile\uFF1A${state.dshProfile}`,
7064
+ `\u5B89\u5168 profile\uFF1A${state.safeProfile}`,
7065
+ `\u6865\u63A5 profile\uFF1A${state.bridgeProfile}`,
7066
+ `dsh \u662F\u5426\u5728\u7EBF\uFF1A${up ? "\u662F" : "\u5426"}${processFound ? `\uFF08pid ${processFound.pid}\uFF09` : ""}`,
7067
+ `\u5FC3\u8DF3\u9F84\uFF1A${heartbeat ? `${heartbeatAgeMs(heartbeat)}ms` : "\u65E0"}`,
7068
+ `\u5DF2\u89C2\u5BDF\u8FC7 dsh \u8FD0\u884C\uFF1A${state.profileSeenUp ? "\u662F" : "\u5426"}`,
7069
+ `\u5B88\u62A4\u8FDB\u7A0B pid\uFF1A${process.pid}`,
7070
+ `\u72B6\u6001\u6587\u4EF6\uFF1A${layout.stateFile}`,
7071
+ `\u5FC3\u8DF3\u6587\u4EF6\uFF1A${layout.heartbeatFile}`
7072
+ ]);
7073
+ }
7074
+ function printResult(result) {
7075
+ for (const message of result.messages) {
7076
+ process.stdout.write(`${message}
7077
+ `);
7078
+ }
7079
+ }
7080
+ function waitForShutdown2() {
7081
+ return new Promise((resolve5) => {
7082
+ const shutdown = () => {
7083
+ process.off("SIGINT", shutdown);
7084
+ process.off("SIGTERM", shutdown);
7085
+ resolve5();
7086
+ };
7087
+ process.once("SIGINT", shutdown);
7088
+ process.once("SIGTERM", shutdown);
7089
+ });
5749
7090
  }
5750
7091
 
5751
7092
  // src/cli.ts
@@ -5762,8 +7103,11 @@ function buildProgram() {
5762
7103
  program.name("dsh-lark-bot").description("Bridge DeepSeek Harness into Feishu / Lark").version(packageVersion(), "-v, --version");
5763
7104
  program.command("setup").description(
5764
7105
  "Install this package as a standard dsh profile bundle (single install path)"
5765
- ).option("--profile <name>", "dsh profile to install into (default: dsh-lark)").action(async (opts) => {
5766
- await runSetup({ ...opts.profile ? { profile: opts.profile } : {} });
7106
+ ).option("--profile <name>", "dsh profile to install into (default: dsh-lark)").option("--guardian", "also install the safety-net guardian service").action(async (opts) => {
7107
+ await runSetup({
7108
+ ...opts.profile ? { profile: opts.profile } : {},
7109
+ ...opts.guardian ? { guardian: true } : {}
7110
+ });
5767
7111
  });
5768
7112
  program.command("doctor").description("Run local diagnostics").option("--profile <name>", "profile name").option("--workspace <path>", "initial working directory").option("--app-id <id>", "existing Lark/Feishu app id").option("--app-secret <secret>", "existing Lark/Feishu app secret").option("--tenant <tenant>", "feishu or lark").action(async (opts) => {
5769
7113
  await runDoctor({ ...opts, version: packageVersion() });
@@ -5773,6 +7117,21 @@ function buildProgram() {
5773
7117
  ).action(async (opts) => {
5774
7118
  await runBot(opts);
5775
7119
  });
7120
+ const guardian = program.command("guardian").description(
7121
+ "Safety-net guardian: a minimal process independent of dsh that keeps the Feishu rescue entrance alive"
7122
+ );
7123
+ guardian.command("run").description("Run the guardian in the foreground (system service entry point)").option("--dsh-profile <name>", "dsh profile to watch / relaunch (default from state)").option("--bridge-profile <name>", "bridge state profile with Feishu credentials").action(async (opts) => {
7124
+ await runGuardian(opts);
7125
+ });
7126
+ guardian.command("install").description("Install the guardian as a system-level resident service").option("--dsh-profile <name>", "dsh profile to watch / relaunch (default: dsh-lark)").option("--bridge-profile <name>", "bridge state profile (default: default)").action(async (opts) => {
7127
+ await installGuardianCommand(opts);
7128
+ });
7129
+ guardian.command("uninstall").description("Remove the system service entry (state file is kept)").action(async () => {
7130
+ await uninstallGuardianCommand();
7131
+ });
7132
+ guardian.command("status").description("Show guardian / dsh / safe-mode state").option("--dsh-profile <name>", "dsh profile to inspect").option("--bridge-profile <name>", "bridge state profile to inspect").action(async (opts) => {
7133
+ await statusGuardianCommand(opts);
7134
+ });
5776
7135
  return program;
5777
7136
  }
5778
7137
  async function main(argv = process.argv) {