dsh-lark-bot 0.19.6 → 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 中的
package/README_EN.md CHANGED
@@ -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
package/dist/cli.js CHANGED
@@ -1093,7 +1093,7 @@ import { homedir as homedir3 } from "os";
1093
1093
  import { mkdir as mkdir2, readFile, rm, writeFile as writeFile2 } from "fs/promises";
1094
1094
  import { homedir } from "os";
1095
1095
  import { dirname as dirname3, join as join3 } from "path";
1096
- import { Document, parseDocument } from "yaml";
1096
+ import { Document, isMap, isSeq, parseDocument } from "yaml";
1097
1097
 
1098
1098
  // src/platform/atomic-write.ts
1099
1099
  import { randomBytes } from "crypto";
@@ -1563,6 +1563,22 @@ function patchNode(document, path, current, next) {
1563
1563
  }
1564
1564
  if (!deepEqualJson(current, next)) document.setIn([...path], next);
1565
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
+ }
1566
1582
  function parseYamlMap(text, filename) {
1567
1583
  if (text === void 0 || text.trim().length === 0) return {};
1568
1584
  const document = parseDocument(text, { prettyErrors: true });
@@ -1752,6 +1768,12 @@ var DshProviderManager = class {
1752
1768
  if (!provider) return void 0;
1753
1769
  return { provider: provider.id, model: selection };
1754
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
+ }
1755
1777
  async setDefaultModel(model) {
1756
1778
  if (!model.trim()) throw new Error("\u9ED8\u8BA4\u6A21\u578B\u4E0D\u80FD\u4E3A\u7A7A");
1757
1779
  const route = await this.resolveModelRoute(model);
@@ -1799,6 +1821,57 @@ var DshProviderManager = class {
1799
1821
  models: [...models, modelRecord(input)]
1800
1822
  });
1801
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
+ }
1802
1875
  async removeDeepseekModel(id) {
1803
1876
  const settings = await this.readSettings();
1804
1877
  const current = isMapLike(settings[DEEPSEEK_NAMESPACE]) ? settings[DEEPSEEK_NAMESPACE] : {};
@@ -8197,7 +8270,7 @@ async function runAttempt(input, cwd, workspaceCwd, sessionId, resuming, replyOp
8197
8270
  const consume = async () => {
8198
8271
  for await (const event of run.events) {
8199
8272
  if (timedOut) return;
8200
- if (resuming && !sawActivity && event.type === "error" && event.terminationReason === "failed") {
8273
+ if (resuming && !sawActivity && event.type === "error" && event.terminationReason === "failed" && classifySessionError(event.message) !== void 0) {
8201
8274
  await showResumeRecovery(event.message);
8202
8275
  return;
8203
8276
  }
@@ -8292,10 +8365,19 @@ async function runAttempt(input, cwd, workspaceCwd, sessionId, resuming, replyOp
8292
8365
  try {
8293
8366
  await consume();
8294
8367
  } catch (error) {
8295
- if (resuming && !sawActivity) {
8368
+ if (resuming && !sawActivity && classifySessionError(errorMessage(error)) !== void 0) {
8296
8369
  await showResumeRecovery(error);
8297
8370
  return;
8298
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
+ }
8299
8381
  throw error;
8300
8382
  }
8301
8383
  };
@@ -8400,7 +8482,7 @@ async function runAttempt(input, cwd, workspaceCwd, sessionId, resuming, replyOp
8400
8482
  if (resumeFailure !== void 0) {
8401
8483
  throw new Error(resumeFailure);
8402
8484
  }
8403
- if (resuming && state.terminal === "error" && !sawActivity) {
8485
+ if (resuming && state.terminal === "error" && !sawActivity && classifySessionError(state.errorMsg ?? "") !== void 0) {
8404
8486
  throw new Error(state.errorMsg ?? "native session resume failed");
8405
8487
  }
8406
8488
  input.sessions.recordExchange(input.scope, workspaceCwd, input.messages, assistantOutput, {
@@ -8415,14 +8497,14 @@ async function runAttempt(input, cwd, workspaceCwd, sessionId, resuming, replyOp
8415
8497
  } : {}
8416
8498
  });
8417
8499
  } catch (error) {
8418
- state = markInterrupted(state);
8419
- if (resuming && !sawActivity) {
8500
+ const runErrorText = errorMessage(error);
8501
+ const healKind = classifySessionError(runErrorText);
8502
+ if (resuming && !sawActivity && healKind !== void 0) {
8420
8503
  log.warn("run-flow", "resume-attempt-failed", { scope: input.scope, runId });
8421
8504
  throw error;
8422
8505
  }
8506
+ if (state.terminal === "running") state = markInterrupted(state);
8423
8507
  log.fail("run-flow", error, { scope: input.scope, runId });
8424
- const runErrorText = errorMessage(error);
8425
- const healKind = classifySessionError(runErrorText);
8426
8508
  if (healKind !== void 0) {
8427
8509
  const brokenSessionId = input.sessions.getRaw(input.scope, workspaceCwd)?.sessionId;
8428
8510
  if (brokenSessionId !== void 0) {
@@ -12019,6 +12101,7 @@ var ResilientCardStreamController = class {
12019
12101
  closed = false;
12020
12102
  timer;
12021
12103
  inFlight;
12104
+ reanchorInFlight;
12022
12105
  async update(card) {
12023
12106
  if (this.closed || this.failed) return;
12024
12107
  this.latest = card;
@@ -12032,10 +12115,11 @@ var ResilientCardStreamController = class {
12032
12115
  this.timer = void 0;
12033
12116
  }
12034
12117
  if (this.inFlight !== void 0) await this.inFlight;
12118
+ if (this.reanchorInFlight !== void 0) await this.reanchorInFlight;
12035
12119
  if (!this.failed && this.dirty) await this.flush();
12036
12120
  }
12037
12121
  schedule() {
12038
- 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;
12039
12123
  this.timer = setTimeout(() => {
12040
12124
  this.timer = void 0;
12041
12125
  this.startFlush();
@@ -12103,6 +12187,17 @@ var ResilientCardStreamController = class {
12103
12187
  */
12104
12188
  async reanchor() {
12105
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() {
12106
12201
  if (this.inFlight !== void 0) await this.inFlight;
12107
12202
  if (this.timer !== void 0) {
12108
12203
  clearTimeout(this.timer);
@@ -12110,9 +12205,11 @@ var ResilientCardStreamController = class {
12110
12205
  }
12111
12206
  const card = this.latest;
12112
12207
  if (card === void 0) return this.messageId;
12208
+ this.dirty = false;
12113
12209
  try {
12114
12210
  await this.channel.recallMessage(this.messageId);
12115
12211
  } catch (error) {
12212
+ this.dirty = true;
12116
12213
  log.warn("lark-card-stream", "reanchor-recall-failed", {
12117
12214
  messageId: this.messageId,
12118
12215
  error: error instanceof Error ? error.message : String(error)
@@ -12122,8 +12219,6 @@ var ResilientCardStreamController = class {
12122
12219
  const sent = await this.channel.send(this.chatId, { card }, {});
12123
12220
  if (!sent.messageId) throw new Error("Feishu card re-anchor returned no message_id");
12124
12221
  this.messageId = sent.messageId;
12125
- this.latest = card;
12126
- this.dirty = false;
12127
12222
  this.failed = false;
12128
12223
  return this.messageId;
12129
12224
  }
@@ -18124,13 +18219,13 @@ async function startBridgeEngine(options) {
18124
18219
  let modelRoute2;
18125
18220
  if (resolvedModel) {
18126
18221
  try {
18127
- modelRoute2 = await dshConfig.resolveModelRoute(resolvedModel);
18222
+ modelRoute2 = await dshConfig.resolveRuntimeModelRoute(resolvedModel);
18128
18223
  } catch (error) {
18129
18224
  await streaming.sendMarkdown(
18130
18225
  first.chatId,
18131
18226
  bilingualMarkdown(
18132
- `\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)}`,
18133
- `\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)}`
18134
18229
  ),
18135
18230
  { replyTo: first.messageId }
18136
18231
  );