@dsh-plus/lifeboat 0.2.4 → 0.2.6
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/lib/client.js +83 -39
- package/lib/index.js +101 -48
- package/lib/style.css +72 -72
- package/package.json +11 -11
- package/src/config.ts +19 -39
- package/src/index.ts +18 -34
- package/src/state-file.ts +94 -0
- package/src/ns.ts +0 -5
package/lib/client.js
CHANGED
|
@@ -17894,20 +17894,20 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
17894
17894
|
json_default = [lang$31];
|
|
17895
17895
|
}));
|
|
17896
17896
|
//#endregion
|
|
17897
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-ui-primitives@0.1.
|
|
17897
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-ui-primitives@0.1.5-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-client-ui-primitives/lib/DiffBlock.module.css
|
|
17898
17898
|
var DiffBlock_module_default;
|
|
17899
17899
|
var init_DiffBlock_module = __esmMin((() => {
|
|
17900
17900
|
DiffBlock_module_default = {
|
|
17901
|
-
"add": "
|
|
17902
|
-
"block": "
|
|
17903
|
-
"body": "
|
|
17904
|
-
"copyButton": "
|
|
17905
|
-
"del": "
|
|
17906
|
-
"expand": "
|
|
17907
|
-
"footer": "
|
|
17908
|
-
"gap": "
|
|
17909
|
-
"line": "
|
|
17910
|
-
"path": "
|
|
17901
|
+
"add": "nVxsMW_add",
|
|
17902
|
+
"block": "nVxsMW_block",
|
|
17903
|
+
"body": "nVxsMW_body",
|
|
17904
|
+
"copyButton": "nVxsMW_copyButton",
|
|
17905
|
+
"del": "nVxsMW_del",
|
|
17906
|
+
"expand": "nVxsMW_expand",
|
|
17907
|
+
"footer": "nVxsMW_footer",
|
|
17908
|
+
"gap": "nVxsMW_gap",
|
|
17909
|
+
"line": "nVxsMW_line",
|
|
17910
|
+
"path": "nVxsMW_path"
|
|
17911
17911
|
};
|
|
17912
17912
|
}));
|
|
17913
17913
|
//#endregion
|
|
@@ -58054,32 +58054,32 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
58054
58054
|
init_types();
|
|
58055
58055
|
}));
|
|
58056
58056
|
//#endregion
|
|
58057
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-ui-primitives@0.1.
|
|
58057
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-ui-primitives@0.1.5-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-client-ui-primitives/lib/markdown/CodeBlock.module.css
|
|
58058
58058
|
var CodeBlock_module_default;
|
|
58059
58059
|
var init_CodeBlock_module = __esmMin((() => {
|
|
58060
58060
|
CodeBlock_module_default = {
|
|
58061
|
-
"action": "
|
|
58062
|
-
"banner": "
|
|
58063
|
-
"bannerWrap": "
|
|
58064
|
-
"block": "
|
|
58065
|
-
"copyButton": "
|
|
58066
|
-
"infostring": "
|
|
58067
|
-
"plain": "
|
|
58068
|
-
"shiki": "
|
|
58061
|
+
"action": "Hbqh2W_action",
|
|
58062
|
+
"banner": "Hbqh2W_banner",
|
|
58063
|
+
"bannerWrap": "Hbqh2W_bannerWrap",
|
|
58064
|
+
"block": "Hbqh2W_block",
|
|
58065
|
+
"copyButton": "Hbqh2W_copyButton",
|
|
58066
|
+
"infostring": "Hbqh2W_infostring",
|
|
58067
|
+
"plain": "Hbqh2W_plain",
|
|
58068
|
+
"shiki": "Hbqh2W_shiki"
|
|
58069
58069
|
};
|
|
58070
58070
|
}));
|
|
58071
58071
|
//#endregion
|
|
58072
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-ui-primitives@0.1.
|
|
58072
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-ui-primitives@0.1.5-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-client-ui-primitives/lib/markdown/MarkdownText.module.css
|
|
58073
58073
|
var MarkdownText_module_default;
|
|
58074
58074
|
var init_MarkdownText_module = __esmMin((() => {
|
|
58075
58075
|
MarkdownText_module_default = {
|
|
58076
|
-
"fileMention": "
|
|
58077
|
-
"image": "
|
|
58078
|
-
"imageAlt": "
|
|
58079
|
-
"linkIcon": "
|
|
58080
|
-
"markdown": "
|
|
58081
|
-
"tableFill": "
|
|
58082
|
-
"tableScroll": "
|
|
58076
|
+
"fileMention": "BL43XG_fileMention",
|
|
58077
|
+
"image": "BL43XG_image",
|
|
58078
|
+
"imageAlt": "BL43XG_imageAlt",
|
|
58079
|
+
"linkIcon": "BL43XG_linkIcon",
|
|
58080
|
+
"markdown": "BL43XG_markdown",
|
|
58081
|
+
"tableFill": "BL43XG_tableFill",
|
|
58082
|
+
"tableScroll": "BL43XG_tableScroll"
|
|
58083
58083
|
};
|
|
58084
58084
|
}));
|
|
58085
58085
|
//#endregion
|
|
@@ -58406,7 +58406,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
58406
58406
|
less_default = [lang];
|
|
58407
58407
|
}));
|
|
58408
58408
|
//#endregion
|
|
58409
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-ui-primitives@0.1.
|
|
58409
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-ui-primitives@0.1.5-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-client-ui-primitives/lib/index.js
|
|
58410
58410
|
/**
|
|
58411
58411
|
* Write text to the host clipboard, preferring the async Clipboard API and
|
|
58412
58412
|
* falling back to `execCommand('copy')` on hosts (jsdom, insecure contexts)
|
|
@@ -59181,6 +59181,30 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
59181
59181
|
return;
|
|
59182
59182
|
}
|
|
59183
59183
|
}
|
|
59184
|
+
/** Protocols a vocabulary-rewritten image destination may carry. */
|
|
59185
|
+
function vocabularyImageUrl(url) {
|
|
59186
|
+
try {
|
|
59187
|
+
const protocol = new URL(url).protocol;
|
|
59188
|
+
return protocol === "http:" || protocol === "https:" || protocol === "blob:" || protocol === "data:" ? url : void 0;
|
|
59189
|
+
} catch {
|
|
59190
|
+
return;
|
|
59191
|
+
}
|
|
59192
|
+
}
|
|
59193
|
+
/**
|
|
59194
|
+
* The displayable source for one image destination: absolute HTTP(S) as
|
|
59195
|
+
* authored, otherwise the context's local-path vocabulary when it vouches for
|
|
59196
|
+
* the destination. Either miss leaves the authored fallback (alt text) to the
|
|
59197
|
+
* caller.
|
|
59198
|
+
* @param url - The authored markdown destination.
|
|
59199
|
+
* @param pathImages - Rewriting vocabulary, when the render pass has one.
|
|
59200
|
+
* @returns The displayable image URL, or undefined.
|
|
59201
|
+
*/
|
|
59202
|
+
function imageSource(url, pathImages) {
|
|
59203
|
+
const remote = remoteImageUrl(sanitizeUrl(normalizeUri(url)));
|
|
59204
|
+
if (remote !== void 0) return remote;
|
|
59205
|
+
const rewritten = pathImages?.resolve(url);
|
|
59206
|
+
return rewritten === void 0 ? void 0 : vocabularyImageUrl(rewritten);
|
|
59207
|
+
}
|
|
59184
59208
|
/**
|
|
59185
59209
|
* Create an empty {@link ReferenceTargets}.
|
|
59186
59210
|
* @returns Fresh empty maps.
|
|
@@ -59295,7 +59319,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
59295
59319
|
inLink: true
|
|
59296
59320
|
}), key, !anchorWrapsOnlyImages(node.children));
|
|
59297
59321
|
case "linkReference": return renderLinkReference(node, key, context);
|
|
59298
|
-
case "image": return renderImage(node.url, node.alt ?? "", key);
|
|
59322
|
+
case "image": return renderImage(node.url, node.alt ?? "", key, context);
|
|
59299
59323
|
case "imageReference": return renderImageReference(node, key, context);
|
|
59300
59324
|
case "footnoteReference": return renderFootnoteReference(node, key, context);
|
|
59301
59325
|
case "definition":
|
|
@@ -59429,20 +59453,36 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
59429
59453
|
return;
|
|
59430
59454
|
}
|
|
59431
59455
|
}
|
|
59432
|
-
function renderImage(url, alt, key) {
|
|
59433
|
-
const imageSrc =
|
|
59456
|
+
function renderImage(url, alt, key, context) {
|
|
59457
|
+
const imageSrc = imageSource(url, context.pathImages);
|
|
59434
59458
|
if (imageSrc === void 0) return (0, react_jsx_runtime.jsx)("span", {
|
|
59435
59459
|
className: MarkdownText_module_default.imageAlt,
|
|
59436
59460
|
children: alt
|
|
59437
59461
|
}, key);
|
|
59462
|
+
return (0, react_jsx_runtime.jsx)(MarkdownImage, {
|
|
59463
|
+
src: imageSrc,
|
|
59464
|
+
alt,
|
|
59465
|
+
destination: url
|
|
59466
|
+
}, `${key}:${imageSrc}`);
|
|
59467
|
+
}
|
|
59468
|
+
/** Failed loads retain the authored alt or destination; a new source remounts the image. */
|
|
59469
|
+
function MarkdownImage({ src, alt, destination }) {
|
|
59470
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
59471
|
+
if (failed) return (0, react_jsx_runtime.jsx)("span", {
|
|
59472
|
+
className: MarkdownText_module_default.imageAlt,
|
|
59473
|
+
children: alt || destination
|
|
59474
|
+
});
|
|
59438
59475
|
return (0, react_jsx_runtime.jsx)("img", {
|
|
59439
59476
|
className: MarkdownText_module_default.image,
|
|
59440
|
-
src
|
|
59477
|
+
src,
|
|
59441
59478
|
alt,
|
|
59479
|
+
onError: () => {
|
|
59480
|
+
setFailed(true);
|
|
59481
|
+
},
|
|
59442
59482
|
loading: "lazy",
|
|
59443
59483
|
decoding: "async",
|
|
59444
59484
|
referrerPolicy: "no-referrer"
|
|
59445
|
-
}
|
|
59485
|
+
});
|
|
59446
59486
|
}
|
|
59447
59487
|
/** The bracketed source text a reference reverts to when its definition is missing. */
|
|
59448
59488
|
function referenceSuffix(node) {
|
|
@@ -59466,7 +59506,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
59466
59506
|
function renderImageReference(node, key, context) {
|
|
59467
59507
|
const definition = context.targets.definitions.get(node.identifier.toUpperCase());
|
|
59468
59508
|
if (definition === void 0) return `![${node.alt ?? ""}${referenceSuffix(node)}`;
|
|
59469
|
-
return renderImage(definition.url, node.alt ?? "", key);
|
|
59509
|
+
return renderImage(definition.url, node.alt ?? "", key, context);
|
|
59470
59510
|
}
|
|
59471
59511
|
function renderFootnoteReference(node, key, context) {
|
|
59472
59512
|
const id = node.identifier.toUpperCase();
|
|
@@ -59527,7 +59567,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
59527
59567
|
* full parse self-heals it.
|
|
59528
59568
|
*/
|
|
59529
59569
|
/** One settled full render: parse with math, resolve references, append the footnote section. */
|
|
59530
|
-
function renderSettled(text, labels, fileMentions) {
|
|
59570
|
+
function renderSettled(text, labels, fileMentions, pathImages) {
|
|
59531
59571
|
const root = parseGfmWithMath(text);
|
|
59532
59572
|
const targets = createReferenceTargets();
|
|
59533
59573
|
collectReferenceTargets(root.children, targets);
|
|
@@ -59535,6 +59575,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
59535
59575
|
streaming: false,
|
|
59536
59576
|
labels,
|
|
59537
59577
|
fileMentions,
|
|
59578
|
+
pathImages,
|
|
59538
59579
|
targets,
|
|
59539
59580
|
footnoteOrder: [],
|
|
59540
59581
|
footnoteCounts: /* @__PURE__ */ new Map()
|
|
@@ -60406,6 +60447,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
60406
60447
|
streaming: true,
|
|
60407
60448
|
labels: this.labels,
|
|
60408
60449
|
fileMentions: void 0,
|
|
60450
|
+
pathImages: void 0,
|
|
60409
60451
|
targets: frameTargets,
|
|
60410
60452
|
footnoteOrder: this.frozenFootnoteOrder,
|
|
60411
60453
|
footnoteCounts: this.frozenFootnoteCounts
|
|
@@ -60422,6 +60464,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
60422
60464
|
streaming: true,
|
|
60423
60465
|
labels: this.labels,
|
|
60424
60466
|
fileMentions: void 0,
|
|
60467
|
+
pathImages: void 0,
|
|
60425
60468
|
targets: frameTargets,
|
|
60426
60469
|
footnoteOrder: [...this.frozenFootnoteOrder],
|
|
60427
60470
|
footnoteCounts: new Map(this.frozenFootnoteCounts)
|
|
@@ -60438,13 +60481,13 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
60438
60481
|
return this.lastRendered;
|
|
60439
60482
|
}
|
|
60440
60483
|
};
|
|
60441
|
-
(0, react.memo)(function MarkdownText({ text, streaming = false, labels, fileMentions }) {
|
|
60484
|
+
(0, react.memo)(function MarkdownText({ text, streaming = false, labels, fileMentions, pathImages }) {
|
|
60442
60485
|
const streamRef = (0, react.useRef)(null);
|
|
60443
60486
|
const streamLabelsRef = (0, react.useRef)(labels);
|
|
60444
60487
|
const children = (0, react.useMemo)(() => {
|
|
60445
60488
|
if (!streaming) {
|
|
60446
60489
|
streamRef.current = null;
|
|
60447
|
-
return renderSettled(text, labels, fileMentions);
|
|
60490
|
+
return renderSettled(text, labels, fileMentions, pathImages);
|
|
60448
60491
|
}
|
|
60449
60492
|
if (streamRef.current === null || streamLabelsRef.current !== labels) {
|
|
60450
60493
|
streamRef.current = new StreamingRenderer(labels);
|
|
@@ -60455,7 +60498,8 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
60455
60498
|
text,
|
|
60456
60499
|
streaming,
|
|
60457
60500
|
labels,
|
|
60458
|
-
fileMentions
|
|
60501
|
+
fileMentions,
|
|
60502
|
+
pathImages
|
|
60459
60503
|
]);
|
|
60460
60504
|
return (0, react_jsx_runtime.jsx)("div", {
|
|
60461
60505
|
className: MarkdownText_module_default.markdown,
|
package/lib/index.js
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import { copyFile, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { copyFile, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { SettingsConflictError } from "@deepseek-ai/dsh-settings";
|
|
5
5
|
import { parseDocument } from "yaml";
|
|
6
|
-
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
7
|
//#region src/config.ts
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* 配置单一事实源。
|
|
10
10
|
* - 行级 Config(cordis 组合层):救生艇自身行为开关,dev/prod patch 层可覆盖。
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
* schema 刻意宽松:lifeboat 是最后防线,自身命名空间的数据问题绝不能让它起不来。
|
|
11
|
+
* - journal 与 LLM 应急翻译状态属运行期数据,按存储规范持久化到
|
|
12
|
+
* $DSH_HOME/dsh-plus/lifeboat/state.json(见 state-file.ts),不进 settings。
|
|
14
13
|
* @module lifeboat/config
|
|
15
14
|
*/
|
|
16
|
-
/** settings 命名空间(字面量直供 settings 服务注册/读写,0.1.2-alpha.2 起编译期校验语法)。 */
|
|
17
|
-
const SETTINGS_NS = "dsh-plus-lifeboat";
|
|
18
15
|
/** 行级配置:全部有默认值,正常部署零配置。 */
|
|
19
16
|
const Config = z.object({
|
|
20
17
|
enabled: z.boolean().description("故障隔离总开关").default(true),
|
|
@@ -22,26 +19,6 @@ const Config = z.object({
|
|
|
22
19
|
llmFallback: z.boolean().description("llm-pi 缺席时自动翻译其配置到官方 llm-pi-ai 并切换默认模型").default(true),
|
|
23
20
|
alertCooldownMs: z.natural().description("同一插件重复告警的最小间隔毫秒数").default(3e5)
|
|
24
21
|
});
|
|
25
|
-
/** journal 单条记录。 */
|
|
26
|
-
const JournalEntry = z.object({
|
|
27
|
-
at: z.string(),
|
|
28
|
-
kind: z.string(),
|
|
29
|
-
detail: z.string()
|
|
30
|
-
});
|
|
31
|
-
/** LLM 应急翻译的持久状态(还原依据)。 */
|
|
32
|
-
const FallbackState = z.object({
|
|
33
|
-
active: z.boolean(),
|
|
34
|
-
originalProvider: z.string(),
|
|
35
|
-
originalModel: z.string(),
|
|
36
|
-
fallbackProvider: z.string(),
|
|
37
|
-
providers: z.array(z.string()),
|
|
38
|
-
at: z.string()
|
|
39
|
-
});
|
|
40
|
-
/** 命名空间 schema:宽松承载 journal 与翻译状态,上限截断防膨胀。 */
|
|
41
|
-
const JournalSchema = z.object({
|
|
42
|
-
journal: z.array(JournalEntry).description("救生艇操作日志(最新在尾,封顶 50 条)").default([]),
|
|
43
|
-
llmFallback: z.union([FallbackState, z.const(null)]).description("LLM 应急翻译状态;null 表示未处于降级").default(null)
|
|
44
|
-
});
|
|
45
22
|
//#endregion
|
|
46
23
|
//#region src/fallback-llm.ts
|
|
47
24
|
/**
|
|
@@ -616,35 +593,111 @@ function registerQuarantineApi(ctx, q) {
|
|
|
616
593
|
});
|
|
617
594
|
}
|
|
618
595
|
//#endregion
|
|
596
|
+
//#region src/state-file.ts
|
|
597
|
+
/**
|
|
598
|
+
* journal 与 LLM 应急翻译状态的文件持久化($DSH_HOME/dsh-plus/lifeboat/state.json)。
|
|
599
|
+
*
|
|
600
|
+
* 存储规范:运行期状态属非配置数据,不进 settings.yaml;本模块自带实现,
|
|
601
|
+
* 维持零 dsh-plus 内部依赖铁律(不 import @dsh-plus/shared)。
|
|
602
|
+
* schema 刻意宽松:lifeboat 是最后防线,自身数据问题绝不能让它起不来。
|
|
603
|
+
* @module lifeboat/state-file
|
|
604
|
+
*/
|
|
605
|
+
/** 状态文件落点:$DSH_HOME/dsh-plus/lifeboat/state.json。 */
|
|
606
|
+
const STATE_FILE = dshHomePath("dsh-plus", "lifeboat", "state.json");
|
|
607
|
+
/** 文件大小上限:超出即视为损坏,丢弃重来(journal 本身封顶 50 条)。 */
|
|
608
|
+
const MAX_BYTES = 262144;
|
|
609
|
+
/** 未知输入收窄为合法 journal 条目(宽松承载数据问题)。 */
|
|
610
|
+
function asEntry(value) {
|
|
611
|
+
if (typeof value !== "object" || value === null) return null;
|
|
612
|
+
const record = value;
|
|
613
|
+
if (typeof record["at"] !== "string" || typeof record["kind"] !== "string") return null;
|
|
614
|
+
return {
|
|
615
|
+
at: record["at"],
|
|
616
|
+
kind: record["kind"],
|
|
617
|
+
detail: String(record["detail"] ?? "")
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
/** 未知输入收窄为合法翻译状态。 */
|
|
621
|
+
function asFallback(value) {
|
|
622
|
+
if (typeof value !== "object" || value === null) return null;
|
|
623
|
+
const record = value;
|
|
624
|
+
if (typeof record["active"] !== "boolean") return null;
|
|
625
|
+
return {
|
|
626
|
+
active: record["active"],
|
|
627
|
+
originalProvider: String(record["originalProvider"] ?? ""),
|
|
628
|
+
originalModel: String(record["originalModel"] ?? ""),
|
|
629
|
+
fallbackProvider: String(record["fallbackProvider"] ?? ""),
|
|
630
|
+
providers: Array.isArray(record["providers"]) ? record["providers"].map((item) => String(item)) : [],
|
|
631
|
+
at: String(record["at"] ?? "")
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
/** 未知输入收窄为完整状态文档;任何缺省回落到空文档。 */
|
|
635
|
+
function normalizeState(raw) {
|
|
636
|
+
if (typeof raw !== "object" || raw === null) return {
|
|
637
|
+
journal: [],
|
|
638
|
+
llmFallback: null
|
|
639
|
+
};
|
|
640
|
+
const record = raw;
|
|
641
|
+
return {
|
|
642
|
+
journal: (Array.isArray(record["journal"]) ? record["journal"].map(asEntry).filter((entry) => entry !== null) : []).slice(-50),
|
|
643
|
+
llmFallback: asFallback(record["llmFallback"])
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
/** 读取状态文件;缺失/损坏/超限时返回空文档(最后防线不得因自身数据起不来)。 */
|
|
647
|
+
async function loadState() {
|
|
648
|
+
try {
|
|
649
|
+
const info = await stat(STATE_FILE);
|
|
650
|
+
if (!info.isFile() || info.size > MAX_BYTES) return {
|
|
651
|
+
journal: [],
|
|
652
|
+
llmFallback: null
|
|
653
|
+
};
|
|
654
|
+
return normalizeState(JSON.parse(await readFile(STATE_FILE, "utf-8")));
|
|
655
|
+
} catch {
|
|
656
|
+
return {
|
|
657
|
+
journal: [],
|
|
658
|
+
llmFallback: null
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/** 原子写状态文件:临时文件 + rename,写入失败仅告警不抛出。 */
|
|
663
|
+
async function saveState(doc, warn) {
|
|
664
|
+
try {
|
|
665
|
+
await mkdir(dirname(STATE_FILE), { recursive: true });
|
|
666
|
+
const tmp = `${STATE_FILE}.tmp`;
|
|
667
|
+
await writeFile(tmp, `${JSON.stringify(doc, null, 2)}\n`, "utf-8");
|
|
668
|
+
await rename(tmp, STATE_FILE);
|
|
669
|
+
} catch (error) {
|
|
670
|
+
warn(`状态文件写入失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
/** 追加一条 journal(内存文档已更新后调用;落盘失败不阻断动作)。 */
|
|
674
|
+
function appendJournal(doc, kind, detail) {
|
|
675
|
+
return {
|
|
676
|
+
...doc,
|
|
677
|
+
journal: [...doc.journal, {
|
|
678
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
679
|
+
kind,
|
|
680
|
+
detail
|
|
681
|
+
}].slice(-50)
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
//#endregion
|
|
619
685
|
//#region src/index.ts
|
|
620
686
|
const name = "dsh-plus-lifeboat";
|
|
621
687
|
const inject = ["settings", "llm"];
|
|
622
688
|
function apply(ctx, config) {
|
|
623
689
|
const logger = ctx.logger("lifeboat");
|
|
690
|
+
const warn = (message) => logger.warn(message);
|
|
624
691
|
let doc = {
|
|
625
692
|
journal: [],
|
|
626
693
|
llmFallback: null
|
|
627
694
|
};
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
doc = scope.get();
|
|
632
|
-
persist = (patch) => scope.update(patch);
|
|
633
|
-
} catch (error) {
|
|
634
|
-
logger.warn(`journal 命名空间注册失败,降级为内存态: ${error instanceof Error ? error.message : String(error)}`);
|
|
635
|
-
}
|
|
695
|
+
loadState().then((loaded) => {
|
|
696
|
+
doc = loaded;
|
|
697
|
+
});
|
|
636
698
|
const journal = (kind, detail) => {
|
|
637
|
-
doc =
|
|
638
|
-
|
|
639
|
-
journal: [...doc.journal, {
|
|
640
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
641
|
-
kind,
|
|
642
|
-
detail
|
|
643
|
-
}].slice(-50)
|
|
644
|
-
};
|
|
645
|
-
persist?.({ journal: doc.journal }).catch((error) => {
|
|
646
|
-
logger.warn(`journal 持久化失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
647
|
-
});
|
|
699
|
+
doc = appendJournal(doc, kind, detail);
|
|
700
|
+
saveState(doc, warn);
|
|
648
701
|
};
|
|
649
702
|
const alert = installAlerter(ctx, journal);
|
|
650
703
|
if (config.enabled) {
|
|
@@ -676,7 +729,7 @@ function apply(ctx, config) {
|
|
|
676
729
|
...doc,
|
|
677
730
|
llmFallback: state
|
|
678
731
|
};
|
|
679
|
-
await
|
|
732
|
+
await saveState(doc, warn);
|
|
680
733
|
}
|
|
681
734
|
});
|
|
682
735
|
}
|
package/lib/style.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.
|
|
1
|
+
.nVxsMW_block {
|
|
2
2
|
--dsl-diff-radius: 12px;
|
|
3
3
|
--dsl-diff-line-height: 22px;
|
|
4
4
|
color: var(--dsw-alias-label-primary);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
position: relative;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
.
|
|
11
|
+
.nVxsMW_copyButton {
|
|
12
12
|
z-index: 1;
|
|
13
13
|
color: var(--dsw-alias-label-secondary);
|
|
14
14
|
cursor: pointer;
|
|
@@ -22,46 +22,46 @@
|
|
|
22
22
|
right: 12px;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
.
|
|
25
|
+
.nVxsMW_body {
|
|
26
26
|
font: var(--dsw-font-markdown-code-block);
|
|
27
27
|
padding: 12px 14px;
|
|
28
28
|
overflow: auto hidden;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
.
|
|
31
|
+
.nVxsMW_line {
|
|
32
32
|
min-height: var(--dsl-diff-line-height);
|
|
33
33
|
white-space: pre;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
.
|
|
36
|
+
.nVxsMW_path {
|
|
37
37
|
color: var(--dsw-alias-label-primary);
|
|
38
38
|
padding-right: 56px;
|
|
39
39
|
font-weight: 600;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
.
|
|
42
|
+
.nVxsMW_gap {
|
|
43
43
|
color: var(--dsw-alias-label-tertiary);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
.
|
|
46
|
+
.nVxsMW_del:before {
|
|
47
47
|
content: "- ";
|
|
48
48
|
color: var(--dsw-alias-state-error-primary);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
.
|
|
51
|
+
.nVxsMW_del {
|
|
52
52
|
color: var(--dsw-alias-state-error-primary);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
.
|
|
55
|
+
.nVxsMW_add:before {
|
|
56
56
|
content: "+ ";
|
|
57
57
|
color: var(--dsw-alias-state-success-primary);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
.
|
|
60
|
+
.nVxsMW_add {
|
|
61
61
|
color: var(--dsw-alias-state-success-primary);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
.
|
|
64
|
+
.nVxsMW_expand {
|
|
65
65
|
width: 100%;
|
|
66
66
|
color: var(--dsw-alias-label-tertiary);
|
|
67
67
|
cursor: pointer;
|
|
@@ -73,16 +73,16 @@
|
|
|
73
73
|
display: block;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
.
|
|
76
|
+
.nVxsMW_expand:hover {
|
|
77
77
|
color: var(--dsw-alias-label-secondary);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
.
|
|
80
|
+
.nVxsMW_footer {
|
|
81
81
|
font: var(--dsw-font-markdown-code-block);
|
|
82
82
|
color: var(--dsw-alias-label-tertiary);
|
|
83
83
|
padding: 0 14px 12px;
|
|
84
84
|
}
|
|
85
|
-
.
|
|
85
|
+
.Hbqh2W_block {
|
|
86
86
|
--dsl-code-block-banner-background-color: var(--dsw-alias-markdown-code-block-banner);
|
|
87
87
|
--dsl-code-block-border-radius: 12px;
|
|
88
88
|
--dsl-code-block-banner-font: 11px/18px var(--dsw-font-family);
|
|
@@ -94,11 +94,11 @@
|
|
|
94
94
|
position: relative;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
.
|
|
97
|
+
.Hbqh2W_block:not(:last-child) {
|
|
98
98
|
margin-bottom: 11px;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
.
|
|
101
|
+
.Hbqh2W_bannerWrap {
|
|
102
102
|
z-index: 6;
|
|
103
103
|
background-color: var(--dsw-alias-bg-base);
|
|
104
104
|
border-top-left-radius: var(--dsl-code-block-border-radius);
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
top: 0;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
.
|
|
110
|
+
.Hbqh2W_banner {
|
|
111
111
|
background: var(--dsl-code-block-banner-background-color);
|
|
112
112
|
font: var(--dsl-code-block-banner-font);
|
|
113
113
|
border-top-left-radius: var(--dsl-code-block-border-radius);
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
display: flex;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
.
|
|
122
|
+
.Hbqh2W_infostring {
|
|
123
123
|
color: var(--dsw-alias-label-primary);
|
|
124
124
|
font-family: var(--ds-font-family-code);
|
|
125
125
|
text-overflow: ellipsis;
|
|
@@ -130,13 +130,13 @@
|
|
|
130
130
|
overflow: hidden;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
.
|
|
133
|
+
.Hbqh2W_action {
|
|
134
134
|
flex-shrink: 0;
|
|
135
135
|
align-items: center;
|
|
136
136
|
display: flex;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
.
|
|
139
|
+
.Hbqh2W_copyButton {
|
|
140
140
|
color: inherit;
|
|
141
141
|
cursor: pointer;
|
|
142
142
|
font: inherit;
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
padding: 0;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
.
|
|
149
|
+
.Hbqh2W_block :where(pre) {
|
|
150
150
|
font: var(--dsl-code-block-content-font);
|
|
151
151
|
white-space: pre-wrap;
|
|
152
152
|
word-break: break-all;
|
|
@@ -158,72 +158,72 @@
|
|
|
158
158
|
margin: 0 !important;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
.
|
|
161
|
+
.Hbqh2W_block :where(pre.Hbqh2W_shiki) {
|
|
162
162
|
background: var(--dsw-alias-markdown-code-block) !important;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
.
|
|
165
|
+
.Hbqh2W_block :where(pre) code {
|
|
166
166
|
font: inherit;
|
|
167
167
|
background: none;
|
|
168
168
|
padding: 0;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
.
|
|
171
|
+
.Hbqh2W_plain {
|
|
172
172
|
color: var(--dsw-alias-label-primary);
|
|
173
173
|
}
|
|
174
|
-
.
|
|
174
|
+
.BL43XG_markdown {
|
|
175
175
|
overflow-wrap: anywhere;
|
|
176
176
|
min-width: 0;
|
|
177
177
|
font: var(--dsw-font-markdown-base);
|
|
178
178
|
color: var(--dsw-alias-label-primary);
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
.
|
|
181
|
+
.BL43XG_markdown strong {
|
|
182
182
|
font-weight: 600;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
.
|
|
185
|
+
.BL43XG_markdown h1 {
|
|
186
186
|
font: var(--dsw-font-markdown-h1);
|
|
187
187
|
margin: 32px 0 16px;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
.
|
|
190
|
+
.BL43XG_markdown h2 {
|
|
191
191
|
font: var(--dsw-font-markdown-h2);
|
|
192
192
|
margin: 32px 0 16px;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
.
|
|
195
|
+
.BL43XG_markdown h3 {
|
|
196
196
|
font: var(--dsw-font-markdown-h3);
|
|
197
197
|
margin: 32px 0 16px;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
.
|
|
200
|
+
.BL43XG_markdown h4 {
|
|
201
201
|
font: var(--dsw-font-markdown-h4);
|
|
202
202
|
margin: 16px 0;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
.
|
|
205
|
+
.BL43XG_markdown :where(h5, h6) {
|
|
206
206
|
font: var(--dsw-font-markdown-base-strong);
|
|
207
207
|
margin: 16px 0;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
.
|
|
210
|
+
.BL43XG_markdown :where(h1, h2, h3, h4, h5, h6) strong {
|
|
211
211
|
font-weight: inherit;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
.
|
|
214
|
+
.BL43XG_markdown p {
|
|
215
215
|
margin: 16px 0;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
.
|
|
218
|
+
.BL43XG_markdown :where(h4, h5, h6) + :where(ul, ol) {
|
|
219
219
|
margin-top: 8px;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
.
|
|
222
|
+
.BL43XG_markdown :where(h4, h5, h6):has( + :where(ul, ol)) {
|
|
223
223
|
margin-bottom: 8px;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
.
|
|
226
|
+
.BL43XG_markdown a {
|
|
227
227
|
color: var(--dsw-alias-link);
|
|
228
228
|
transition: box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
229
229
|
border: 2px solid #fff0;
|
|
@@ -235,17 +235,17 @@
|
|
|
235
235
|
position: relative;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
.
|
|
238
|
+
.BL43XG_markdown a:hover, .BL43XG_markdown a:focus {
|
|
239
239
|
text-decoration: underline dotted var(--dsw-alias-link);
|
|
240
240
|
text-underline-offset: 3px;
|
|
241
241
|
outline: none;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
.
|
|
244
|
+
.BL43XG_markdown a:focus-visible {
|
|
245
245
|
box-shadow: 0 0 0 2px var(--dsw-alias-state-business-primary);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
.
|
|
248
|
+
.BL43XG_linkIcon {
|
|
249
249
|
vertical-align: -.25em;
|
|
250
250
|
flex: none;
|
|
251
251
|
width: 1.1em;
|
|
@@ -253,46 +253,46 @@
|
|
|
253
253
|
margin-right: 5px;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
.
|
|
256
|
+
.BL43XG_markdown :where(ul, ol) {
|
|
257
257
|
margin: 16px 0;
|
|
258
258
|
padding-left: 18px;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
.
|
|
261
|
+
.BL43XG_markdown li:not(:first-child) {
|
|
262
262
|
margin-top: 6px;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
.
|
|
265
|
+
.BL43XG_markdown li > :where(ul, ol) {
|
|
266
266
|
margin-top: 4px;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
.
|
|
269
|
+
.BL43XG_markdown li::marker {
|
|
270
270
|
color: var(--dsw-alias-label-secondary);
|
|
271
271
|
line-height: 24px;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
.
|
|
274
|
+
.BL43XG_markdown :where(ul, ol) ol {
|
|
275
275
|
padding-left: 0;
|
|
276
276
|
list-style-position: inside;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
.
|
|
279
|
+
.BL43XG_markdown :where(ul, ol) ol li p {
|
|
280
280
|
display: inline;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
.
|
|
283
|
+
.BL43XG_markdown li > p {
|
|
284
284
|
margin: 8px 0;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
.
|
|
287
|
+
.BL43XG_markdown li > :first-child {
|
|
288
288
|
margin-top: 0;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
.
|
|
291
|
+
.BL43XG_markdown li > :last-child:not(.md-code-block) {
|
|
292
292
|
margin-bottom: 0;
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
.
|
|
295
|
+
.BL43XG_markdown hr {
|
|
296
296
|
background: var(--dsw-alias-border-l2);
|
|
297
297
|
border: none;
|
|
298
298
|
height: .5px;
|
|
@@ -300,19 +300,19 @@
|
|
|
300
300
|
display: block;
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
.
|
|
303
|
+
.BL43XG_markdown blockquote {
|
|
304
304
|
border-left: 2px solid var(--dsw-alias-label-caption);
|
|
305
305
|
margin: 16px 0 0;
|
|
306
306
|
padding-left: 14px;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
.
|
|
309
|
+
.BL43XG_markdown pre {
|
|
310
310
|
font-family: var(--ds-font-family-code);
|
|
311
311
|
margin: 16px 0;
|
|
312
312
|
overflow: auto;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
.
|
|
315
|
+
.BL43XG_markdown :not(pre) > code {
|
|
316
316
|
box-sizing: border-box;
|
|
317
317
|
font: var(--dsw-font-markdown-code);
|
|
318
318
|
font-family: var(--ds-font-family-code);
|
|
@@ -325,54 +325,54 @@
|
|
|
325
325
|
font-size: .875em !important;
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
-
.
|
|
328
|
+
.BL43XG_markdown :where(h1, h2, h3, h4, h5, h6) code {
|
|
329
329
|
font: inherit;
|
|
330
330
|
font-family: var(--ds-font-family-code);
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
-
.
|
|
333
|
+
.BL43XG_markdown .katex-display {
|
|
334
334
|
max-width: 100%;
|
|
335
335
|
overflow: auto hidden;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
.
|
|
338
|
+
.BL43XG_markdown input[type="checkbox"] {
|
|
339
339
|
accent-color: var(--dsw-alias-label-secondary);
|
|
340
340
|
margin: 0 8px 0 0;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
.
|
|
343
|
+
.BL43XG_tableScroll {
|
|
344
344
|
overscroll-behavior-x: contain;
|
|
345
345
|
max-width: 100%;
|
|
346
346
|
overflow-x: auto;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
.
|
|
349
|
+
.BL43XG_tableScroll.md-table-wide {
|
|
350
350
|
padding-bottom: var(--dsh-scrollbar-width, 8px);
|
|
351
351
|
overflow-x: hidden;
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
.
|
|
354
|
+
.BL43XG_tableScroll.md-table-wide:hover, .BL43XG_tableScroll.md-table-wide:focus-visible {
|
|
355
355
|
padding-bottom: 0;
|
|
356
356
|
overflow-x: auto;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
.
|
|
359
|
+
.BL43XG_tableScroll:focus-visible {
|
|
360
360
|
box-shadow: 0 0 0 2px var(--dsw-alias-state-business-primary);
|
|
361
361
|
outline: none;
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
.
|
|
364
|
+
.BL43XG_tableScroll table {
|
|
365
365
|
border-collapse: collapse;
|
|
366
366
|
width: max-content;
|
|
367
367
|
max-width: max-content;
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
.
|
|
370
|
+
.BL43XG_tableFill table {
|
|
371
371
|
width: 100%;
|
|
372
372
|
max-width: none;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
.
|
|
375
|
+
.BL43XG_tableScroll th {
|
|
376
376
|
text-align: start;
|
|
377
377
|
border-bottom: .5px solid var(--dsw-alias-border-l3);
|
|
378
378
|
font: var(--dsw-font-markdown-table-head);
|
|
@@ -382,7 +382,7 @@
|
|
|
382
382
|
padding: 10px 16px;
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
-
.
|
|
385
|
+
.BL43XG_tableScroll td {
|
|
386
386
|
border-bottom: .5px solid var(--dsw-alias-border-l2);
|
|
387
387
|
font: var(--dsw-font-markdown-table);
|
|
388
388
|
min-width: 100px;
|
|
@@ -390,32 +390,32 @@
|
|
|
390
390
|
padding: 10px 16px;
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
.
|
|
393
|
+
.BL43XG_tableScroll th:first-child, .BL43XG_tableScroll td:first-child {
|
|
394
394
|
padding-left: 0;
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
-
.
|
|
397
|
+
.BL43XG_tableScroll td:last-child {
|
|
398
398
|
padding-right: 0;
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
.
|
|
401
|
+
.BL43XG_tableScroll table code {
|
|
402
402
|
font-size: 11px;
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
.
|
|
405
|
+
.BL43XG_markdown > :first-child, .BL43XG_markdown p:first-child {
|
|
406
406
|
margin-top: 0 !important;
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
-
.
|
|
409
|
+
.BL43XG_markdown > :last-child, .BL43XG_markdown p:last-child {
|
|
410
410
|
margin-bottom: 0 !important;
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
-
.
|
|
413
|
+
.BL43XG_imageAlt {
|
|
414
414
|
color: var(--dsw-alias-label-tertiary);
|
|
415
415
|
font-style: italic;
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
.
|
|
418
|
+
.BL43XG_image {
|
|
419
419
|
background: var(--dsw-alias-bg-base);
|
|
420
420
|
object-fit: contain;
|
|
421
421
|
border-radius: 8px;
|
|
@@ -426,7 +426,7 @@
|
|
|
426
426
|
display: block;
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
-
.
|
|
429
|
+
.BL43XG_fileMention {
|
|
430
430
|
font: inherit;
|
|
431
431
|
color: var(--dsw-alias-link);
|
|
432
432
|
cursor: pointer;
|
|
@@ -438,7 +438,7 @@
|
|
|
438
438
|
text-decoration: none;
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
-
.
|
|
441
|
+
.BL43XG_fileMention:hover, .BL43XG_fileMention:focus {
|
|
442
442
|
text-decoration: underline dotted var(--dsw-alias-link);
|
|
443
443
|
text-underline-offset: 3px;
|
|
444
444
|
outline: none;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plus/lifeboat",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "dsh-plus service plugin: 故障救生艇——兄弟插件崩溃时自动隔离(写用户 patch 层禁用),llm-pi 缺席时把其配置临时翻译为官方 llm-pi-ai 路由,恢复后自动还原",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -31,21 +31,21 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"yaml": "^2.9.0",
|
|
34
|
-
"@dsh-plus/shared": "0.1.
|
|
34
|
+
"@dsh-plus/shared": "0.1.11"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
38
|
-
"@deepseek-ai/dsh-home-paths": "
|
|
39
|
-
"@deepseek-ai/dsh-llm": "
|
|
40
|
-
"@deepseek-ai/dsh-settings": "
|
|
41
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
37
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
38
|
+
"@deepseek-ai/dsh-home-paths": "*",
|
|
39
|
+
"@deepseek-ai/dsh-llm": "*",
|
|
40
|
+
"@deepseek-ai/dsh-settings": "*",
|
|
41
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@deepseek-ai/cordis": "4.0.2",
|
|
45
|
-
"@deepseek-ai/dsh-home-paths": "0.1.
|
|
46
|
-
"@deepseek-ai/dsh-host-webserver": "0.1.
|
|
47
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
48
|
-
"@deepseek-ai/dsh-settings": "0.1.
|
|
45
|
+
"@deepseek-ai/dsh-home-paths": "0.1.5-alpha.1",
|
|
46
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.5-alpha.1",
|
|
47
|
+
"@deepseek-ai/dsh-llm": "0.1.5-alpha.1",
|
|
48
|
+
"@deepseek-ai/dsh-settings": "0.1.5-alpha.1",
|
|
49
49
|
"@deepseek-ai/schemastery": "3.18.2",
|
|
50
50
|
"@types/node": "26.4.0",
|
|
51
51
|
"@types/react": "~18.3.31",
|
package/src/config.ts
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 配置单一事实源。
|
|
3
3
|
* - 行级 Config(cordis 组合层):救生艇自身行为开关,dev/prod patch 层可覆盖。
|
|
4
|
-
* -
|
|
5
|
-
*
|
|
6
|
-
* schema 刻意宽松:lifeboat 是最后防线,自身命名空间的数据问题绝不能让它起不来。
|
|
4
|
+
* - journal 与 LLM 应急翻译状态属运行期数据,按存储规范持久化到
|
|
5
|
+
* $DSH_HOME/dsh-plus/lifeboat/state.json(见 state-file.ts),不进 settings。
|
|
7
6
|
* @module lifeboat/config
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import z from '@deepseek-ai/schemastery'
|
|
11
10
|
|
|
12
|
-
import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
|
|
13
|
-
|
|
14
|
-
/** settings 命名空间(字面量直供 settings 服务注册/读写,0.1.2-alpha.2 起编译期校验语法)。 */
|
|
15
|
-
export const SETTINGS_NS = NS_LITERAL
|
|
16
|
-
|
|
17
11
|
/** 行级配置:全部有默认值,正常部署零配置。 */
|
|
18
12
|
export const Config = z.object({
|
|
19
13
|
enabled: z.boolean().description('故障隔离总开关').default(true),
|
|
@@ -32,33 +26,19 @@ export const Config = z.object({
|
|
|
32
26
|
|
|
33
27
|
export type LifeboatConfig = Schemastery.TypeT<typeof Config>
|
|
34
28
|
|
|
35
|
-
/** journal
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
/** 命名空间 schema:宽松承载 journal 与翻译状态,上限截断防膨胀。 */
|
|
54
|
-
// biome-ignore lint/suspicious/noExplicitAny: dts 可移植性(同 JournalEntry;上次成功构建未触发是因旧 lockfile,现同 usage-panel 口径)
|
|
55
|
-
export const JournalSchema: any = z.object({
|
|
56
|
-
journal: z.array(JournalEntry).description('救生艇操作日志(最新在尾,封顶 50 条)').default([]),
|
|
57
|
-
llmFallback: z
|
|
58
|
-
.union([FallbackState, z.const(null)])
|
|
59
|
-
.description('LLM 应急翻译状态;null 表示未处于降级')
|
|
60
|
-
.default(null),
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
export type JournalDoc = Schemastery.TypeT<typeof JournalSchema>
|
|
64
|
-
export type FallbackStateT = Schemastery.TypeT<typeof FallbackState>
|
|
29
|
+
/** journal 单条记录(纯类型,磁盘收窄见 state-file.ts)。 */
|
|
30
|
+
export interface JournalEntryT {
|
|
31
|
+
at: string
|
|
32
|
+
kind: string
|
|
33
|
+
detail: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** LLM 应急翻译的持久状态(还原依据;纯类型,磁盘收窄见 state-file.ts)。 */
|
|
37
|
+
export interface FallbackStateT {
|
|
38
|
+
active: boolean
|
|
39
|
+
originalProvider: string
|
|
40
|
+
originalModel: string
|
|
41
|
+
fallbackProvider: string
|
|
42
|
+
providers: string[]
|
|
43
|
+
at: string
|
|
44
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,28 +6,26 @@
|
|
|
6
6
|
* 失败插件缺席、其余正常,修复后删覆盖即恢复。
|
|
7
7
|
* 2. LLM 应急翻译——默认模型 provider 无 adapter 时,把 dsh-plus-llm-pi 配置
|
|
8
8
|
* 临时翻译为官方 llm-pi-ai 路由并切换默认模型,恢复后自动还原。
|
|
9
|
-
* 3. 告警与 journal
|
|
10
|
-
* 发邮件(缺席降级为日志)。
|
|
9
|
+
* 3. 告警与 journal——一切动作记录进运行期状态文件(state-file.ts),并经
|
|
10
|
+
* notify-email 发邮件(缺席降级为日志)。
|
|
11
11
|
*
|
|
12
12
|
* 零 dsh-plus 内部依赖铁律:不 import 本仓库任何其他包,防止共享代码故障团灭。
|
|
13
|
+
*
|
|
14
|
+
* 接线约束:inject 声明须覆盖本模块全部 ctx.<service> 直接访问(fallback-llm
|
|
15
|
+
* 读 settings 命名空间与 llm.listProviders),漏掉任一即运行期
|
|
16
|
+
* "cannot get property without inject" → 插件树加载失败 → dsh boot 中止。
|
|
13
17
|
* @module @dsh-plus/lifeboat
|
|
14
18
|
*/
|
|
15
19
|
import type { Context } from '@deepseek-ai/cordis'
|
|
16
20
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
|
17
21
|
|
|
18
|
-
import {
|
|
19
|
-
Config,
|
|
20
|
-
type FallbackStateT,
|
|
21
|
-
type JournalDoc,
|
|
22
|
-
JournalSchema,
|
|
23
|
-
type LifeboatConfig,
|
|
24
|
-
SETTINGS_NS,
|
|
25
|
-
} from './config.ts'
|
|
22
|
+
import { Config, type FallbackStateT, type LifeboatConfig } from './config.ts'
|
|
26
23
|
import { installLlmFallback } from './fallback-llm.ts'
|
|
27
24
|
import { registerHealthApi } from './health-api.ts'
|
|
28
25
|
import { installAlerter } from './notify.ts'
|
|
29
26
|
import { createQuarantine, installHostWatch } from './quarantine.ts'
|
|
30
27
|
import { registerQuarantineApi } from './quarantine-api.ts'
|
|
28
|
+
import { appendJournal, loadState, type StateDoc, saveState } from './state-file.ts'
|
|
31
29
|
|
|
32
30
|
export const name = 'dsh-plus-lifeboat'
|
|
33
31
|
|
|
@@ -35,34 +33,20 @@ export const inject = ['settings', 'llm'] as const
|
|
|
35
33
|
|
|
36
34
|
export { Config }
|
|
37
35
|
|
|
38
|
-
/** journal 上限:只留最近 50 条,防长期运行膨胀。 */
|
|
39
|
-
const JOURNAL_CAP = 50
|
|
40
|
-
|
|
41
36
|
export function apply(ctx: Context, config: LifeboatConfig): void {
|
|
42
37
|
const logger = ctx.logger('lifeboat')
|
|
43
38
|
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
let
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
persist = (patch) => scope.update(patch)
|
|
52
|
-
} catch (error) {
|
|
53
|
-
logger.warn(
|
|
54
|
-
`journal 命名空间注册失败,降级为内存态: ${error instanceof Error ? error.message : String(error)}`,
|
|
55
|
-
)
|
|
56
|
-
}
|
|
39
|
+
// 运行期状态(journal + 翻译状态):文件持久化。加载失败降级为内存态
|
|
40
|
+
// (重启丢失 journal,但隔离/翻译功能仍在)。
|
|
41
|
+
const warn = (message: string): void => logger.warn(message)
|
|
42
|
+
let doc: StateDoc = { journal: [], llmFallback: null }
|
|
43
|
+
void loadState().then((loaded) => {
|
|
44
|
+
doc = loaded
|
|
45
|
+
})
|
|
57
46
|
|
|
58
47
|
const journal = (kind: string, detail: string): void => {
|
|
59
|
-
doc =
|
|
60
|
-
|
|
61
|
-
journal: [...doc.journal, { at: new Date().toISOString(), kind, detail }].slice(-JOURNAL_CAP),
|
|
62
|
-
}
|
|
63
|
-
void persist?.({ journal: doc.journal }).catch((error: unknown) => {
|
|
64
|
-
logger.warn(`journal 持久化失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
65
|
-
})
|
|
48
|
+
doc = appendJournal(doc, kind, detail)
|
|
49
|
+
void saveState(doc, warn)
|
|
66
50
|
}
|
|
67
51
|
|
|
68
52
|
const alert = installAlerter(ctx, journal)
|
|
@@ -98,7 +82,7 @@ export function apply(ctx: Context, config: LifeboatConfig): void {
|
|
|
98
82
|
readState: () => doc.llmFallback,
|
|
99
83
|
writeState: async (state: FallbackStateT | null) => {
|
|
100
84
|
doc = { ...doc, llmFallback: state }
|
|
101
|
-
await
|
|
85
|
+
await saveState(doc, warn)
|
|
102
86
|
},
|
|
103
87
|
})
|
|
104
88
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* journal 与 LLM 应急翻译状态的文件持久化($DSH_HOME/dsh-plus/lifeboat/state.json)。
|
|
3
|
+
*
|
|
4
|
+
* 存储规范:运行期状态属非配置数据,不进 settings.yaml;本模块自带实现,
|
|
5
|
+
* 维持零 dsh-plus 内部依赖铁律(不 import @dsh-plus/shared)。
|
|
6
|
+
* schema 刻意宽松:lifeboat 是最后防线,自身数据问题绝不能让它起不来。
|
|
7
|
+
* @module lifeboat/state-file
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises'
|
|
11
|
+
import { dirname } from 'node:path'
|
|
12
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
|
13
|
+
import type { FallbackStateT, JournalEntryT } from './config.ts'
|
|
14
|
+
|
|
15
|
+
/** 状态文件落点:$DSH_HOME/dsh-plus/lifeboat/state.json。 */
|
|
16
|
+
export const STATE_FILE = dshHomePath('dsh-plus', 'lifeboat', 'state.json')
|
|
17
|
+
|
|
18
|
+
/** 磁盘文档形状(与旧 settings 命名空间同构,仅位置不同)。 */
|
|
19
|
+
export interface StateDoc {
|
|
20
|
+
journal: JournalEntryT[]
|
|
21
|
+
llmFallback: FallbackStateT | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** journal 上限:只留最近 50 条,防长期运行膨胀。 */
|
|
25
|
+
const JOURNAL_CAP = 50
|
|
26
|
+
|
|
27
|
+
/** 文件大小上限:超出即视为损坏,丢弃重来(journal 本身封顶 50 条)。 */
|
|
28
|
+
const MAX_BYTES = 256 * 1024
|
|
29
|
+
|
|
30
|
+
/** 未知输入收窄为合法 journal 条目(宽松承载数据问题)。 */
|
|
31
|
+
function asEntry(value: unknown): JournalEntryT | null {
|
|
32
|
+
if (typeof value !== 'object' || value === null) return null
|
|
33
|
+
const record = value as Record<string, unknown>
|
|
34
|
+
if (typeof record['at'] !== 'string' || typeof record['kind'] !== 'string') return null
|
|
35
|
+
return { at: record['at'], kind: record['kind'], detail: String(record['detail'] ?? '') }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 未知输入收窄为合法翻译状态。 */
|
|
39
|
+
function asFallback(value: unknown): FallbackStateT | null {
|
|
40
|
+
if (typeof value !== 'object' || value === null) return null
|
|
41
|
+
const record = value as Record<string, unknown>
|
|
42
|
+
if (typeof record['active'] !== 'boolean') return null
|
|
43
|
+
return {
|
|
44
|
+
active: record['active'],
|
|
45
|
+
originalProvider: String(record['originalProvider'] ?? ''),
|
|
46
|
+
originalModel: String(record['originalModel'] ?? ''),
|
|
47
|
+
fallbackProvider: String(record['fallbackProvider'] ?? ''),
|
|
48
|
+
providers: Array.isArray(record['providers'])
|
|
49
|
+
? record['providers'].map((item) => String(item))
|
|
50
|
+
: [],
|
|
51
|
+
at: String(record['at'] ?? ''),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 未知输入收窄为完整状态文档;任何缺省回落到空文档。 */
|
|
56
|
+
export function normalizeState(raw: unknown): StateDoc {
|
|
57
|
+
if (typeof raw !== 'object' || raw === null) return { journal: [], llmFallback: null }
|
|
58
|
+
const record = raw as Record<string, unknown>
|
|
59
|
+
const journal = Array.isArray(record['journal'])
|
|
60
|
+
? record['journal'].map(asEntry).filter((entry): entry is JournalEntryT => entry !== null)
|
|
61
|
+
: []
|
|
62
|
+
return { journal: journal.slice(-JOURNAL_CAP), llmFallback: asFallback(record['llmFallback']) }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 读取状态文件;缺失/损坏/超限时返回空文档(最后防线不得因自身数据起不来)。 */
|
|
66
|
+
export async function loadState(): Promise<StateDoc> {
|
|
67
|
+
try {
|
|
68
|
+
const info = await stat(STATE_FILE)
|
|
69
|
+
if (!info.isFile() || info.size > MAX_BYTES) return { journal: [], llmFallback: null }
|
|
70
|
+
return normalizeState(JSON.parse(await readFile(STATE_FILE, 'utf-8')))
|
|
71
|
+
} catch {
|
|
72
|
+
return { journal: [], llmFallback: null }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 原子写状态文件:临时文件 + rename,写入失败仅告警不抛出。 */
|
|
77
|
+
export async function saveState(doc: StateDoc, warn: (message: string) => void): Promise<void> {
|
|
78
|
+
try {
|
|
79
|
+
await mkdir(dirname(STATE_FILE), { recursive: true })
|
|
80
|
+
const tmp = `${STATE_FILE}.tmp`
|
|
81
|
+
await writeFile(tmp, `${JSON.stringify(doc, null, 2)}\n`, 'utf-8')
|
|
82
|
+
await rename(tmp, STATE_FILE)
|
|
83
|
+
} catch (error) {
|
|
84
|
+
warn(`状态文件写入失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 追加一条 journal(内存文档已更新后调用;落盘失败不阻断动作)。 */
|
|
89
|
+
export function appendJournal(doc: StateDoc, kind: string, detail: string): StateDoc {
|
|
90
|
+
return {
|
|
91
|
+
...doc,
|
|
92
|
+
journal: [...doc.journal, { at: new Date().toISOString(), kind, detail }].slice(-JOURNAL_CAP),
|
|
93
|
+
}
|
|
94
|
+
}
|