@dsh-plus/lifeboat 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +27 -29
- package/lib/index.d.ts +0 -3
- package/lib/index.js +65 -45
- package/package.json +5 -5
- package/src/quarantine.ts +22 -6
package/lib/client.js
CHANGED
|
@@ -3,37 +3,35 @@ window.__ModuleLoader__.load({
|
|
|
3
3
|
factory: (require) => {
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
function apply(ctx) {
|
|
16
|
-
try {
|
|
17
|
-
const reported = /* @__PURE__ */ new Set();
|
|
18
|
-
ctx.root.on("internal/status", (fiber) => {
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region src/client/client.ts
|
|
8
|
+
const name = "dsh-plus-lifeboat";
|
|
9
|
+
/** Runtime mirror: FiberState 是 cordis 跨包 const enum(FAILED = 3,cordis 4.0.1)。 */
|
|
10
|
+
const FIBER_FAILED = 3;
|
|
11
|
+
/** 永不回报的条目:救生艇自身与聚合 bundle。 */
|
|
12
|
+
const EXCLUDED = /* @__PURE__ */ new Set(["dsh-plus-lifeboat", "dsh-plus-bundle-main"]);
|
|
13
|
+
const GUARD_RE = /^dsh-plus-[a-z0-9-]+$/;
|
|
14
|
+
function apply(ctx) {
|
|
19
15
|
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
const reported = /* @__PURE__ */ new Set();
|
|
17
|
+
ctx.root.on("internal/status", (fiber) => {
|
|
18
|
+
try {
|
|
19
|
+
if (fiber.state !== FIBER_FAILED) return;
|
|
20
|
+
const plugin = fiber.name;
|
|
21
|
+
if (!GUARD_RE.test(plugin) || EXCLUDED.has(plugin) || reported.has(plugin)) return;
|
|
22
|
+
reported.add(plugin);
|
|
23
|
+
fetch("/dsh-plus/lifeboat/quarantine", {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "content-type": "application/json" },
|
|
26
|
+
body: JSON.stringify({ name: plugin })
|
|
27
|
+
}).catch(() => {});
|
|
28
|
+
} catch {}
|
|
29
|
+
});
|
|
29
30
|
} catch {}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
//#endregion
|
|
35
|
-
exports.apply = apply;
|
|
36
|
-
exports.name = name;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
exports.apply = apply;
|
|
34
|
+
exports.name = name;
|
|
37
35
|
return module.exports;
|
|
38
36
|
}
|
|
39
37
|
});
|
package/lib/index.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import "@deepseek-ai/dsh-settings";
|
|
2
1
|
import z from "@deepseek-ai/schemastery";
|
|
3
2
|
import { Context } from "@deepseek-ai/cordis";
|
|
4
|
-
|
|
5
3
|
//#region src/config.d.ts
|
|
6
|
-
|
|
7
4
|
/** 行级配置:全部有默认值,正常部署零配置。 */
|
|
8
5
|
declare const Config: z<Schemastery.ObjectS<{
|
|
9
6
|
enabled: z<boolean, boolean>;
|
package/lib/index.js
CHANGED
|
@@ -3,18 +3,18 @@ import { SettingsConflictError, settingsNamespace } from "@deepseek-ai/dsh-setti
|
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
4
|
import { copyFile, readFile, rename, writeFile } from "node:fs/promises";
|
|
5
5
|
import { parseDocument } from "yaml";
|
|
6
|
-
|
|
7
|
-
//#region src/ns.ts
|
|
8
|
-
/**
|
|
9
|
-
* settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
|
|
10
|
-
* @module lifeboat/ns
|
|
11
|
-
*/
|
|
12
|
-
const SETTINGS_NS$1 = "dsh-plus-lifeboat";
|
|
13
|
-
|
|
14
6
|
//#endregion
|
|
15
7
|
//#region src/config.ts
|
|
8
|
+
/**
|
|
9
|
+
* 配置与 journal 单一事实源。
|
|
10
|
+
* - 行级 Config(cordis 组合层):救生艇自身行为开关,dev/prod patch 层可覆盖。
|
|
11
|
+
* - settings 命名空间 dsh-plus-lifeboat:运行期 journal 与 LLM 应急翻译状态,
|
|
12
|
+
* 经 dsh-settings-file 持久化到 $DSH_HOME/settings.yaml,重启后可据此还原。
|
|
13
|
+
* schema 刻意宽松:lifeboat 是最后防线,自身命名空间的数据问题绝不能让它起不来。
|
|
14
|
+
* @module lifeboat/config
|
|
15
|
+
*/
|
|
16
16
|
/** settings 命名空间(品牌化后供 settings 服务注册/读写)。 */
|
|
17
|
-
const SETTINGS_NS = settingsNamespace(
|
|
17
|
+
const SETTINGS_NS = settingsNamespace("dsh-plus-lifeboat");
|
|
18
18
|
/** 行级配置:全部有默认值,正常部署零配置。 */
|
|
19
19
|
const Config = z.object({
|
|
20
20
|
enabled: z.boolean().description("故障隔离总开关").default(true),
|
|
@@ -42,18 +42,27 @@ const JournalSchema = z.object({
|
|
|
42
42
|
journal: z.array(JournalEntry).description("救生艇操作日志(最新在尾,封顶 50 条)").default([]),
|
|
43
43
|
llmFallback: z.union([FallbackState, z.const(null)]).description("LLM 应急翻译状态;null 表示未处于降级").default(null)
|
|
44
44
|
});
|
|
45
|
-
|
|
46
45
|
//#endregion
|
|
47
46
|
//#region src/fallback-llm.ts
|
|
47
|
+
/**
|
|
48
|
+
* LLM 应急翻译:默认模型路由的 provider 无已注册 adapter 时(典型场景:llm-pi
|
|
49
|
+
* 被隔离/加载失败),把 settings.yaml 里 dsh-plus-llm-pi 的 provider 配置翻译为
|
|
50
|
+
* 官方 llm-pi-ai 命名空间的原生格式(键加 -fb 后缀避开 DUPLICATE_ADAPTER 冲突),
|
|
51
|
+
* 默认模型指到翻译路由;源 provider 恢复健康后按 journal 自动还原。
|
|
52
|
+
*
|
|
53
|
+
* 为什么读原始 settings.yaml 而不是 settings 服务:插件被禁用后其命名空间随之
|
|
54
|
+
* 注销,服务层读不到——但数据仍在文件里(只读,不修改)。
|
|
55
|
+
* 降级语义:翻译只覆盖官方物化的字段子集,dsh-plus 扩展的全量 compat 丢弃;
|
|
56
|
+
* 协议推断是启发式(显式 api > 路由名 > 模型继承源 > openai-completions 兜底)。
|
|
57
|
+
* @module lifeboat/fallback-llm
|
|
58
|
+
*/
|
|
48
59
|
const NS_PI_AI = settingsNamespace("llm-pi-ai");
|
|
49
60
|
const NS_AGENT_DEFAULT = settingsNamespace("agent-default-model");
|
|
50
61
|
const RAW_NS_LLM_PI = "dsh-plus-llm-pi";
|
|
51
|
-
/** 翻译路由键后缀:与源路由错开,避免 llm-pi 恢复瞬间撞 DUPLICATE_ADAPTER。 */
|
|
52
|
-
const FALLBACK_SUFFIX = "-fb";
|
|
53
62
|
/** 官方配置路径物化的 compat 子集(其余字段降级期丢弃)。 */
|
|
54
63
|
const COMPAT_WHITELIST = ["thinkingFormat", "supportsReasoningEffort"];
|
|
55
64
|
/** 官方 reasoning 词表(ModelThinkingLevel 的保守子集),不在其中的值丢弃。 */
|
|
56
|
-
const REASONING_LEVELS = new Set([
|
|
65
|
+
const REASONING_LEVELS = /* @__PURE__ */ new Set([
|
|
57
66
|
"off",
|
|
58
67
|
"minimal",
|
|
59
68
|
"low",
|
|
@@ -120,12 +129,13 @@ function translateProvider(routeKey, raw) {
|
|
|
120
129
|
/** 整表翻译:键加 -fb 后缀(纯函数)。 */
|
|
121
130
|
function translateProviders(providers) {
|
|
122
131
|
const out = {};
|
|
123
|
-
for (const [key, raw] of Object.entries(providers)) out[`${key}
|
|
132
|
+
for (const [key, raw] of Object.entries(providers)) out[`${key}-fb`] = translateProvider(key, raw);
|
|
124
133
|
return out;
|
|
125
134
|
}
|
|
126
135
|
/** 读 settings.yaml 原始文档的指定段落(只读;文件/段落缺失返回 undefined)。 */
|
|
127
136
|
async function readRawSection(settingsFile, ns) {
|
|
128
|
-
const
|
|
137
|
+
const text = await readFile(settingsFile, "utf-8");
|
|
138
|
+
const section = parseDocument(text).toJS()[ns];
|
|
129
139
|
if (typeof section !== "object" || section === null) return void 0;
|
|
130
140
|
return section;
|
|
131
141
|
}
|
|
@@ -154,7 +164,7 @@ function installLlmFallback(ctx, deps) {
|
|
|
154
164
|
return;
|
|
155
165
|
}
|
|
156
166
|
const translated = translateProviders(providers);
|
|
157
|
-
const fbProvider = `${provider}
|
|
167
|
+
const fbProvider = `${provider}-fb`;
|
|
158
168
|
await writeWithRetry(() => writes.update(NS_PI_AI, { providers: translated }));
|
|
159
169
|
await writeWithRetry(() => writes.update(NS_AGENT_DEFAULT, {
|
|
160
170
|
provider: fbProvider,
|
|
@@ -210,7 +220,7 @@ function installLlmFallback(ctx, deps) {
|
|
|
210
220
|
const bootTimer = setTimeout(run, 1e4);
|
|
211
221
|
ctx.effect(() => () => clearTimeout(bootTimer), "lifeboat: fallback boot timer");
|
|
212
222
|
ctx.on("ready", run);
|
|
213
|
-
const WATCHED_NS = new Set([
|
|
223
|
+
const WATCHED_NS = /* @__PURE__ */ new Set([
|
|
214
224
|
"agent-default-model",
|
|
215
225
|
"llm-pi-ai",
|
|
216
226
|
RAW_NS_LLM_PI
|
|
@@ -220,7 +230,6 @@ function installLlmFallback(ctx, deps) {
|
|
|
220
230
|
});
|
|
221
231
|
ctx.root.on("llm/adapters-updated", run);
|
|
222
232
|
}
|
|
223
|
-
|
|
224
233
|
//#endregion
|
|
225
234
|
//#region src/notify.ts
|
|
226
235
|
/**
|
|
@@ -253,9 +262,15 @@ function installAlerter(ctx, journal) {
|
|
|
253
262
|
});
|
|
254
263
|
};
|
|
255
264
|
}
|
|
256
|
-
|
|
257
265
|
//#endregion
|
|
258
266
|
//#region src/patch-file.ts
|
|
267
|
+
/**
|
|
268
|
+
* profile 用户 patch 文件(cordis.patch.yml)的安全写入。
|
|
269
|
+
* 官方预留的逃生门:用户层支持 id 定向 disabled,覆盖 bundle 层 insert。
|
|
270
|
+
* 写入纪律:先备份、幂等(已禁用跳过)、原子落盘(tmp + rename),失败抛错由调用方告警,
|
|
271
|
+
* 绝不写半个文件、绝不改动既有条目内容。
|
|
272
|
+
* @module lifeboat/patch-file
|
|
273
|
+
*/
|
|
259
274
|
/** 条目(含 insert 子列表)中是否已有针对 targetId 的 disabled 覆盖。 */
|
|
260
275
|
function hasDisable(entries, targetId) {
|
|
261
276
|
for (const entry of entries) {
|
|
@@ -269,7 +284,8 @@ function hasDisable(entries, targetId) {
|
|
|
269
284
|
* @throws 文件不可读/顶层不是数组/落盘失败——调用方负责告警。
|
|
270
285
|
*/
|
|
271
286
|
async function appendDisable(patchFile, targetId) {
|
|
272
|
-
const
|
|
287
|
+
const text = await readFile(patchFile, "utf-8");
|
|
288
|
+
const doc = parseDocument(text);
|
|
273
289
|
const root = doc.toJS();
|
|
274
290
|
if (!Array.isArray(root)) throw new Error(`patch 文件顶层不是数组: ${patchFile}`);
|
|
275
291
|
if (hasDisable(root, targetId)) return false;
|
|
@@ -283,42 +299,51 @@ async function appendDisable(patchFile, targetId) {
|
|
|
283
299
|
await rename(tmp, patchFile);
|
|
284
300
|
return true;
|
|
285
301
|
}
|
|
286
|
-
|
|
287
302
|
//#endregion
|
|
288
303
|
//#region src/quarantine.ts
|
|
289
304
|
/** Runtime mirror: FiberState 是 cordis 跨包 const enum(官方 dsh-host-plugin-inventory 同款做法)。 */
|
|
290
305
|
const FIBER_FAILED = 3;
|
|
291
306
|
/** 永不隔离的条目:救生艇自身与聚合 bundle(禁了它们等于关掉整个防线/全家)。 */
|
|
292
|
-
const EXCLUDED = new Set(["dsh-plus-lifeboat", "dsh-plus-bundle-main"]);
|
|
307
|
+
const EXCLUDED = /* @__PURE__ */ new Set(["dsh-plus-lifeboat", "dsh-plus-bundle-main"]);
|
|
293
308
|
const GUARD_RE = /^dsh-plus-[a-z0-9-]+$/;
|
|
294
309
|
/** 名字是否属于可隔离的 dsh-plus 兄弟插件(entry id 与插件 name 同字面量是本仓库约定)。 */
|
|
295
|
-
function isGuardedPlugin(name
|
|
296
|
-
return typeof name
|
|
310
|
+
function isGuardedPlugin(name) {
|
|
311
|
+
return typeof name === "string" && GUARD_RE.test(name) && !EXCLUDED.has(name);
|
|
312
|
+
}
|
|
313
|
+
/** 告警/journal 里的失败原因摘录上限,防长堆栈淹没通知渠道。 */
|
|
314
|
+
const REASON_CAP = 500;
|
|
315
|
+
/** 提取 fiber 失败原因的短文本(无则空串);cordis 把错误挂在 fiber._error。 */
|
|
316
|
+
function reasonText(reason) {
|
|
317
|
+
if (reason === void 0 || reason === null) return "";
|
|
318
|
+
const text = reason instanceof Error ? reason.stack ?? reason.message : String(reason);
|
|
319
|
+
return text.length > REASON_CAP ? `${text.slice(0, REASON_CAP)}…` : text;
|
|
297
320
|
}
|
|
298
321
|
/** 装配隔离执行器(纯逻辑,可测)。 */
|
|
299
322
|
function createQuarantine(ctx, deps) {
|
|
300
323
|
const logger = ctx.logger("lifeboat");
|
|
301
324
|
const lastAlertAt = /* @__PURE__ */ new Map();
|
|
302
325
|
const inFlight = /* @__PURE__ */ new Set();
|
|
303
|
-
async function quarantine(name
|
|
304
|
-
if (!isGuardedPlugin(name
|
|
305
|
-
if (inFlight.has(name
|
|
306
|
-
inFlight.add(name
|
|
326
|
+
async function quarantine(name, origin, reason) {
|
|
327
|
+
if (!isGuardedPlugin(name)) return;
|
|
328
|
+
if (inFlight.has(name)) return;
|
|
329
|
+
inFlight.add(name);
|
|
330
|
+
const reasonBlock = reasonText(reason);
|
|
331
|
+
const reasonSuffix = reasonBlock === "" ? "" : `\n失败原因:\n${reasonBlock}`;
|
|
307
332
|
try {
|
|
308
|
-
const written = await appendDisable(deps.patchFile, name
|
|
309
|
-
deps.journal("quarantine", `${name
|
|
333
|
+
const written = await appendDisable(deps.patchFile, name);
|
|
334
|
+
deps.journal("quarantine", `${name}(来源 ${origin},${written ? "已写入禁用" : "已存在禁用"})${reasonSuffix}`);
|
|
310
335
|
const now = Date.now();
|
|
311
|
-
if (now - (lastAlertAt.get(name
|
|
312
|
-
lastAlertAt.set(name
|
|
313
|
-
deps.alert(`[DSH] 插件 ${name
|
|
336
|
+
if (now - (lastAlertAt.get(name) ?? 0) >= deps.alertCooldownMs) {
|
|
337
|
+
lastAlertAt.set(name, now);
|
|
338
|
+
deps.alert(`[DSH] 插件 ${name} 加载失败,已自动隔离`, `检测到插件 ${name} 的 fiber 进入 FAILED(来源:${origin})。\n已向 ${deps.patchFile} 写入 disabled 覆盖,重启/刷新后该插件不再加载,其余插件不受影响。\n修复后删除该条覆盖即可恢复。${reasonSuffix}`);
|
|
314
339
|
}
|
|
315
340
|
} catch (error) {
|
|
316
341
|
const message = error instanceof Error ? error.message : String(error);
|
|
317
|
-
logger.warn(`隔离 ${name
|
|
318
|
-
deps.journal("quarantine-error", `${name
|
|
319
|
-
deps.alert(`[DSH] 插件 ${name
|
|
342
|
+
logger.warn(`隔离 ${name} 失败: ${message}`);
|
|
343
|
+
deps.journal("quarantine-error", `${name}: ${message}`);
|
|
344
|
+
deps.alert(`[DSH] 插件 ${name} 加载失败,自动隔离未果`, `隔离写入失败:${message}\n请手动在 patch 层禁用该插件。`);
|
|
320
345
|
} finally {
|
|
321
|
-
inFlight.delete(name
|
|
346
|
+
inFlight.delete(name);
|
|
322
347
|
}
|
|
323
348
|
}
|
|
324
349
|
return { quarantine };
|
|
@@ -331,10 +356,9 @@ function installHostWatch(ctx, q) {
|
|
|
331
356
|
ctx.root.on("internal/status", (fiber) => {
|
|
332
357
|
if (fiber.state !== FIBER_FAILED) return;
|
|
333
358
|
if (!isGuardedPlugin(fiber.name)) return;
|
|
334
|
-
q.quarantine(fiber.name, "host");
|
|
359
|
+
q.quarantine(fiber.name, "host", fiber._error);
|
|
335
360
|
});
|
|
336
361
|
}
|
|
337
|
-
|
|
338
362
|
//#endregion
|
|
339
363
|
//#region src/quarantine-api.ts
|
|
340
364
|
const ROUTE = "/dsh-plus/lifeboat/quarantine";
|
|
@@ -378,13 +402,10 @@ function registerQuarantineApi(ctx, q) {
|
|
|
378
402
|
}
|
|
379
403
|
});
|
|
380
404
|
}
|
|
381
|
-
|
|
382
405
|
//#endregion
|
|
383
406
|
//#region src/index.ts
|
|
384
407
|
const name = "dsh-plus-lifeboat";
|
|
385
408
|
const inject = ["settings", "llm"];
|
|
386
|
-
/** journal 上限:只留最近 50 条,防长期运行膨胀。 */
|
|
387
|
-
const JOURNAL_CAP = 50;
|
|
388
409
|
function apply(ctx, config) {
|
|
389
410
|
const logger = ctx.logger("lifeboat");
|
|
390
411
|
let doc = {
|
|
@@ -406,7 +427,7 @@ function apply(ctx, config) {
|
|
|
406
427
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
407
428
|
kind,
|
|
408
429
|
detail
|
|
409
|
-
}].slice(-
|
|
430
|
+
}].slice(-50)
|
|
410
431
|
};
|
|
411
432
|
persist?.({ journal: doc.journal }).catch((error) => {
|
|
412
433
|
logger.warn(`journal 持久化失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -438,6 +459,5 @@ function apply(ctx, config) {
|
|
|
438
459
|
}
|
|
439
460
|
});
|
|
440
461
|
}
|
|
441
|
-
|
|
442
462
|
//#endregion
|
|
443
|
-
export { Config, apply, inject, name };
|
|
463
|
+
export { Config, apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plus/lifeboat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "dsh-plus service plugin: 故障救生艇——兄弟插件崩溃时自动隔离(写用户 patch 层禁用),llm-pi 缺席时把其配置临时翻译为官方 llm-pi-ai 路由,恢复后自动还原",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@deepseek-ai/cordis": "4.0.1",
|
|
41
|
-
"@deepseek-ai/dsh-home-paths": "0.1.
|
|
42
|
-
"@deepseek-ai/dsh-host-webserver": "0.1.
|
|
43
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
44
|
-
"@deepseek-ai/dsh-settings": "0.1.
|
|
41
|
+
"@deepseek-ai/dsh-home-paths": "0.1.1-rc.2",
|
|
42
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.1-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-llm": "0.1.1-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-settings": "0.1.1-rc.2",
|
|
45
45
|
"@deepseek-ai/schemastery": "3.18.1"
|
|
46
46
|
},
|
|
47
47
|
"publishConfig": {
|
package/src/quarantine.ts
CHANGED
|
@@ -31,7 +31,17 @@ export interface QuarantineDeps {
|
|
|
31
31
|
|
|
32
32
|
export interface Quarantine {
|
|
33
33
|
/** 隔离指定插件(幂等;冷却期内重复触发只计 journal 不写文件)。 */
|
|
34
|
-
quarantine(name: string, origin: 'host' | 'client'): Promise<void>
|
|
34
|
+
quarantine(name: string, origin: 'host' | 'client', reason?: unknown): Promise<void>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 告警/journal 里的失败原因摘录上限,防长堆栈淹没通知渠道。 */
|
|
38
|
+
const REASON_CAP = 500
|
|
39
|
+
|
|
40
|
+
/** 提取 fiber 失败原因的短文本(无则空串);cordis 把错误挂在 fiber._error。 */
|
|
41
|
+
function reasonText(reason: unknown): string {
|
|
42
|
+
if (reason === undefined || reason === null) return ''
|
|
43
|
+
const text = reason instanceof Error ? (reason.stack ?? reason.message) : String(reason)
|
|
44
|
+
return text.length > REASON_CAP ? `${text.slice(0, REASON_CAP)}…` : text
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
/** 装配隔离执行器(纯逻辑,可测)。 */
|
|
@@ -40,15 +50,21 @@ export function createQuarantine(ctx: Context, deps: QuarantineDeps): Quarantine
|
|
|
40
50
|
const lastAlertAt = new Map<string, number>()
|
|
41
51
|
const inFlight = new Set<string>()
|
|
42
52
|
|
|
43
|
-
async function quarantine(
|
|
53
|
+
async function quarantine(
|
|
54
|
+
name: string,
|
|
55
|
+
origin: 'host' | 'client',
|
|
56
|
+
reason?: unknown,
|
|
57
|
+
): Promise<void> {
|
|
44
58
|
if (!isGuardedPlugin(name)) return
|
|
45
59
|
if (inFlight.has(name)) return
|
|
46
60
|
inFlight.add(name)
|
|
61
|
+
const reasonBlock = reasonText(reason)
|
|
62
|
+
const reasonSuffix = reasonBlock === '' ? '' : `\n失败原因:\n${reasonBlock}`
|
|
47
63
|
try {
|
|
48
64
|
const written = await appendDisable(deps.patchFile, name)
|
|
49
65
|
deps.journal(
|
|
50
66
|
'quarantine',
|
|
51
|
-
`${name}(来源 ${origin},${written ? '已写入禁用' : '已存在禁用'}
|
|
67
|
+
`${name}(来源 ${origin},${written ? '已写入禁用' : '已存在禁用'})${reasonSuffix}`,
|
|
52
68
|
)
|
|
53
69
|
const now = Date.now()
|
|
54
70
|
const last = lastAlertAt.get(name) ?? 0
|
|
@@ -58,7 +74,7 @@ export function createQuarantine(ctx: Context, deps: QuarantineDeps): Quarantine
|
|
|
58
74
|
`[DSH] 插件 ${name} 加载失败,已自动隔离`,
|
|
59
75
|
`检测到插件 ${name} 的 fiber 进入 FAILED(来源:${origin})。\n` +
|
|
60
76
|
`已向 ${deps.patchFile} 写入 disabled 覆盖,重启/刷新后该插件不再加载,其余插件不受影响。\n` +
|
|
61
|
-
|
|
77
|
+
`修复后删除该条覆盖即可恢复。${reasonSuffix}`,
|
|
62
78
|
)
|
|
63
79
|
}
|
|
64
80
|
} catch (error) {
|
|
@@ -82,9 +98,9 @@ export function createQuarantine(ctx: Context, deps: QuarantineDeps): Quarantine
|
|
|
82
98
|
* fiber.name 对 loader 条目即插件 display name(模块导出的 name 字段)。
|
|
83
99
|
*/
|
|
84
100
|
export function installHostWatch(ctx: Context, q: Quarantine): void {
|
|
85
|
-
ctx.root.on('internal/status', (fiber: { state: number; name: string }) => {
|
|
101
|
+
ctx.root.on('internal/status', (fiber: { state: number; name: string; _error?: unknown }) => {
|
|
86
102
|
if (fiber.state !== FIBER_FAILED) return
|
|
87
103
|
if (!isGuardedPlugin(fiber.name)) return
|
|
88
|
-
void q.quarantine(fiber.name, 'host')
|
|
104
|
+
void q.quarantine(fiber.name, 'host', fiber._error)
|
|
89
105
|
})
|
|
90
106
|
}
|