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/dist/plugin.d.ts CHANGED
@@ -1321,9 +1321,11 @@ interface GuardianUpdateState {
1321
1321
  route: GuardianUpdateRoute;
1322
1322
  startedAt: string;
1323
1323
  finishedAt?: string;
1324
+ errorCode?: GuardianUpdateErrorCode;
1324
1325
  error?: string;
1325
1326
  delivered?: boolean;
1326
1327
  }
1328
+ type GuardianUpdateErrorCode = 'filesystem-access' | 'registry-unavailable' | 'bootstrap-unavailable' | 'upgrade-failed';
1327
1329
  interface GuardianUpdateHandoffOptions {
1328
1330
  file: string;
1329
1331
  packageName: string;
package/dist/plugin.js CHANGED
@@ -864,9 +864,9 @@ async function writeFileAtomic(target, data, options = {}) {
864
864
  }
865
865
  }
866
866
  async function rmSilently(path) {
867
- const { rm: rm10 } = await import("fs/promises");
867
+ const { rm: rm11 } = await import("fs/promises");
868
868
  try {
869
- await rm10(path, { force: true });
869
+ await rm11(path, { force: true });
870
870
  } catch {
871
871
  }
872
872
  }
@@ -2538,6 +2538,18 @@ function patchYamlFor(options) {
2538
2538
  "",
2539
2539
  "- id: hmr",
2540
2540
  " disabled: true",
2541
+ "",
2542
+ // Model-invocable channel skill: expose the dsh-lark-bot operations guide
2543
+ // to THIS agent session's skill catalog — the one the `skill` tool reads.
2544
+ // The bridge engine registers the same skill on its own cordis context
2545
+ // (plugin.ts), but that context is never the one the model reads. This row
2546
+ // ensures the skill also lands on the agent runtime's context so that
2547
+ // `skill("dsh-lark-bot")` resolves in a live SDK session. It is kept
2548
+ // outside the bridgeTools gate because it needs no callback endpoint and
2549
+ // is also useful to the guardian's core-only safe profile.
2550
+ "- insert:",
2551
+ " - id: lark-skill",
2552
+ ` name: '${own.name}/skill'`,
2541
2553
  ""
2542
2554
  ];
2543
2555
  if (bridgeTools) {
@@ -4561,7 +4573,11 @@ function processElements(state, locale, maxTools) {
4561
4573
  function thinkingPanel(state, locale, maxTools) {
4562
4574
  return {
4563
4575
  tag: "collapsible_panel",
4564
- expanded: state.terminal === "running",
4576
+ // Default to collapsed: the top-level compatibility snapshot already shows
4577
+ // the latest status and current tools, so keep the detailed per-tool list
4578
+ // collapsed to avoid two overlapping expanded surfaces. (Both sections are
4579
+ // retained for now.)
4580
+ expanded: false,
4565
4581
  header: {
4566
4582
  title: { tag: "plain_text", content: `\u2699\uFE0F ${locale === "zh_cn" ? "\u6267\u884C\u8FC7\u7A0B" : "Execution"} \xB7 ${summaryText(state, locale)}` },
4567
4583
  icon: { tag: "standard_icon", token: "down-small-ccm_outlined" },
@@ -5247,6 +5263,15 @@ async function runAttempt(input, cwd, workspaceCwd, sessionId, resuming, replyOp
5247
5263
  log.warn("run-flow", "card-update-failed", { scope: input.scope, error });
5248
5264
  }
5249
5265
  };
5266
+ const unregisterReanchor = input.runCardAnchors?.register(input.chatId, async () => {
5267
+ if (state.terminal !== "running") return;
5268
+ try {
5269
+ await controller.update(renderer(state, density, Date.now()));
5270
+ if (typeof controller.reanchor === "function") await controller.reanchor();
5271
+ } catch (error) {
5272
+ log.warn("run-flow", "card-reanchor-failed", { scope: input.scope, error });
5273
+ }
5274
+ });
5250
5275
  const showResumeRecovery = async (error) => {
5251
5276
  if (classifySessionError(errorMessage(error)) === void 0) throw error;
5252
5277
  resumeFailure = errorMessage(error);
@@ -5420,6 +5445,7 @@ async function runAttempt(input, cwd, workspaceCwd, sessionId, resuming, replyOp
5420
5445
  unsubscribeQuestion?.();
5421
5446
  unsubscribePlan?.();
5422
5447
  unsubscribeApproval?.();
5448
+ unregisterReanchor?.();
5423
5449
  }
5424
5450
  };
5425
5451
  let producerStarted = false;
@@ -9203,6 +9229,41 @@ var ResilientCardStreamController = class {
9203
9229
  });
9204
9230
  }
9205
9231
  }
9232
+ /**
9233
+ * Recall the current card and re-create it as the newest top-level message in
9234
+ * the chat, rebinding the controller to the fresh message id. Because Feishu
9235
+ * cannot reorder an existing message, this is the only way to keep an
9236
+ * in-progress process card visible at the tail while interim agent bubbles are
9237
+ * appended below. Best-effort: if the recall fails the card is left where it
9238
+ * is (no duplicate is created); if the controller is closed or failed the id
9239
+ * is left unchanged.
9240
+ */
9241
+ async reanchor() {
9242
+ if (this.closed || this.failed) return this.messageId;
9243
+ if (this.inFlight !== void 0) await this.inFlight;
9244
+ if (this.timer !== void 0) {
9245
+ clearTimeout(this.timer);
9246
+ this.timer = void 0;
9247
+ }
9248
+ const card = this.latest;
9249
+ if (card === void 0) return this.messageId;
9250
+ try {
9251
+ await this.channel.recallMessage(this.messageId);
9252
+ } catch (error) {
9253
+ log.warn("lark-card-stream", "reanchor-recall-failed", {
9254
+ messageId: this.messageId,
9255
+ error: error instanceof Error ? error.message : String(error)
9256
+ });
9257
+ return this.messageId;
9258
+ }
9259
+ const sent = await this.channel.send(this.chatId, { card }, {});
9260
+ if (!sent.messageId) throw new Error("Feishu card re-anchor returned no message_id");
9261
+ this.messageId = sent.messageId;
9262
+ this.latest = card;
9263
+ this.dirty = false;
9264
+ this.failed = false;
9265
+ return this.messageId;
9266
+ }
9206
9267
  };
9207
9268
  function adaptLarkChannel(channel) {
9208
9269
  const base = {
@@ -11446,6 +11507,63 @@ function wizardContextFor(event, deps, channel, scope) {
11446
11507
  };
11447
11508
  }
11448
11509
 
11510
+ // src/bridge/run-card-anchors.ts
11511
+ var RunCardAnchors = class {
11512
+ byChatId = /* @__PURE__ */ new Map();
11513
+ /**
11514
+ * Register a re-anchor callback for a chat. Returns a disposer; safe to call
11515
+ * more than once (idempotent).
11516
+ */
11517
+ register(chatId, reanchor) {
11518
+ const set = this.byChatId.get(chatId) ?? /* @__PURE__ */ new Set();
11519
+ set.add(reanchor);
11520
+ this.byChatId.set(chatId, set);
11521
+ let active = true;
11522
+ return () => {
11523
+ if (!active) return;
11524
+ active = false;
11525
+ set.delete(reanchor);
11526
+ if (set.size === 0) this.byChatId.delete(chatId);
11527
+ };
11528
+ }
11529
+ /** Notify that a new bubble was delivered to the chat. */
11530
+ async bubbleSent(chatId) {
11531
+ const set = this.byChatId.get(chatId);
11532
+ if (!set || set.size === 0) return;
11533
+ await Promise.allSettled([...set].map((reanchor) => reanchor()));
11534
+ }
11535
+ };
11536
+ function attachRunCardAnchors(channel, anchors) {
11537
+ const notify = (chatId) => {
11538
+ void anchors.bubbleSent(chatId).catch((error) => {
11539
+ log.warn("run-card-anchors", "bubble-sent-failed", {
11540
+ chatId,
11541
+ error: error instanceof Error ? error.message : String(error)
11542
+ });
11543
+ });
11544
+ };
11545
+ return {
11546
+ ...channel,
11547
+ async sendMarkdown(chatId, markdown2, options) {
11548
+ const result = await channel.sendMarkdown(chatId, markdown2, options);
11549
+ notify(chatId);
11550
+ return result;
11551
+ },
11552
+ async sendCard(chatId, card, options) {
11553
+ if (typeof channel.sendCard !== "function") return void 0;
11554
+ const result = await channel.sendCard(chatId, card, options);
11555
+ notify(chatId);
11556
+ return result;
11557
+ },
11558
+ async sendFile(chatId, fileName, content2, options) {
11559
+ if (typeof channel.sendFile !== "function") return void 0;
11560
+ const result = await channel.sendFile(chatId, fileName, content2, options);
11561
+ notify(chatId);
11562
+ return result;
11563
+ }
11564
+ };
11565
+ }
11566
+
11449
11567
  // src/bridge/scope-directory.ts
11450
11568
  import { readFile as readFile11 } from "fs/promises";
11451
11569
  var ScopeDirectory = class {
@@ -15008,14 +15126,14 @@ function matchProfileProcess(cmdline, dshProfile) {
15008
15126
  if (/(?:^|\s)plugin(?:\s|$)/.test(cmdline)) return false;
15009
15127
  return hasProfileFlag(cmdline, dshProfile) && looksLikeDshProcess(cmdline);
15010
15128
  }
15011
- async function listProcesses() {
15012
- if (process.platform === "win32") {
15013
- return listProcessesWindows();
15129
+ async function listProcesses(platform = process.platform, run = captureOutput) {
15130
+ if (platform === "win32") {
15131
+ return listProcessesWindows(run);
15014
15132
  }
15015
- return listProcessesPosix();
15133
+ return listProcessesPosix(run);
15016
15134
  }
15017
- async function listProcessesPosix() {
15018
- const { stdout } = await captureOutput("ps", ["-axo", "pid=,args="], 1e4);
15135
+ async function listProcessesPosix(run) {
15136
+ const { stdout } = await run("ps", ["-axo", "pid=,args="], 1e4);
15019
15137
  const result = [];
15020
15138
  for (const line of stdout.split("\n")) {
15021
15139
  const match = /^\s*(\d+)\s+(.+)$/.exec(line);
@@ -15025,33 +15143,46 @@ async function listProcessesPosix() {
15025
15143
  }
15026
15144
  return result;
15027
15145
  }
15028
- async function listProcessesWindows() {
15029
- const { stdout } = await captureOutput(
15146
+ async function listProcessesWindows(run) {
15147
+ const { stdout } = await run(
15030
15148
  "powershell.exe",
15031
15149
  [
15032
15150
  "-NoProfile",
15033
15151
  "-NonInteractive",
15034
15152
  "-Command",
15035
- "Get-CimInstance Win32_Process | Select-Object ProcessId, CommandLine | ConvertTo-Csv -NoTypeInformation"
15153
+ "Get-CimInstance Win32_Process | Select-Object ProcessId, CommandLine | ConvertTo-Json -Compress"
15036
15154
  ],
15037
15155
  1e4
15038
15156
  );
15039
- const result = [];
15040
- for (const line of stdout.split("\n").slice(1)) {
15041
- const [pid, ...rest] = line.trim().split(",");
15042
- const numeric = Number(pid?.replace(/"/g, ""));
15043
- if (Number.isInteger(numeric) && rest.length > 0) {
15044
- result.push({ pid: numeric, cmdline: rest.join(",").replace(/^"|"$/g, "") });
15045
- }
15157
+ try {
15158
+ const parsed = JSON.parse(stdout);
15159
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
15160
+ return rows.flatMap((row) => {
15161
+ if (typeof row !== "object" || row === null) return [];
15162
+ const record = row;
15163
+ const pid = Number(record.ProcessId);
15164
+ const cmdline = record.CommandLine;
15165
+ return Number.isInteger(pid) && pid > 0 && typeof cmdline === "string" ? [{ pid, cmdline }] : [];
15166
+ });
15167
+ } catch {
15168
+ return [];
15046
15169
  }
15047
- return result;
15048
15170
  }
15049
- async function captureOutput(command, args, timeoutMs = 3e4) {
15171
+ async function captureOutput(command, args, timeoutMs = 3e4, options = {}) {
15050
15172
  return new Promise((resolve6) => {
15051
- const child = spawn5(command, [...args], {
15052
- stdio: ["ignore", "pipe", "pipe"],
15053
- windowsHide: true
15054
- });
15173
+ const previousUmask = options.umask !== void 0 && process.platform !== "win32" ? process.umask(options.umask) : void 0;
15174
+ const child = (() => {
15175
+ try {
15176
+ return spawn5(command, [...args], {
15177
+ stdio: ["ignore", "pipe", "pipe"],
15178
+ windowsHide: true,
15179
+ ...options.cwd ? { cwd: options.cwd } : {},
15180
+ ...options.env ? { env: options.env } : {}
15181
+ });
15182
+ } finally {
15183
+ if (previousUmask !== void 0) process.umask(previousUmask);
15184
+ }
15185
+ })();
15055
15186
  let stdout = "";
15056
15187
  let stderr = "";
15057
15188
  child.stdout?.on("data", (chunk) => {
@@ -16374,11 +16505,35 @@ function buildSecretHandler(deps) {
16374
16505
  init_own_package();
16375
16506
 
16376
16507
  // src/guardian/update-handoff.ts
16377
- import { randomUUID as randomUUID13 } from "crypto";
16508
+ import { createHash as createHash3, randomUUID as randomUUID13 } from "crypto";
16378
16509
  import { spawn as spawn7 } from "child_process";
16379
- import { mkdir as mkdir20, readFile as readFile28 } from "fs/promises";
16510
+ import { chmod, mkdir as mkdir20, readFile as readFile28, rm as rm10 } from "fs/promises";
16380
16511
  import { dirname as dirname15, join as join23 } from "path";
16381
16512
  init_own_package();
16513
+ function guardianUpdateFailureHint(code) {
16514
+ switch (code) {
16515
+ case "filesystem-access":
16516
+ return {
16517
+ 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",
16518
+ en: "The private update workspace is inaccessible. Send `/doctor` to check file permissions, then retry."
16519
+ };
16520
+ case "registry-unavailable":
16521
+ return {
16522
+ zh: "\u65E0\u6CD5\u8FDE\u63A5 npm \u6B63\u5F0F\u6E90\uFF1B\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\u3002",
16523
+ en: "The npm registry is unreachable. Check the network, then retry."
16524
+ };
16525
+ case "bootstrap-unavailable":
16526
+ return {
16527
+ zh: "\u65E0\u6CD5\u542F\u52A8 npm/npx\uFF1B\u8BF7\u53D1\u9001 `/doctor` \u68C0\u67E5 Node.js \u4E0E npm\u3002",
16528
+ en: "npm/npx could not start. Send `/doctor` to check Node.js and npm."
16529
+ };
16530
+ default:
16531
+ return {
16532
+ zh: "\u5347\u7EA7\u547D\u4EE4\u672A\u5B8C\u6210\uFF1B\u8BF7\u53D1\u9001 `/doctor` \u68C0\u67E5\u540E\u91CD\u8BD5\u3002",
16533
+ en: "The upgrade command did not complete. Send `/doctor`, then retry."
16534
+ };
16535
+ }
16536
+ }
16382
16537
  async function loadState(file) {
16383
16538
  try {
16384
16539
  const parsed = JSON.parse(await readFile28(file, "utf8"));
@@ -16734,6 +16889,7 @@ async function startBridgeEngine(options) {
16734
16889
  const questions = new QuestionRegistry();
16735
16890
  const plans = new PlanApprovalRegistry();
16736
16891
  const densityStore = new DensityStore();
16892
+ const runCardAnchors = new RunCardAnchors();
16737
16893
  const models = new ModelStore();
16738
16894
  const wizardStore = new WizardStore();
16739
16895
  const dshConfig = new DshProviderManager({ env: process.env });
@@ -16964,6 +17120,7 @@ async function startBridgeEngine(options) {
16964
17120
  plans,
16965
17121
  densityStore,
16966
17122
  channel: streaming,
17123
+ runCardAnchors,
16967
17124
  defaultWorkspace,
16968
17125
  replyTo: first.messageId,
16969
17126
  deliverFinalReply: (replyScope, chatId, markdown2, options2) => replyDispatcher.deliver(replyScope, chatId, markdown2, options2),
@@ -17166,17 +17323,18 @@ async function startBridgeEngine(options) {
17166
17323
  error: "channel does not expose getBotIdentity"
17167
17324
  });
17168
17325
  }
17169
- streaming = adaptLarkChannel(bridge.channel);
17326
+ streaming = attachRunCardAnchors(adaptLarkChannel(bridge.channel), runCardAnchors);
17170
17327
  larkChannel = bridge.channel;
17171
17328
  const deliverUpdateResult = async () => {
17172
17329
  await updateHandoff.deliverResult(async (state) => {
17173
17330
  if (!streaming) throw new Error("channel is not ready");
17331
+ const failureHint = guardianUpdateFailureHint(state.errorCode);
17174
17332
  const markdown2 = state.status === "succeeded" ? bilingualMarkdown(
17175
17333
  `\u2705 dsh-lark-bot \u5DF2\u66F4\u65B0\u5230 \`${state.targetVersion}\`\uFF0C\u673A\u5668\u4EBA\u5DF2\u5B8C\u6210\u91CD\u8F7D\u3002`,
17176
17334
  `\u2705 dsh-lark-bot was updated to \`${state.targetVersion}\` and reloaded.`
17177
17335
  ) : bilingualMarkdown(
17178
- `\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`,
17179
- `\u26A0\uFE0F Failed to update dsh-lark-bot to \`${state.targetVersion}\`. Send \`/upgrade\` to retry, or \`/doctor\` if it still fails.`
17336
+ `\u26A0\uFE0F dsh-lark-bot \u66F4\u65B0\u5230 \`${state.targetVersion}\` \u5931\u8D25\u3002${failureHint.zh}`,
17337
+ `\u26A0\uFE0F Failed to update dsh-lark-bot to \`${state.targetVersion}\`. ${failureHint.en}`
17180
17338
  );
17181
17339
  await streaming.sendMarkdown(state.route.chatId, markdown2, {
17182
17340
  ...state.route.threadId ? { threadId: state.route.threadId } : {}