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 +3 -1
- package/README_EN.md +4 -1
- package/dist/cli.js +109 -14
- package/dist/cli.js.map +1 -1
- package/dist/plugin.d.ts +10 -0
- package/dist/plugin.js +109 -14
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
8419
|
-
|
|
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.
|
|
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
|
);
|