dsh-lark-bot 0.19.2 → 0.19.4

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/README.md CHANGED
@@ -334,7 +334,7 @@ profile 的前台进程会拒绝并提示先停止,生命周期锁阻止并发
334
334
 
335
335
  ### 升级
336
336
 
337
- **完全不接触命令行:** profile 管理员在飞书发送 `/upgrade`。有新版本时 bot 弹出只允许发起人操作的确认卡;点击“确认更新”后,Guardian 通过独立 worker 安装卡片中确认的精确 npm 版本,复用完整升级、runtime profile 修复、guardian/profile 重启和 doctor 验证链,并在重载后回到原会话报告结果。点击“取消”不会产生任何变更。更新会重启机器人,正在执行的任务可能被中断;配置、会话、归档和凭据保持不变。每次 `/new` / `/reset` 都会 best-effort 查询一次 npm;仅在有新版本时额外发送一条简短普通消息。
337
+ **完全不接触命令行:** profile 管理员在飞书发送 `/upgrade`。有新版本时 bot 弹出只允许发起人操作的确认卡;点击“确认更新”后,Guardian 通过独立 worker 安装卡片中确认的精确 npm 版本,复用完整升级、runtime profile 修复、guardian/profile 重启和 doctor 验证链,并在重载后回到原会话报告结果。worker 使用 owner-only 中立工作目录与每次请求隔离的 npm cache,不依赖 bridge 启动目录或用户全局 npm cache;失败时只回传脱敏的可行动类别,不发送原始命令输出。点击“取消”不会产生任何变更。更新会重启机器人,正在执行的任务可能被中断;配置、会话、归档和凭据保持不变。每次 `/new` / `/reset` 都会 best-effort 查询一次 npm;仅在有新版本时额外发送一条简短普通消息。
338
338
 
339
339
  **推荐:一行命令彻底升级(v0.12.0+ 新增,issue #10)**
340
340
 
@@ -724,6 +724,8 @@ pnpm publish:dual
724
724
  默认安装的「安全网守护」(`src/guardian/`)独立于 dsh 进程常驻:dsh 在线时静默,下线时接管飞书
725
725
  通道接收 `/safemode` 控制信号,以仅核心 profile(`dsh-base` + `dsh-headless`)拉起受限对话
726
726
  用于自愈,`/safemode exit` 重启完整 profile 并交还通道。
727
+ `dsh-lark-bot guardian status` 只报告精确 `guardian run` 进程身份唯一且存活的常驻 PID,并在
728
+ 系统服务 PID 可用时交叉验证;身份不确定时显示“未发现”,不会把查询命令自身 PID 当成守护进程。
727
729
 
728
730
  ## 目录结构
729
731
 
package/dist/cli.js CHANGED
@@ -838,14 +838,67 @@ var init_acp_adapter = __esm({
838
838
  var process_exports = {};
839
839
  __export(process_exports, {
840
840
  captureOutput: () => captureOutput,
841
+ findGuardianProcess: () => findGuardianProcess,
841
842
  findProfileProcess: () => findProfileProcess,
842
843
  isProcessAlive: () => isProcessAlive,
843
844
  listProcesses: () => listProcesses,
844
845
  looksLikeDshProcess: () => looksLikeDshProcess,
846
+ matchGuardianProcess: () => matchGuardianProcess,
845
847
  matchProfileProcess: () => matchProfileProcess,
846
848
  spawnDetached: () => spawnDetached
847
849
  });
848
850
  import { spawn as spawn5 } from "child_process";
851
+ function commandTokens(cmdline) {
852
+ const tokens = [];
853
+ let token = "";
854
+ let quote;
855
+ const input = cmdline.trim();
856
+ for (let index = 0; index < input.length; index += 1) {
857
+ const character = input[index] ?? "";
858
+ const next = input[index + 1];
859
+ if (character === "\\" && quote === void 0 && next !== void 0 && /[\s\\"']/u.test(next)) {
860
+ token += next;
861
+ index += 1;
862
+ } else if (character === "\\" && quote === '"' && next === '"') {
863
+ token += next;
864
+ index += 1;
865
+ } else if (quote !== void 0) {
866
+ if (character === quote) quote = void 0;
867
+ else token += character;
868
+ } else if (character === '"' || character === "'") {
869
+ quote = character;
870
+ } else if (/\s/u.test(character)) {
871
+ if (token !== "") {
872
+ tokens.push(token);
873
+ token = "";
874
+ }
875
+ } else {
876
+ token += character;
877
+ }
878
+ }
879
+ if (token !== "") tokens.push(token);
880
+ return tokens;
881
+ }
882
+ function isGuardianCliEntry(token) {
883
+ const normalized = token.replace(/\\/gu, "/").toLowerCase();
884
+ return normalized.endsWith("/dist/cli.js") && (normalized.includes("/dsh-lark-bot/") || normalized.includes("/dsh-feishu-bot/"));
885
+ }
886
+ function hasOnlyGuardianRunOptions(tokens) {
887
+ for (let index = 0; index < tokens.length; index += 2) {
888
+ if (tokens[index] !== "--dsh-profile" && tokens[index] !== "--bridge-profile") return false;
889
+ if (tokens[index + 1] === void 0 || tokens[index + 1]?.startsWith("--")) return false;
890
+ }
891
+ return true;
892
+ }
893
+ function matchGuardianProcess(cmdline) {
894
+ const tokens = commandTokens(cmdline);
895
+ const cliIndex = tokens.findIndex(isGuardianCliEntry);
896
+ if (cliIndex < 1) return false;
897
+ const executable = tokens[cliIndex - 1]?.replace(/\\/gu, "/").split("/").pop()?.toLowerCase();
898
+ if (executable !== "node" && executable !== "node.exe") return false;
899
+ if (tokens[cliIndex + 1] !== "guardian" || tokens[cliIndex + 2] !== "run") return false;
900
+ return hasOnlyGuardianRunOptions(tokens.slice(cliIndex + 3));
901
+ }
849
902
  function hasProfileFlag(cmdline, dshProfile) {
850
903
  const pattern = new RegExp(
851
904
  `(?:^|\\s)--profile(?:\\s+|=)${escapeRegExp(dshProfile)}(?:\\s|$)`
@@ -865,14 +918,14 @@ function matchProfileProcess(cmdline, dshProfile) {
865
918
  if (/(?:^|\s)plugin(?:\s|$)/.test(cmdline)) return false;
866
919
  return hasProfileFlag(cmdline, dshProfile) && looksLikeDshProcess(cmdline);
867
920
  }
868
- async function listProcesses() {
869
- if (process.platform === "win32") {
870
- return listProcessesWindows();
921
+ async function listProcesses(platform = process.platform, run = captureOutput) {
922
+ if (platform === "win32") {
923
+ return listProcessesWindows(run);
871
924
  }
872
- return listProcessesPosix();
925
+ return listProcessesPosix(run);
873
926
  }
874
- async function listProcessesPosix() {
875
- const { stdout } = await captureOutput("ps", ["-axo", "pid=,args="], 1e4);
927
+ async function listProcessesPosix(run) {
928
+ const { stdout } = await run("ps", ["-axo", "pid=,args="], 1e4);
876
929
  const result = [];
877
930
  for (const line of stdout.split("\n")) {
878
931
  const match = /^\s*(\d+)\s+(.+)$/.exec(line);
@@ -882,33 +935,46 @@ async function listProcessesPosix() {
882
935
  }
883
936
  return result;
884
937
  }
885
- async function listProcessesWindows() {
886
- const { stdout } = await captureOutput(
938
+ async function listProcessesWindows(run) {
939
+ const { stdout } = await run(
887
940
  "powershell.exe",
888
941
  [
889
942
  "-NoProfile",
890
943
  "-NonInteractive",
891
944
  "-Command",
892
- "Get-CimInstance Win32_Process | Select-Object ProcessId, CommandLine | ConvertTo-Csv -NoTypeInformation"
945
+ "Get-CimInstance Win32_Process | Select-Object ProcessId, CommandLine | ConvertTo-Json -Compress"
893
946
  ],
894
947
  1e4
895
948
  );
896
- const result = [];
897
- for (const line of stdout.split("\n").slice(1)) {
898
- const [pid, ...rest] = line.trim().split(",");
899
- const numeric = Number(pid?.replace(/"/g, ""));
900
- if (Number.isInteger(numeric) && rest.length > 0) {
901
- result.push({ pid: numeric, cmdline: rest.join(",").replace(/^"|"$/g, "") });
902
- }
949
+ try {
950
+ const parsed = JSON.parse(stdout);
951
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
952
+ return rows.flatMap((row) => {
953
+ if (typeof row !== "object" || row === null) return [];
954
+ const record = row;
955
+ const pid = Number(record.ProcessId);
956
+ const cmdline = record.CommandLine;
957
+ return Number.isInteger(pid) && pid > 0 && typeof cmdline === "string" ? [{ pid, cmdline }] : [];
958
+ });
959
+ } catch {
960
+ return [];
903
961
  }
904
- return result;
905
962
  }
906
- async function captureOutput(command, args, timeoutMs = 3e4) {
963
+ async function captureOutput(command, args, timeoutMs = 3e4, options = {}) {
907
964
  return new Promise((resolve6) => {
908
- const child = spawn5(command, [...args], {
909
- stdio: ["ignore", "pipe", "pipe"],
910
- windowsHide: true
911
- });
965
+ const previousUmask = options.umask !== void 0 && process.platform !== "win32" ? process.umask(options.umask) : void 0;
966
+ const child = (() => {
967
+ try {
968
+ return spawn5(command, [...args], {
969
+ stdio: ["ignore", "pipe", "pipe"],
970
+ windowsHide: true,
971
+ ...options.cwd ? { cwd: options.cwd } : {},
972
+ ...options.env ? { env: options.env } : {}
973
+ });
974
+ } finally {
975
+ if (previousUmask !== void 0) process.umask(previousUmask);
976
+ }
977
+ })();
912
978
  let stdout = "";
913
979
  let stderr = "";
914
980
  child.stdout?.on("data", (chunk) => {
@@ -934,6 +1000,47 @@ async function findProfileProcess(dshProfile) {
934
1000
  const processes = await listProcesses();
935
1001
  return processes.find((entry) => matchProfileProcess(entry.cmdline, dshProfile));
936
1002
  }
1003
+ async function managedGuardianPid(platform, run, uid) {
1004
+ let result;
1005
+ if (platform === "linux") {
1006
+ result = await run(
1007
+ "systemctl",
1008
+ ["--user", "show", "dsh-lark-guardian.service", "--property=MainPID", "--value"],
1009
+ 1e4
1010
+ );
1011
+ } else if (platform === "darwin") {
1012
+ result = await run(
1013
+ "launchctl",
1014
+ ["print", `gui/${uid}/io.dsh-lark.dsh-lark-guardian`],
1015
+ 1e4
1016
+ );
1017
+ } else {
1018
+ return void 0;
1019
+ }
1020
+ if (result.code !== 0) return void 0;
1021
+ const raw = platform === "darwin" ? /(?:^|\n)\s*pid\s*=\s*(\d+)\s*(?:\n|$)/u.exec(result.stdout)?.[1] : /^(\d+)\s*$/u.exec(result.stdout.trim())?.[1];
1022
+ const pid = Number(raw);
1023
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
1024
+ }
1025
+ async function findGuardianProcess(options = {}) {
1026
+ const platform = options.platform ?? process.platform;
1027
+ const run = options.run ?? captureOutput;
1028
+ const isAlive = options.isAlive ?? isProcessAlive;
1029
+ const currentPid = options.currentPid ?? process.pid;
1030
+ const candidates = (await listProcesses(platform, run)).filter(
1031
+ (entry) => entry.pid !== currentPid && matchGuardianProcess(entry.cmdline) && isAlive(entry.pid)
1032
+ );
1033
+ if (candidates.length !== 1) return void 0;
1034
+ const candidate = candidates[0];
1035
+ if (candidate === void 0) return void 0;
1036
+ const servicePid = await managedGuardianPid(
1037
+ platform,
1038
+ run,
1039
+ options.uid ?? process.getuid?.() ?? 0
1040
+ );
1041
+ if (servicePid !== void 0 && servicePid !== candidate.pid) return void 0;
1042
+ return isAlive(candidate.pid) ? candidate : void 0;
1043
+ }
937
1044
  function isProcessAlive(pid) {
938
1045
  try {
939
1046
  process.kill(pid, 0);
@@ -997,9 +1104,9 @@ async function writeFileAtomic(target, data, options = {}) {
997
1104
  }
998
1105
  }
999
1106
  async function rmSilently(path) {
1000
- const { rm: rm12 } = await import("fs/promises");
1107
+ const { rm: rm13 } = await import("fs/promises");
1001
1108
  try {
1002
- await rm12(path, { force: true });
1109
+ await rm13(path, { force: true });
1003
1110
  } catch {
1004
1111
  }
1005
1112
  }
@@ -2672,6 +2779,18 @@ function patchYamlFor(options) {
2672
2779
  "",
2673
2780
  "- id: hmr",
2674
2781
  " disabled: true",
2782
+ "",
2783
+ // Model-invocable channel skill: expose the dsh-lark-bot operations guide
2784
+ // to THIS agent session's skill catalog — the one the `skill` tool reads.
2785
+ // The bridge engine registers the same skill on its own cordis context
2786
+ // (plugin.ts), but that context is never the one the model reads. This row
2787
+ // ensures the skill also lands on the agent runtime's context so that
2788
+ // `skill("dsh-lark-bot")` resolves in a live SDK session. It is kept
2789
+ // outside the bridgeTools gate because it needs no callback endpoint and
2790
+ // is also useful to the guardian's core-only safe profile.
2791
+ "- insert:",
2792
+ " - id: lark-skill",
2793
+ ` name: '${own.name}/skill'`,
2675
2794
  ""
2676
2795
  ];
2677
2796
  if (bridgeTools) {
@@ -7207,7 +7326,11 @@ function processElements(state, locale, maxTools) {
7207
7326
  function thinkingPanel(state, locale, maxTools) {
7208
7327
  return {
7209
7328
  tag: "collapsible_panel",
7210
- expanded: state.terminal === "running",
7329
+ // Default to collapsed: the top-level compatibility snapshot already shows
7330
+ // the latest status and current tools, so keep the detailed per-tool list
7331
+ // collapsed to avoid two overlapping expanded surfaces. (Both sections are
7332
+ // retained for now.)
7333
+ expanded: false,
7211
7334
  header: {
7212
7335
  title: { tag: "plain_text", content: `\u2699\uFE0F ${locale === "zh_cn" ? "\u6267\u884C\u8FC7\u7A0B" : "Execution"} \xB7 ${summaryText(state, locale)}` },
7213
7336
  icon: { tag: "standard_icon", token: "down-small-ccm_outlined" },
@@ -7893,6 +8016,15 @@ async function runAttempt(input, cwd, workspaceCwd, sessionId, resuming, replyOp
7893
8016
  log.warn("run-flow", "card-update-failed", { scope: input.scope, error });
7894
8017
  }
7895
8018
  };
8019
+ const unregisterReanchor = input.runCardAnchors?.register(input.chatId, async () => {
8020
+ if (state.terminal !== "running") return;
8021
+ try {
8022
+ await controller.update(renderer(state, density, Date.now()));
8023
+ if (typeof controller.reanchor === "function") await controller.reanchor();
8024
+ } catch (error) {
8025
+ log.warn("run-flow", "card-reanchor-failed", { scope: input.scope, error });
8026
+ }
8027
+ });
7896
8028
  const showResumeRecovery = async (error) => {
7897
8029
  if (classifySessionError(errorMessage(error)) === void 0) throw error;
7898
8030
  resumeFailure = errorMessage(error);
@@ -8066,6 +8198,7 @@ async function runAttempt(input, cwd, workspaceCwd, sessionId, resuming, replyOp
8066
8198
  unsubscribeQuestion?.();
8067
8199
  unsubscribePlan?.();
8068
8200
  unsubscribeApproval?.();
8201
+ unregisterReanchor?.();
8069
8202
  }
8070
8203
  };
8071
8204
  let producerStarted = false;
@@ -11797,6 +11930,41 @@ var ResilientCardStreamController = class {
11797
11930
  });
11798
11931
  }
11799
11932
  }
11933
+ /**
11934
+ * Recall the current card and re-create it as the newest top-level message in
11935
+ * the chat, rebinding the controller to the fresh message id. Because Feishu
11936
+ * cannot reorder an existing message, this is the only way to keep an
11937
+ * in-progress process card visible at the tail while interim agent bubbles are
11938
+ * appended below. Best-effort: if the recall fails the card is left where it
11939
+ * is (no duplicate is created); if the controller is closed or failed the id
11940
+ * is left unchanged.
11941
+ */
11942
+ async reanchor() {
11943
+ if (this.closed || this.failed) return this.messageId;
11944
+ if (this.inFlight !== void 0) await this.inFlight;
11945
+ if (this.timer !== void 0) {
11946
+ clearTimeout(this.timer);
11947
+ this.timer = void 0;
11948
+ }
11949
+ const card = this.latest;
11950
+ if (card === void 0) return this.messageId;
11951
+ try {
11952
+ await this.channel.recallMessage(this.messageId);
11953
+ } catch (error) {
11954
+ log.warn("lark-card-stream", "reanchor-recall-failed", {
11955
+ messageId: this.messageId,
11956
+ error: error instanceof Error ? error.message : String(error)
11957
+ });
11958
+ return this.messageId;
11959
+ }
11960
+ const sent = await this.channel.send(this.chatId, { card }, {});
11961
+ if (!sent.messageId) throw new Error("Feishu card re-anchor returned no message_id");
11962
+ this.messageId = sent.messageId;
11963
+ this.latest = card;
11964
+ this.dirty = false;
11965
+ this.failed = false;
11966
+ return this.messageId;
11967
+ }
11800
11968
  };
11801
11969
  function adaptLarkChannel(channel) {
11802
11970
  const base = {
@@ -14040,6 +14208,63 @@ function wizardContextFor(event, deps, channel, scope) {
14040
14208
  };
14041
14209
  }
14042
14210
 
14211
+ // src/bridge/run-card-anchors.ts
14212
+ var RunCardAnchors = class {
14213
+ byChatId = /* @__PURE__ */ new Map();
14214
+ /**
14215
+ * Register a re-anchor callback for a chat. Returns a disposer; safe to call
14216
+ * more than once (idempotent).
14217
+ */
14218
+ register(chatId, reanchor) {
14219
+ const set = this.byChatId.get(chatId) ?? /* @__PURE__ */ new Set();
14220
+ set.add(reanchor);
14221
+ this.byChatId.set(chatId, set);
14222
+ let active = true;
14223
+ return () => {
14224
+ if (!active) return;
14225
+ active = false;
14226
+ set.delete(reanchor);
14227
+ if (set.size === 0) this.byChatId.delete(chatId);
14228
+ };
14229
+ }
14230
+ /** Notify that a new bubble was delivered to the chat. */
14231
+ async bubbleSent(chatId) {
14232
+ const set = this.byChatId.get(chatId);
14233
+ if (!set || set.size === 0) return;
14234
+ await Promise.allSettled([...set].map((reanchor) => reanchor()));
14235
+ }
14236
+ };
14237
+ function attachRunCardAnchors(channel, anchors) {
14238
+ const notify = (chatId) => {
14239
+ void anchors.bubbleSent(chatId).catch((error) => {
14240
+ log.warn("run-card-anchors", "bubble-sent-failed", {
14241
+ chatId,
14242
+ error: error instanceof Error ? error.message : String(error)
14243
+ });
14244
+ });
14245
+ };
14246
+ return {
14247
+ ...channel,
14248
+ async sendMarkdown(chatId, markdown2, options) {
14249
+ const result = await channel.sendMarkdown(chatId, markdown2, options);
14250
+ notify(chatId);
14251
+ return result;
14252
+ },
14253
+ async sendCard(chatId, card, options) {
14254
+ if (typeof channel.sendCard !== "function") return void 0;
14255
+ const result = await channel.sendCard(chatId, card, options);
14256
+ notify(chatId);
14257
+ return result;
14258
+ },
14259
+ async sendFile(chatId, fileName, content, options) {
14260
+ if (typeof channel.sendFile !== "function") return void 0;
14261
+ const result = await channel.sendFile(chatId, fileName, content, options);
14262
+ notify(chatId);
14263
+ return result;
14264
+ }
14265
+ };
14266
+ }
14267
+
14043
14268
  // src/notify/server.ts
14044
14269
  import { createServer } from "http";
14045
14270
  import { randomBytes as randomBytes2 } from "crypto";
@@ -17059,12 +17284,36 @@ function buildSecretHandler(deps) {
17059
17284
  init_own_package();
17060
17285
 
17061
17286
  // src/guardian/update-handoff.ts
17062
- import { randomUUID as randomUUID13 } from "crypto";
17287
+ import { createHash as createHash3, randomUUID as randomUUID13 } from "crypto";
17063
17288
  import { spawn as spawn7 } from "child_process";
17064
- import { mkdir as mkdir21, readFile as readFile31 } from "fs/promises";
17289
+ import { chmod, mkdir as mkdir21, readFile as readFile31, rm as rm10 } from "fs/promises";
17065
17290
  import { dirname as dirname16, join as join26 } from "path";
17066
17291
  init_own_package();
17067
17292
  init_process();
17293
+ function guardianUpdateFailureHint(code) {
17294
+ switch (code) {
17295
+ case "filesystem-access":
17296
+ return {
17297
+ zh: "\u66F4\u65B0\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u53EF\u8BBF\u95EE\uFF1B\u8BF7\u53D1\u9001 `/doctor` \u68C0\u67E5\u6587\u4EF6\u6743\u9650\u540E\u91CD\u8BD5\u3002",
17298
+ en: "The private update workspace is inaccessible. Send `/doctor` to check file permissions, then retry."
17299
+ };
17300
+ case "registry-unavailable":
17301
+ return {
17302
+ zh: "\u65E0\u6CD5\u8FDE\u63A5 npm \u6B63\u5F0F\u6E90\uFF1B\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\u3002",
17303
+ en: "The npm registry is unreachable. Check the network, then retry."
17304
+ };
17305
+ case "bootstrap-unavailable":
17306
+ return {
17307
+ zh: "\u65E0\u6CD5\u542F\u52A8 npm/npx\uFF1B\u8BF7\u53D1\u9001 `/doctor` \u68C0\u67E5 Node.js \u4E0E npm\u3002",
17308
+ en: "npm/npx could not start. Send `/doctor` to check Node.js and npm."
17309
+ };
17310
+ default:
17311
+ return {
17312
+ zh: "\u5347\u7EA7\u547D\u4EE4\u672A\u5B8C\u6210\uFF1B\u8BF7\u53D1\u9001 `/doctor` \u68C0\u67E5\u540E\u91CD\u8BD5\u3002",
17313
+ en: "The upgrade command did not complete. Send `/doctor`, then retry."
17314
+ };
17315
+ }
17316
+ }
17068
17317
  async function loadState(file) {
17069
17318
  try {
17070
17319
  const parsed = JSON.parse(await readFile31(file, "utf8"));
@@ -17085,6 +17334,36 @@ function validPackageName(value) {
17085
17334
  function validVersion(value) {
17086
17335
  return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value);
17087
17336
  }
17337
+ async function ensurePrivateDirectory(path) {
17338
+ await mkdir21(path, { recursive: true, mode: 448 });
17339
+ if (process.platform !== "win32") await chmod(path, 448);
17340
+ }
17341
+ function classifyWorkerFailure(code, stdout, stderr) {
17342
+ const output = `${stderr}
17343
+ ${stdout}`;
17344
+ if (/\b(?:EACCES|EPERM)\b|permission denied|access is denied/iu.test(output)) {
17345
+ return {
17346
+ errorCode: "filesystem-access",
17347
+ error: "upgrade worker could not access its private working files"
17348
+ };
17349
+ }
17350
+ if (/\b(?:ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ETIMEDOUT)\b|registry unavailable/iu.test(output)) {
17351
+ return {
17352
+ errorCode: "registry-unavailable",
17353
+ error: "upgrade worker could not reach the npm registry"
17354
+ };
17355
+ }
17356
+ if (/\bENOENT\b|not found|not recognized/iu.test(output)) {
17357
+ return {
17358
+ errorCode: "bootstrap-unavailable",
17359
+ error: "upgrade worker could not start the npm bootstrap command"
17360
+ };
17361
+ }
17362
+ return {
17363
+ errorCode: "upgrade-failed",
17364
+ error: `upgrade worker exited with code ${code}`
17365
+ };
17366
+ }
17088
17367
  async function defaultLaunch(request) {
17089
17368
  const cliPath = join26(ownPackageInfo().root, "dist", "cli.js");
17090
17369
  await new Promise((resolve6, reject) => {
@@ -17120,23 +17399,48 @@ async function runGuardianUpdateWorker(request, options = {}) {
17120
17399
  if (delayMs > 0) await new Promise((resolve6) => setTimeout(resolve6, delayMs));
17121
17400
  const spec = `${state.packageName}@${state.targetVersion}`;
17122
17401
  const run = options.run ?? captureOutput;
17123
- const result = await run(
17124
- process.platform === "win32" ? "npx.cmd" : "npx",
17125
- [
17126
- "--yes",
17127
- "--registry",
17128
- defaultRegistryUrl(),
17129
- spec,
17130
- "upgrade",
17131
- "--profile",
17132
- state.dshProfile,
17133
- "--yes",
17134
- "--restart",
17135
- "--package",
17136
- spec
17137
- ],
17138
- 30 * 6e4
17402
+ const workerRoot = join26(dirname16(request.stateFile), "update-worker");
17403
+ const cacheRoot = join26(workerRoot, "npm-cache");
17404
+ const cacheDir = join26(
17405
+ cacheRoot,
17406
+ createHash3("sha256").update(state.id).digest("hex")
17139
17407
  );
17408
+ const cwd = join26(workerRoot, "cwd");
17409
+ let result;
17410
+ try {
17411
+ await ensurePrivateDirectory(workerRoot);
17412
+ await rm10(cacheRoot, { recursive: true, force: true });
17413
+ await rm10(cwd, { recursive: true, force: true });
17414
+ await ensurePrivateDirectory(cacheRoot);
17415
+ await ensurePrivateDirectory(cacheDir);
17416
+ await ensurePrivateDirectory(cwd);
17417
+ result = await run(
17418
+ process.platform === "win32" ? "npx.cmd" : "npx",
17419
+ [
17420
+ "--yes",
17421
+ "--cache",
17422
+ cacheDir,
17423
+ "--registry",
17424
+ defaultRegistryUrl(),
17425
+ spec,
17426
+ "upgrade",
17427
+ "--profile",
17428
+ state.dshProfile,
17429
+ "--yes",
17430
+ "--restart",
17431
+ "--package",
17432
+ spec
17433
+ ],
17434
+ 30 * 6e4,
17435
+ { cwd, umask: 63 }
17436
+ );
17437
+ } catch (error) {
17438
+ result = {
17439
+ code: 1,
17440
+ stdout: "",
17441
+ stderr: error instanceof Error ? `${error.name}: ${error.message}` : "unknown worker failure"
17442
+ };
17443
+ }
17140
17444
  const latest = await loadState(request.stateFile);
17141
17445
  if (!latest || latest.id !== request.id) return;
17142
17446
  if (latest.status === "succeeded") return;
@@ -17145,7 +17449,7 @@ async function runGuardianUpdateWorker(request, options = {}) {
17145
17449
  status: result.code === 0 ? "succeeded" : "failed",
17146
17450
  finishedAt: (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
17147
17451
  delivered: false,
17148
- ...result.code === 0 ? {} : { error: `upgrade worker exited with code ${result.code}` }
17452
+ ...result.code === 0 ? {} : classifyWorkerFailure(result.code, result.stdout, result.stderr)
17149
17453
  };
17150
17454
  await saveState(request.stateFile, finished);
17151
17455
  }
@@ -17476,6 +17780,7 @@ async function startBridgeEngine(options) {
17476
17780
  const questions = new QuestionRegistry();
17477
17781
  const plans = new PlanApprovalRegistry();
17478
17782
  const densityStore = new DensityStore();
17783
+ const runCardAnchors = new RunCardAnchors();
17479
17784
  const models = new ModelStore();
17480
17785
  const wizardStore = new WizardStore();
17481
17786
  const dshConfig = new DshProviderManager({ env: process.env });
@@ -17706,6 +18011,7 @@ async function startBridgeEngine(options) {
17706
18011
  plans,
17707
18012
  densityStore,
17708
18013
  channel: streaming,
18014
+ runCardAnchors,
17709
18015
  defaultWorkspace,
17710
18016
  replyTo: first.messageId,
17711
18017
  deliverFinalReply: (replyScope, chatId, markdown2, options2) => replyDispatcher.deliver(replyScope, chatId, markdown2, options2),
@@ -17908,17 +18214,18 @@ async function startBridgeEngine(options) {
17908
18214
  error: "channel does not expose getBotIdentity"
17909
18215
  });
17910
18216
  }
17911
- streaming = adaptLarkChannel(bridge.channel);
18217
+ streaming = attachRunCardAnchors(adaptLarkChannel(bridge.channel), runCardAnchors);
17912
18218
  larkChannel = bridge.channel;
17913
18219
  const deliverUpdateResult = async () => {
17914
18220
  await updateHandoff.deliverResult(async (state) => {
17915
18221
  if (!streaming) throw new Error("channel is not ready");
18222
+ const failureHint = guardianUpdateFailureHint(state.errorCode);
17916
18223
  const markdown2 = state.status === "succeeded" ? bilingualMarkdown(
17917
18224
  `\u2705 dsh-lark-bot \u5DF2\u66F4\u65B0\u5230 \`${state.targetVersion}\`\uFF0C\u673A\u5668\u4EBA\u5DF2\u5B8C\u6210\u91CD\u8F7D\u3002`,
17918
18225
  `\u2705 dsh-lark-bot was updated to \`${state.targetVersion}\` and reloaded.`
17919
18226
  ) : bilingualMarkdown(
17920
- `\u26A0\uFE0F dsh-lark-bot \u66F4\u65B0\u5230 \`${state.targetVersion}\` \u5931\u8D25\u3002\u8BF7\u91CD\u65B0\u53D1\u9001 \`/upgrade\`\uFF1B\u5982\u4ECD\u5931\u8D25\uFF0C\u53D1\u9001 \`/doctor\` \u68C0\u67E5\u3002`,
17921
- `\u26A0\uFE0F Failed to update dsh-lark-bot to \`${state.targetVersion}\`. Send \`/upgrade\` to retry, or \`/doctor\` if it still fails.`
18227
+ `\u26A0\uFE0F dsh-lark-bot \u66F4\u65B0\u5230 \`${state.targetVersion}\` \u5931\u8D25\u3002${failureHint.zh}`,
18228
+ `\u26A0\uFE0F Failed to update dsh-lark-bot to \`${state.targetVersion}\`. ${failureHint.en}`
17922
18229
  );
17923
18230
  await streaming.sendMarkdown(state.route.chatId, markdown2, {
17924
18231
  ...state.route.threadId ? { threadId: state.route.threadId } : {}
@@ -18310,11 +18617,11 @@ async function restartProfileProcess(profile, options = {}) {
18310
18617
  // src/upgrade/runtime.ts
18311
18618
  init_own_package();
18312
18619
  import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
18313
- import { mkdir as mkdir24, rm as rm10, symlink } from "fs/promises";
18620
+ import { mkdir as mkdir24, rm as rm11, symlink } from "fs/promises";
18314
18621
  import { join as join29 } from "path";
18315
18622
  init_acp_runtime();
18316
18623
  async function defaultRelink(linkPath, target) {
18317
- await rm10(linkPath, { recursive: true, force: true });
18624
+ await rm11(linkPath, { recursive: true, force: true });
18318
18625
  await mkdir24(join29(linkPath, ".."), { recursive: true });
18319
18626
  await symlink(target, linkPath, process.platform === "win32" ? "junction" : "dir");
18320
18627
  }
@@ -20055,7 +20362,7 @@ async function uninstallGuardianCommand(options = {}) {
20055
20362
  const result = await uninstallGuardian({ env });
20056
20363
  printResult(result);
20057
20364
  }
20058
- async function statusGuardianCommand(options = {}) {
20365
+ async function statusGuardianCommand(options = {}, deps = {}) {
20059
20366
  const env = envWithOverrides(options);
20060
20367
  const paths = resolveAppPaths(env.home);
20061
20368
  const fallback = newGuardianState({
@@ -20066,6 +20373,7 @@ async function statusGuardianCommand(options = {}) {
20066
20373
  const layout = guardianLayoutFor(env, state.bridgeProfile);
20067
20374
  const heartbeat = await readHeartbeat(layout.heartbeatFile);
20068
20375
  const processFound = await findProfileProcess(state.dshProfile);
20376
+ const guardianProcess = await (deps.findGuardianProcess ?? findGuardianProcess)();
20069
20377
  const up = isHeartbeatFresh(heartbeat, env.guardianStaleMs) || processFound !== void 0;
20070
20378
  printLines([
20071
20379
  "\u5B89\u5168\u7F51\u5B88\u62A4\u72B6\u6001",
@@ -20077,7 +20385,7 @@ async function statusGuardianCommand(options = {}) {
20077
20385
  `dsh \u662F\u5426\u5728\u7EBF\uFF1A${up ? "\u662F" : "\u5426"}${processFound ? `\uFF08pid ${processFound.pid}\uFF09` : ""}`,
20078
20386
  `\u5FC3\u8DF3\u9F84\uFF1A${heartbeat ? `${heartbeatAgeMs(heartbeat)}ms` : "\u65E0"}`,
20079
20387
  `\u5DF2\u89C2\u5BDF\u8FC7 dsh \u8FD0\u884C\uFF1A${state.profileSeenUp ? "\u662F" : "\u5426"}`,
20080
- "\u5B88\u62A4\u8FDB\u7A0B pid\uFF1A\u672A\u53D1\u73B0\uFF08\u72B6\u6001\u547D\u4EE4\u4E0D\u4F1A\u628A\u81EA\u8EAB PID \u5192\u5145\u4E3A\u5B88\u62A4\u8FDB\u7A0B\uFF09",
20388
+ guardianProcess === void 0 ? "\u5B88\u62A4\u8FDB\u7A0B pid\uFF1A\u672A\u53D1\u73B0\uFF08\u65E0\u6CD5\u552F\u4E00\u8BC1\u660E resident guardian \u8EAB\u4EFD\uFF09" : `\u5B88\u62A4\u8FDB\u7A0B pid\uFF1A${guardianProcess.pid}`,
20081
20389
  `\u72B6\u6001\u6587\u4EF6\uFF1A${layout.stateFile}`,
20082
20390
  `\u5FC3\u8DF3\u6587\u4EF6\uFF1A${layout.heartbeatFile}`
20083
20391
  ]);
@@ -20320,7 +20628,7 @@ async function runServiceRuntime(options = {}, deps = {}) {
20320
20628
  // src/cli/commands/bot.ts
20321
20629
  init_dsh_runtime();
20322
20630
  import { homedir as homedir24 } from "os";
20323
- import { rm as rm11 } from "fs/promises";
20631
+ import { rm as rm12 } from "fs/promises";
20324
20632
  import { join as join34 } from "path";
20325
20633
  async function runBotCommand(action, options = {}, deps = {}) {
20326
20634
  const output = deps.output ?? ((text) => process.stdout.write(text));
@@ -20379,7 +20687,7 @@ ${rows.join("\n")}
20379
20687
  current.dshProfile,
20380
20688
  instanceEnv(sourceEnv, name, current.dshHome)
20381
20689
  ).uninstall();
20382
- await rm11(join34(paths.botDshHome(name), ".credentials.yaml"), { force: true });
20690
+ await rm12(join34(paths.botDshHome(name), ".credentials.yaml"), { force: true });
20383
20691
  await configs.removeProfile(current.bridgeProfile);
20384
20692
  await fleet.remove(name);
20385
20693
  output(
@@ -20416,7 +20724,7 @@ ${rows.join("\n")}
20416
20724
  } catch (error) {
20417
20725
  try {
20418
20726
  await service.uninstall();
20419
- await rm11(join34(dshHome, ".credentials.yaml"), { force: true });
20727
+ await rm12(join34(dshHome, ".credentials.yaml"), { force: true });
20420
20728
  await fleet.remove(name);
20421
20729
  await configs.removeProfile(name);
20422
20730
  } catch (cleanupError) {