dsh-lark-bot 0.19.5 → 0.19.7

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
@@ -162,7 +162,9 @@ Markdown、toast 与旧客户端降级路径同时显示中英文。agent 最终
162
162
 
163
163
  飞书消息中的图片会按文件内容识别 PNG/JPEG/WebP/GIF 并补全安全扩展名;默认 SDK 会经 dsh
164
164
  附件存储校验后发送原生 image block,而不是把路径当作图片。无法读取或模型不支持视觉时会明确
165
- 失败,agent 被要求不得用工作区内其他图片替代。文本类文件会读取内容并注入任务上下文。
165
+ 失败,agent 被要求不得用工作区内其他图片替代。选中的 `deepseek-official` 视觉模型会在每轮运行前
166
+ 幂等补入 dsh runtime 实际消费的模型目录并声明 `text,image`,避免展示目录已识别视觉能力但 rc.8
167
+ runtime 仍按 text-only 拒绝。文本类文件会读取内容并注入任务上下文。
166
168
  `/model` 卡片会把 dsh 默认模型并入可切换目录(即使 provider 的显式列表尚未包含它),
167
169
  并用去除公共前缀后的短标签、每行最多两个按钮适配移动端。provider 名称、模型、输入模态和
168
170
  推理档位从 models.dev 运行时目录发现(15 分钟内存缓存);网络失败时只使用 dsh settings 中的
@@ -354,9 +356,16 @@ npx dsh-lark-bot@latest upgrade --profile dsh-lark --yes
354
356
  - `--no-guardian`:跳过守护升级;
355
357
  - **runtime profile 一致性修复**:升级后自动把 `dsh-lark-sdk` / `dsh-lark-acp` 的
356
358
  own-package 链接重指到新版本,并当场强制刷新 runtime profile 及被链接主插件中版本陈旧或物理
357
- 内容损坏的 SDK server / ACP 依赖;
358
- - **受管服务安全重载**:更新 worker 的临时 callback URL/token npx cache PATH 不会写入持久
359
- service env;只有飞书通道、callback server Guardian 心跳均已就绪后才确认更新成功。
359
+ 内容损坏的 SDK server / ACP 依赖;修复目标以 dsh profile 中重新读取并验证的安装包为准,
360
+ 不会链接到临时 npx worker。npm/npx 扁平依赖按 Node 的真实模块解析结果校验;pnpm 安装则先
361
+ 解引用 profile 中的包链接,再从 `.pnpm` 物理包目录解析其依赖,避免误判缺失并强制重装;
362
+ - **托管环境 pnpm 一致性**:执行 `dsh plugin add` 前读取既有 profile 的
363
+ `node_modules/.modules.yaml`,把其中记录的精确 pnpm 版本同步到 profile `package.json`,避免
364
+ Corepack 因 source-managed dsh 的工作目录选择另一 pnpm 主版本并触发
365
+ `ERR_PNPM_UNEXPECTED_STORE`;全新 profile 不会被提前创建 manifest;
366
+ - **受管服务安全重载**:更新 worker 的临时 callback URL/token 与 npx/npm package-bin PATH 不会写入持久
367
+ service env 或 Guardian unit;Guardian 仅保留 Node 所在目录及稳定的用户/系统 PATH。只有飞书通道、
368
+ callback server 和 Guardian 心跳均已就绪后才确认更新成功。
360
369
 
361
370
  无需交互确认时加 `--yes`(非交互环境不带 `--yes` 会安全中止)。其余方式:
362
371
 
package/README_EN.md CHANGED
@@ -94,7 +94,7 @@ question-card buttons. If an existing app was created by an older flow, enable c
94
94
  Console → Events & Callbacks → Callback Configuration and publish the app again. Without it, messages still work
95
95
  while card clicks never reach the bot.
96
96
 
97
- `setup` automatically: locates your local dsh → pre-approves pnpm's build policy (protobufjs) → runs the standard `dsh plugin --profile dsh-lark add dsh-lark-bot@<version>` (pinned to the running package) → installs the safety-net guardian system service. One command installs everything.
97
+ `setup` automatically: locates your local dsh → pre-approves pnpm's build policy (protobufjs; an existing profile is also pinned to the exact pnpm release recorded by its install metadata, preventing Corepack/store-major conflicts) → runs the standard `dsh plugin --profile dsh-lark add dsh-lark-bot@<version>` (pinned to the running package) → installs the safety-net guardian system service. A fresh profile is still initialized by dsh. One command installs everything.
98
98
 
99
99
  > **No public IP / domain / server / tunneling required** (Feishu outbound WebSocket long connection); works on Linux / macOS / Windows.
100
100
  > With an existing PersonalAgent app you can skip the QR step (see Configuration): `DSH_LARK_APP_ID=cli_xxx DSH_LARK_APP_SECRET=<secret> DSH_LARK_TENANT=feishu dsh --profile dsh-lark`
@@ -165,7 +165,10 @@ to the cloud LLM.
165
165
  Feishu images are detected by content as PNG/JPEG/WebP/GIF and receive a safe extension. The default
166
166
  SDK validates them through dsh's attachment store and sends native image blocks instead of path text.
167
167
  Unreadable or unsupported images fail explicitly, and the agent is instructed never to substitute another
168
- workspace image. Text files are read and injected into the task context.
168
+ workspace image. Before each run, the selected `deepseek-official` vision model is idempotently added to the
169
+ model catalogue consumed by the dsh runtime with `text,image` modalities. This prevents rc.8 from treating the
170
+ model as text-only even when the display catalogue already recognizes its vision capability. Text files are read
171
+ and injected into the task context.
169
172
  The `/model` card merges the dsh default into its switchable catalogue even when a provider's explicit
170
173
  list omits it, and uses compact distinguishing labels with at most two buttons per mobile row. Provider
171
174
  names, models, input modalities, and reasoning-effort options are discovered from the models.dev runtime
@@ -358,12 +361,16 @@ npx dsh-lark-bot@latest upgrade --profile dsh-lark --yes
358
361
  - `--rollback`: reinstall the version recorded before the last upgrade (`~/.dsh-lark/upgrade-state.json`);
359
362
  - `--force`: reinstall the running version when npm is unreachable (offline);
360
363
  - `--no-guardian`: skip the guardian upgrade;
361
- - **Runtime-profile consistency repair**: after upgrading, the own-package links of `dsh-lark-sdk` / `dsh-lark-acp` are re-pointed and stale SDK-server / ACP dependencies are idempotently reinstalled immediately.
364
+ - **Runtime-profile consistency repair**: after `dsh plugin add`, the upgrader re-reads and verifies the exact target installed in the dsh profile, then re-points `dsh-lark-sdk` / `dsh-lark-acp` only to that stable package root—never to the transient npx worker—and refreshes stale SDK-server / ACP dependencies.
365
+ - **npm/npx/pnpm layout support**: npm/npx-flat dependencies use Node's actual module resolution; pnpm package links are resolved to their physical `.pnpm` target before dependency validation, avoiding false corruption reports and unnecessary forced installs.
366
+ - **Corepack-managed dsh compatibility**: before installing into an existing profile, the exact pnpm release from `node_modules/.modules.yaml` is persisted to `package.json#packageManager`, preventing a source-managed dsh working directory from selecting an incompatible pnpm store major.
362
367
 
363
368
  Pass `--yes` to skip the interactive confirmation (non-interactive runs fail closed without it). Alternatives:
364
369
 
365
370
  - Plugin: re-run `setup` (or `dsh plugin --profile <name> add dsh-lark-bot`) to pull the latest npm release.
366
371
  - Safety-net guardian: installed / upgraded together with `upgrade` / `setup` (idempotent), or standalone via `dsh-lark-bot guardian install`.
372
+ - Managed reloads never persist the update worker's npx/npm package-bin paths into the service environment or
373
+ Guardian unit. Guardian keeps only the Node directory and stable user/system PATH entries.
367
374
  - CLI tool (optional): `npm i -g dsh-lark-bot@latest`; not needed when using `npx`.
368
375
  - Restart the profile after upgrading (when not using `--restart`): `dsh --profile dsh-lark`.
369
376
 
package/dist/cli.js CHANGED
@@ -282,8 +282,7 @@ import { join as join7 } from "path";
282
282
  function acpProfileRoot(home, profile, env) {
283
283
  return join7(resolveDshHome(home, env), "profiles", profile);
284
284
  }
285
- function packageJsonFor2(profile) {
286
- const own = ownPackageInfo();
285
+ function packageJsonFor2(profile, own = ownPackageInfo()) {
287
286
  return `${JSON.stringify(
288
287
  {
289
288
  name: `dsh-profile-${profile}`,
@@ -372,12 +371,11 @@ function acpPatchYaml(provider, model) {
372
371
  ""
373
372
  ].join("\n");
374
373
  }
375
- function isAcpProfileReady(profileRoot) {
376
- const own = ownPackageInfo();
374
+ function isAcpProfileReady(profileRoot, own = ownPackageInfo()) {
377
375
  return existsSync4(join7(profileRoot, "package.json")) && existsSync4(join7(profileRoot, "cordis.yml")) && existsSync4(join7(profileRoot, "cordis.patch.yml")) && profilePackageMatches(profileRoot, ACP_PACKAGE, ACP_VERSION) && ownPackageLinked2(profileRoot, own);
378
376
  }
379
- function isAcpManagedProfileCurrent(profileRoot, provider, model) {
380
- if (!isAcpProfileReady(profileRoot)) return false;
377
+ function isAcpManagedProfileCurrent(profileRoot, provider, model, own = ownPackageInfo()) {
378
+ if (!isAcpProfileReady(profileRoot, own)) return false;
381
379
  try {
382
380
  return readFileSync4(join7(profileRoot, "cordis.patch.yml"), "utf8") === acpPatchYaml(provider, model);
383
381
  } catch {
@@ -423,10 +421,11 @@ ${tail}`));
423
421
  }
424
422
  async function ensureAcpProfile(options) {
425
423
  const profile = options.profile ?? DEFAULT_ACP_PROFILE;
424
+ const own = options.ownPackage ?? ownPackageInfo();
426
425
  const root = acpProfileRoot(options.home, profile, options.env);
427
426
  const provider = options.provider?.trim();
428
427
  const model = options.model?.trim();
429
- const ready = isAcpProfileReady(root);
428
+ const ready = isAcpProfileReady(root, own);
430
429
  try {
431
430
  if (!provider || !model) {
432
431
  throw new Error(
@@ -434,14 +433,14 @@ async function ensureAcpProfile(options) {
434
433
  );
435
434
  }
436
435
  await mkdir4(root, { recursive: true });
437
- await writeFile4(join7(root, "package.json"), packageJsonFor2(profile), "utf8");
436
+ await writeFile4(join7(root, "package.json"), packageJsonFor2(profile, own), "utf8");
438
437
  await writeFile4(join7(root, "cordis.yml"), "[]\n", "utf8");
439
438
  await writeFile4(join7(root, "cordis.patch.yml"), acpPatchYaml(provider, model), "utf8");
440
439
  if (!ready) {
441
440
  const install = options.install ?? runPnpmInstall2;
442
441
  await install(root, { force: true });
443
442
  }
444
- if (!isAcpProfileReady(root)) {
443
+ if (!isAcpProfileReady(root, own)) {
445
444
  return {
446
445
  ok: false,
447
446
  created: true,
@@ -1094,7 +1093,7 @@ import { homedir as homedir3 } from "os";
1094
1093
  import { mkdir as mkdir2, readFile, rm, writeFile as writeFile2 } from "fs/promises";
1095
1094
  import { homedir } from "os";
1096
1095
  import { dirname as dirname3, join as join3 } from "path";
1097
- import { Document, parseDocument } from "yaml";
1096
+ import { Document, isMap, isSeq, parseDocument } from "yaml";
1098
1097
 
1099
1098
  // src/platform/atomic-write.ts
1100
1099
  import { randomBytes } from "crypto";
@@ -1564,6 +1563,22 @@ function patchNode(document, path, current, next) {
1564
1563
  }
1565
1564
  if (!deepEqualJson(current, next)) document.setIn([...path], next);
1566
1565
  }
1566
+ function patchRuntimeModelModalities(document, modelId, modalities) {
1567
+ const modelsPath = [DEEPSEEK_NAMESPACE, "models"];
1568
+ const modelsNode = document.getIn(modelsPath, true);
1569
+ if (!isSeq(modelsNode)) {
1570
+ document.setIn(modelsPath, [{ id: modelId, inputModalities: modalities }]);
1571
+ return;
1572
+ }
1573
+ const modelIndex = modelsNode.items.findIndex(
1574
+ (item) => isMap(item) && item.get("id") === modelId
1575
+ );
1576
+ if (modelIndex === -1) {
1577
+ modelsNode.add({ id: modelId, inputModalities: modalities });
1578
+ return;
1579
+ }
1580
+ document.setIn([...modelsPath, modelIndex, "inputModalities"], modalities);
1581
+ }
1567
1582
  function parseYamlMap(text, filename) {
1568
1583
  if (text === void 0 || text.trim().length === 0) return {};
1569
1584
  const document = parseDocument(text, { prettyErrors: true });
@@ -1753,6 +1768,12 @@ var DshProviderManager = class {
1753
1768
  if (!provider) return void 0;
1754
1769
  return { provider: provider.id, model: selection };
1755
1770
  }
1771
+ /** Resolve a model route and make its managed runtime catalog ready. */
1772
+ async resolveRuntimeModelRoute(selection) {
1773
+ const route = await this.resolveModelRoute(selection);
1774
+ if (route !== void 0) await this.ensureRuntimeModelModalities(route);
1775
+ return route;
1776
+ }
1756
1777
  async setDefaultModel(model) {
1757
1778
  if (!model.trim()) throw new Error("\u9ED8\u8BA4\u6A21\u578B\u4E0D\u80FD\u4E3A\u7A7A");
1758
1779
  const route = await this.resolveModelRoute(model);
@@ -1800,6 +1821,57 @@ var DshProviderManager = class {
1800
1821
  models: [...models, modelRecord(input)]
1801
1822
  });
1802
1823
  }
1824
+ /**
1825
+ * Ensure the selected DeepSeek vision model is present in the catalog that
1826
+ * the managed SDK/ACP runtime actually consumes. The upstream DeepSeek
1827
+ * adapter treats an unlisted model as text-only even when its id identifies
1828
+ * a vision endpoint, so read-time normalization alone is insufficient.
1829
+ * Returns true only when settings were changed.
1830
+ */
1831
+ async ensureRuntimeModelModalities(route) {
1832
+ if (route.provider !== DEEPSEEK_PROVIDER || !isVisionModelId(route.model)) return false;
1833
+ await mkdir2(dirname3(this.settingsFile), { recursive: true });
1834
+ let changed = false;
1835
+ await withFileLock(this.settingsFile, async () => {
1836
+ const text = await readOptional(this.settingsFile);
1837
+ const root = parseYamlMap(text, this.settingsFile);
1838
+ const current = isMapLike(root[DEEPSEEK_NAMESPACE]) ? root[DEEPSEEK_NAMESPACE] : {};
1839
+ const models = rawModels(current.models);
1840
+ const index = models.findIndex((model) => model.id === route.model);
1841
+ const existing = index === -1 ? void 0 : models[index];
1842
+ const configured = Array.isArray(existing?.inputModalities) ? existing.inputModalities.filter(
1843
+ (modality) => modality === "text" || modality === "image"
1844
+ ) : void 0;
1845
+ const modalities = normalizeVisionModelInputModalities(
1846
+ route.model,
1847
+ configured,
1848
+ false
1849
+ );
1850
+ if (existing !== void 0 && deepEqualJson(existing.inputModalities, modalities)) return;
1851
+ const nextModel = { ...existing ?? { id: route.model }, inputModalities: modalities };
1852
+ const section = {
1853
+ ...current,
1854
+ models: index === -1 ? [...models, nextModel] : models.map((model, modelIndex) => modelIndex === index ? nextModel : model)
1855
+ };
1856
+ if (text === void 0 || text.trim().length === 0) {
1857
+ await writeFileAtomic(
1858
+ this.settingsFile,
1859
+ new Document({ [DEEPSEEK_NAMESPACE]: section }).toString(),
1860
+ {}
1861
+ );
1862
+ changed = true;
1863
+ return;
1864
+ }
1865
+ const document = parseDocument(text);
1866
+ if (document.errors.length > 0) {
1867
+ throw new Error(`invalid dsh settings at ${this.settingsFile}`);
1868
+ }
1869
+ patchRuntimeModelModalities(document, route.model, modalities);
1870
+ await writeFileAtomic(this.settingsFile, document.toString(), {});
1871
+ changed = true;
1872
+ });
1873
+ return changed;
1874
+ }
1803
1875
  async removeDeepseekModel(id) {
1804
1876
  const settings = await this.readSettings();
1805
1877
  const current = isMapLike(settings[DEEPSEEK_NAMESPACE]) ? settings[DEEPSEEK_NAMESPACE] : {};
@@ -2765,6 +2837,7 @@ init_bridge_persona();
2765
2837
  init_own_package();
2766
2838
  init_profile_package();
2767
2839
  import { spawn as spawn3 } from "cross-spawn";
2840
+ import { createRequire } from "module";
2768
2841
  import { existsSync as existsSync3, realpathSync as realpathSync2 } from "fs";
2769
2842
  import { readFileSync as readFileSync3 } from "fs";
2770
2843
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
@@ -2900,7 +2973,24 @@ function isSdkManagedProfileCurrent(profileRoot, options, own = ownPackageInfo()
2900
2973
  return isSdkProfileReady(profileRoot, own) && readFileIfPresent(join6(profileRoot, "cordis.patch.yml")) === patchYamlFor(options);
2901
2974
  }
2902
2975
  function ownSdkServerDependencyReady(own) {
2903
- return profilePackageMatches(own.root, SDK_SERVER_PACKAGE, SDK_SERVER_VERSION);
2976
+ try {
2977
+ const physicalOwnRoot = realpathSync2(own.root);
2978
+ const entry = createRequire(join6(physicalOwnRoot, "package.json")).resolve(SDK_SERVER_PACKAGE);
2979
+ let current = dirname5(entry);
2980
+ for (let depth = 0; depth < 8; depth += 1) {
2981
+ try {
2982
+ const parsed = JSON.parse(readFileSync3(join6(current, "package.json"), "utf8"));
2983
+ if (parsed.name === SDK_SERVER_PACKAGE) return parsed.version === SDK_SERVER_VERSION;
2984
+ } catch {
2985
+ }
2986
+ const parent = dirname5(current);
2987
+ if (parent === current) return false;
2988
+ current = parent;
2989
+ }
2990
+ return false;
2991
+ } catch {
2992
+ return false;
2993
+ }
2904
2994
  }
2905
2995
  function findPnpmProjectRoot(start) {
2906
2996
  let current = start;
@@ -4141,6 +4231,20 @@ function systemdUnit(nodeBin, cliEntry, env = {}) {
4141
4231
  ""
4142
4232
  ].join("\n");
4143
4233
  }
4234
+ function stableGuardianServicePath(nodeBin, inheritedPath = process.env.PATH) {
4235
+ const entries = [dirname9(nodeBin), ...inheritedPath?.split(delimiter) ?? []];
4236
+ const seen = /* @__PURE__ */ new Set();
4237
+ return entries.filter((entry) => {
4238
+ if (!entry) return false;
4239
+ const normalized = entry.replaceAll("\\", "/");
4240
+ if (normalized.includes("/node_modules/.bin") || /\/_npx(?:\/|$)/.test(normalized) || /\/guardian\/update-worker\/npm-cache(?:\/|$)/.test(normalized)) {
4241
+ return false;
4242
+ }
4243
+ if (seen.has(entry)) return false;
4244
+ seen.add(entry);
4245
+ return true;
4246
+ }).join(delimiter);
4247
+ }
4144
4248
  function launchdPlist(nodeBin, cliEntry, label2 = `io.dsh-lark.${GUARDIAN_LABEL}`, logPath = "/tmp/dsh-lark-guardian.log") {
4145
4249
  return [
4146
4250
  '<?xml version="1.0" encoding="UTF-8"?>',
@@ -4203,7 +4307,7 @@ async function installGuardian(options) {
4203
4307
  await writeFile5(
4204
4308
  unitPath,
4205
4309
  systemdUnit(nodeBin, cliEntry, {
4206
- PATH: [dirname9(nodeBin), process.env.PATH].filter((value) => Boolean(value)).join(delimiter),
4310
+ PATH: stableGuardianServicePath(nodeBin),
4207
4311
  DSH_LARK_GUARDIAN_DISABLED: "0"
4208
4312
  }),
4209
4313
  "utf8"
@@ -8166,7 +8270,7 @@ async function runAttempt(input, cwd, workspaceCwd, sessionId, resuming, replyOp
8166
8270
  const consume = async () => {
8167
8271
  for await (const event of run.events) {
8168
8272
  if (timedOut) return;
8169
- if (resuming && !sawActivity && event.type === "error" && event.terminationReason === "failed") {
8273
+ if (resuming && !sawActivity && event.type === "error" && event.terminationReason === "failed" && classifySessionError(event.message) !== void 0) {
8170
8274
  await showResumeRecovery(event.message);
8171
8275
  return;
8172
8276
  }
@@ -8261,10 +8365,19 @@ async function runAttempt(input, cwd, workspaceCwd, sessionId, resuming, replyOp
8261
8365
  try {
8262
8366
  await consume();
8263
8367
  } catch (error) {
8264
- if (resuming && !sawActivity) {
8368
+ if (resuming && !sawActivity && classifySessionError(errorMessage(error)) !== void 0) {
8265
8369
  await showResumeRecovery(error);
8266
8370
  return;
8267
8371
  }
8372
+ if (state.terminal === "running") {
8373
+ state = applyEvent(state, {
8374
+ type: "error",
8375
+ message: errorMessage(error),
8376
+ terminationReason: "failed"
8377
+ }, stopRequested);
8378
+ state = { ...state, lastActivityMs: Date.now() };
8379
+ await safeUpdate();
8380
+ }
8268
8381
  throw error;
8269
8382
  }
8270
8383
  };
@@ -8369,7 +8482,7 @@ async function runAttempt(input, cwd, workspaceCwd, sessionId, resuming, replyOp
8369
8482
  if (resumeFailure !== void 0) {
8370
8483
  throw new Error(resumeFailure);
8371
8484
  }
8372
- if (resuming && state.terminal === "error" && !sawActivity) {
8485
+ if (resuming && state.terminal === "error" && !sawActivity && classifySessionError(state.errorMsg ?? "") !== void 0) {
8373
8486
  throw new Error(state.errorMsg ?? "native session resume failed");
8374
8487
  }
8375
8488
  input.sessions.recordExchange(input.scope, workspaceCwd, input.messages, assistantOutput, {
@@ -8384,14 +8497,14 @@ async function runAttempt(input, cwd, workspaceCwd, sessionId, resuming, replyOp
8384
8497
  } : {}
8385
8498
  });
8386
8499
  } catch (error) {
8387
- state = markInterrupted(state);
8388
- if (resuming && !sawActivity) {
8500
+ const runErrorText = errorMessage(error);
8501
+ const healKind = classifySessionError(runErrorText);
8502
+ if (resuming && !sawActivity && healKind !== void 0) {
8389
8503
  log.warn("run-flow", "resume-attempt-failed", { scope: input.scope, runId });
8390
8504
  throw error;
8391
8505
  }
8506
+ if (state.terminal === "running") state = markInterrupted(state);
8392
8507
  log.fail("run-flow", error, { scope: input.scope, runId });
8393
- const runErrorText = errorMessage(error);
8394
- const healKind = classifySessionError(runErrorText);
8395
8508
  if (healKind !== void 0) {
8396
8509
  const brokenSessionId = input.sessions.getRaw(input.scope, workspaceCwd)?.sessionId;
8397
8510
  if (brokenSessionId !== void 0) {
@@ -11988,6 +12101,7 @@ var ResilientCardStreamController = class {
11988
12101
  closed = false;
11989
12102
  timer;
11990
12103
  inFlight;
12104
+ reanchorInFlight;
11991
12105
  async update(card) {
11992
12106
  if (this.closed || this.failed) return;
11993
12107
  this.latest = card;
@@ -12001,10 +12115,11 @@ var ResilientCardStreamController = class {
12001
12115
  this.timer = void 0;
12002
12116
  }
12003
12117
  if (this.inFlight !== void 0) await this.inFlight;
12118
+ if (this.reanchorInFlight !== void 0) await this.reanchorInFlight;
12004
12119
  if (!this.failed && this.dirty) await this.flush();
12005
12120
  }
12006
12121
  schedule() {
12007
- if (this.timer !== void 0 || this.inFlight !== void 0 || this.failed) return;
12122
+ if (this.timer !== void 0 || this.inFlight !== void 0 || this.reanchorInFlight !== void 0 || this.failed) return;
12008
12123
  this.timer = setTimeout(() => {
12009
12124
  this.timer = void 0;
12010
12125
  this.startFlush();
@@ -12072,6 +12187,17 @@ var ResilientCardStreamController = class {
12072
12187
  */
12073
12188
  async reanchor() {
12074
12189
  if (this.closed || this.failed) return this.messageId;
12190
+ if (this.reanchorInFlight !== void 0) return this.reanchorInFlight;
12191
+ const task = this.performReanchor();
12192
+ this.reanchorInFlight = task;
12193
+ try {
12194
+ return await task;
12195
+ } finally {
12196
+ if (this.reanchorInFlight === task) this.reanchorInFlight = void 0;
12197
+ if (!this.closed && this.dirty && !this.failed) this.schedule();
12198
+ }
12199
+ }
12200
+ async performReanchor() {
12075
12201
  if (this.inFlight !== void 0) await this.inFlight;
12076
12202
  if (this.timer !== void 0) {
12077
12203
  clearTimeout(this.timer);
@@ -12079,9 +12205,11 @@ var ResilientCardStreamController = class {
12079
12205
  }
12080
12206
  const card = this.latest;
12081
12207
  if (card === void 0) return this.messageId;
12208
+ this.dirty = false;
12082
12209
  try {
12083
12210
  await this.channel.recallMessage(this.messageId);
12084
12211
  } catch (error) {
12212
+ this.dirty = true;
12085
12213
  log.warn("lark-card-stream", "reanchor-recall-failed", {
12086
12214
  messageId: this.messageId,
12087
12215
  error: error instanceof Error ? error.message : String(error)
@@ -12091,8 +12219,6 @@ var ResilientCardStreamController = class {
12091
12219
  const sent = await this.channel.send(this.chatId, { card }, {});
12092
12220
  if (!sent.messageId) throw new Error("Feishu card re-anchor returned no message_id");
12093
12221
  this.messageId = sent.messageId;
12094
- this.latest = card;
12095
- this.dirty = false;
12096
12222
  this.failed = false;
12097
12223
  return this.messageId;
12098
12224
  }
@@ -18093,13 +18219,13 @@ async function startBridgeEngine(options) {
18093
18219
  let modelRoute2;
18094
18220
  if (resolvedModel) {
18095
18221
  try {
18096
- modelRoute2 = await dshConfig.resolveModelRoute(resolvedModel);
18222
+ modelRoute2 = await dshConfig.resolveRuntimeModelRoute(resolvedModel);
18097
18223
  } catch (error) {
18098
18224
  await streaming.sendMarkdown(
18099
18225
  first.chatId,
18100
18226
  bilingualMarkdown(
18101
- `\u26A0\uFE0F \u8BFB\u53D6 dsh \u914D\u7F6E\u5931\u8D25\uFF0C\u65E0\u6CD5\u89E3\u6790\u6A21\u578B \`${resolvedModel}\` \u7684 provider \u8DEF\u7531\uFF1A${error instanceof Error ? error.message : String(error)}`,
18102
- `\u26A0\uFE0F Failed to read dsh configuration, so the provider route for model \`${resolvedModel}\` could not be resolved: ${error instanceof Error ? error.message : String(error)}`
18227
+ `\u26A0\uFE0F \u8BFB\u53D6\u6216\u51C6\u5907 dsh \u8FD0\u884C\u65F6\u914D\u7F6E\u5931\u8D25\uFF0C\u65E0\u6CD5\u89E3\u6790\u6A21\u578B \`${resolvedModel}\` \u7684 provider \u8DEF\u7531\uFF1A${error instanceof Error ? error.message : String(error)}`,
18228
+ `\u26A0\uFE0F Failed to read or prepare dsh runtime configuration, so the provider route for model \`${resolvedModel}\` could not be resolved: ${error instanceof Error ? error.message : String(error)}`
18103
18229
  ),
18104
18230
  { replyTo: first.messageId }
18105
18231
  );
@@ -18513,6 +18639,7 @@ import { spawn as spawn8 } from "child_process";
18513
18639
  import { mkdir as mkdir23, readFile as readFile32, writeFile as writeFile9 } from "fs/promises";
18514
18640
  import { homedir as homedir17 } from "os";
18515
18641
  import { join as join28 } from "path";
18642
+ import { parse } from "yaml";
18516
18643
  async function runSetup(options = {}) {
18517
18644
  const profile = options.profile ?? "dsh-lark";
18518
18645
  const own = ownPackageInfo();
@@ -18586,6 +18713,7 @@ function runDshPlugin(bin, profile, packageSpec, env = process.env) {
18586
18713
  });
18587
18714
  }
18588
18715
  async function approveBuilds(profileDir) {
18716
+ await preserveInstalledPnpmVersion(profileDir);
18589
18717
  const workspaceFile = join28(profileDir, "pnpm-workspace.yaml");
18590
18718
  let existing = "";
18591
18719
  try {
@@ -18610,6 +18738,32 @@ allowBuilds:
18610
18738
  }
18611
18739
  await writeFile9(workspaceFile, existing, "utf8");
18612
18740
  }
18741
+ async function preserveInstalledPnpmVersion(profileDir) {
18742
+ const packageFile = join28(profileDir, "package.json");
18743
+ const modulesFile = join28(profileDir, "node_modules", ".modules.yaml");
18744
+ let packageText;
18745
+ let modulesText;
18746
+ try {
18747
+ [packageText, modulesText] = await Promise.all([
18748
+ readFile32(packageFile, "utf8"),
18749
+ readFile32(modulesFile, "utf8")
18750
+ ]);
18751
+ } catch {
18752
+ return;
18753
+ }
18754
+ try {
18755
+ const profilePackage = JSON.parse(packageText);
18756
+ const modules = parse(modulesText);
18757
+ const packageManager = modules?.packageManager;
18758
+ if (typeof packageManager !== "string" || !/^pnpm@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(packageManager) || profilePackage.packageManager === packageManager) {
18759
+ return;
18760
+ }
18761
+ profilePackage.packageManager = packageManager;
18762
+ await writeFile9(packageFile, `${JSON.stringify(profilePackage, null, 2)}
18763
+ `, "utf8");
18764
+ } catch {
18765
+ }
18766
+ }
18613
18767
 
18614
18768
  // src/cli/commands/upgrade.ts
18615
18769
  init_own_package();
@@ -18768,7 +18922,7 @@ function existingAcpRoute(profileRoot) {
18768
18922
  }
18769
18923
  }
18770
18924
  async function repairRuntimeProfiles(options) {
18771
- const own = ownPackageInfo();
18925
+ const own = options.ownPackage ?? ownPackageInfo();
18772
18926
  const env = options.env ?? process.env;
18773
18927
  const relink = options.relinkFn ?? defaultRelink;
18774
18928
  const managedEnv = { ...env, DSH_HOME: options.dshHome };
@@ -18786,18 +18940,19 @@ async function repairRuntimeProfiles(options) {
18786
18940
  {
18787
18941
  profile: DEFAULT_SDK_PROFILE,
18788
18942
  root: sdkRoot,
18789
- isReady: isSdkManagedProfileCurrent,
18790
- ensure: () => ensureSdk({ home: options.dshHome, env: managedEnv })
18943
+ isReady: (root) => isSdkManagedProfileCurrent(root, void 0, own),
18944
+ ensure: () => ensureSdk({ home: options.dshHome, env: managedEnv, ownPackage: own })
18791
18945
  },
18792
18946
  {
18793
18947
  profile: DEFAULT_ACP_PROFILE,
18794
18948
  root: acpRoot,
18795
- isReady: (root) => isAcpManagedProfileCurrent(root, acpRoute.provider, acpRoute.model),
18949
+ isReady: (root) => isAcpManagedProfileCurrent(root, acpRoute.provider, acpRoute.model, own),
18796
18950
  ensure: () => ensureAcp({
18797
18951
  home: options.dshHome,
18798
18952
  env: managedEnv,
18799
18953
  provider: acpRoute.provider,
18800
- model: acpRoute.model
18954
+ model: acpRoute.model,
18955
+ ownPackage: own
18801
18956
  })
18802
18957
  }
18803
18958
  ];
@@ -19014,10 +19169,21 @@ ${changeLines.join("\n")}
19014
19169
  await (options.pluginSpawnFn ?? runDshPlugin)(bin, profile, target.spec);
19015
19170
  write(out, `\u2705 \u5305\u672C\u4F53\u5DF2\u66F4\u65B0\u5230 ${target.version}\u3002
19016
19171
  `);
19172
+ const installedTarget = await (options.readInstalledPackageFn ?? readInstalledPackage)(
19173
+ dshHome,
19174
+ profile,
19175
+ own.name
19176
+ );
19177
+ if (!installedTarget || installedTarget.name !== own.name || installedTarget.version !== target.version) {
19178
+ throw new Error(
19179
+ `dsh plugin add completed but ${own.name}@${target.version} is not installed in profile ${profile}`
19180
+ );
19181
+ }
19017
19182
  const repair = options.repairRuntimeFn ?? repairRuntimeProfiles;
19018
19183
  const runtimeStates = await repair({
19019
19184
  dshHome,
19020
19185
  env: process.env,
19186
+ ownPackage: installedTarget,
19021
19187
  provider: env.provider,
19022
19188
  model: env.model
19023
19189
  });
@@ -19133,7 +19299,7 @@ import { join as join32 } from "path";
19133
19299
  import {
19134
19300
  createLarkChannel as createLarkChannel3
19135
19301
  } from "@larksuite/channel";
19136
- import { parse } from "yaml";
19302
+ import { parse as parse2 } from "yaml";
19137
19303
  init_dsh_runtime();
19138
19304
 
19139
19305
  // src/guardian/control.ts
@@ -20382,7 +20548,7 @@ function safeScopeForAction(event) {
20382
20548
  function readDshDefaultSelection(home, env) {
20383
20549
  try {
20384
20550
  const file = join32(resolveDshHome(home, env), "settings.yaml");
20385
- const doc = parse(readFileSync6(file, "utf8"));
20551
+ const doc = parse2(readFileSync6(file, "utf8"));
20386
20552
  const value = doc?.["agent-default-model"];
20387
20553
  if (typeof value === "object" && value !== null && typeof value.provider === "string" && typeof value.model === "string") {
20388
20554
  const selection = value;