@dsh-plus/lifeboat 0.2.2 → 0.2.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/index.js CHANGED
@@ -164,12 +164,19 @@ async function writeWithRetry(write) {
164
164
  await write();
165
165
  }
166
166
  }
167
- /** 装配 LLM 应急翻译:启动后及 settings/llm 变化时评估一次(并发合并)。 */
168
- function installLlmFallback(ctx, deps) {
167
+ /** 启动宽限(ms):llm-pi 等兄弟插件的 adapter 注册晚于 lifeboat 就绪,
168
+ * 启动竞态下立即评估会把"还没加载"误判为"缺席"——每次重启都误发
169
+ * 「应急翻译 + 已还原」两条通知。宽限期内一律不评估,宽限结束自动评估
170
+ * 一次,事件订阅覆盖后续变化;真故障(llm-pi 缺席)在宽限后照常告警。 */
171
+ const BOOT_GRACE_MS = 6e4;
172
+ /** 装配 LLM 应急翻译:启动宽限结束后及 settings/llm 变化时评估一次(并发合并)。 */
173
+ function installLlmFallback(ctx, deps, graceMs = BOOT_GRACE_MS) {
169
174
  const logger = ctx.logger("lifeboat");
170
175
  const writes = ctx.settings;
171
176
  const settingsFile = dshHomePath("settings.yaml");
172
177
  let inFlight;
178
+ const startedAt = Date.now();
179
+ const withinGrace = () => Date.now() - startedAt < graceMs;
173
180
  const listProviderIds = () => new Set(ctx.llm.listProviders().map((p) => p.id));
174
181
  async function activate(provider, model) {
175
182
  const providers = (await readRawSection(settingsFile, RAW_NS_LLM_PI).catch(() => void 0))?.providers;
@@ -227,6 +234,7 @@ function installLlmFallback(ctx, deps) {
227
234
  await activate(def.provider, def.model);
228
235
  }
229
236
  const run = () => {
237
+ if (withinGrace()) return;
230
238
  if (inFlight !== void 0) return;
231
239
  inFlight = evaluate().catch((error) => {
232
240
  const message = error instanceof Error ? error.message : String(error);
@@ -236,7 +244,7 @@ function installLlmFallback(ctx, deps) {
236
244
  inFlight = void 0;
237
245
  });
238
246
  };
239
- const bootTimer = setTimeout(run, 1e4);
247
+ const bootTimer = setTimeout(run, graceMs);
240
248
  ctx.effect(() => () => clearTimeout(bootTimer), "lifeboat: fallback boot timer");
241
249
  ctx.on("ready", run);
242
250
  const WATCHED_NS = /* @__PURE__ */ new Set([
package/lib/style.css CHANGED
@@ -1,4 +1,4 @@
1
- .IDEaOG_block {
1
+ .obLtqa_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
- .IDEaOG_copyButton {
11
+ .obLtqa_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
- .IDEaOG_body {
25
+ .obLtqa_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
- .IDEaOG_line {
31
+ .obLtqa_line {
32
32
  min-height: var(--dsl-diff-line-height);
33
33
  white-space: pre;
34
34
  }
35
35
 
36
- .IDEaOG_path {
36
+ .obLtqa_path {
37
37
  color: var(--dsw-alias-label-primary);
38
38
  padding-right: 56px;
39
39
  font-weight: 600;
40
40
  }
41
41
 
42
- .IDEaOG_gap {
42
+ .obLtqa_gap {
43
43
  color: var(--dsw-alias-label-tertiary);
44
44
  }
45
45
 
46
- .IDEaOG_del:before {
46
+ .obLtqa_del:before {
47
47
  content: "- ";
48
48
  color: var(--dsw-alias-state-error-primary);
49
49
  }
50
50
 
51
- .IDEaOG_del {
51
+ .obLtqa_del {
52
52
  color: var(--dsw-alias-state-error-primary);
53
53
  }
54
54
 
55
- .IDEaOG_add:before {
55
+ .obLtqa_add:before {
56
56
  content: "+ ";
57
57
  color: var(--dsw-alias-state-success-primary);
58
58
  }
59
59
 
60
- .IDEaOG_add {
60
+ .obLtqa_add {
61
61
  color: var(--dsw-alias-state-success-primary);
62
62
  }
63
63
 
64
- .IDEaOG_expand {
64
+ .obLtqa_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
- .IDEaOG_expand:hover {
76
+ .obLtqa_expand:hover {
77
77
  color: var(--dsw-alias-label-secondary);
78
78
  }
79
79
 
80
- .IDEaOG_footer {
80
+ .obLtqa_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
- .xeLGkW_block {
85
+ .folzqq_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
- .xeLGkW_block:not(:last-child) {
97
+ .folzqq_block:not(:last-child) {
98
98
  margin-bottom: 11px;
99
99
  }
100
100
 
101
- .xeLGkW_bannerWrap {
101
+ .folzqq_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
- .xeLGkW_banner {
110
+ .folzqq_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
- .xeLGkW_infostring {
122
+ .folzqq_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
- .xeLGkW_action {
133
+ .folzqq_action {
134
134
  flex-shrink: 0;
135
135
  align-items: center;
136
136
  display: flex;
137
137
  }
138
138
 
139
- .xeLGkW_copyButton {
139
+ .folzqq_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
- .xeLGkW_block :where(pre) {
149
+ .folzqq_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,155 +158,166 @@
158
158
  margin: 0 !important;
159
159
  }
160
160
 
161
- .xeLGkW_block :where(pre.xeLGkW_shiki) {
161
+ .folzqq_block :where(pre.folzqq_shiki) {
162
162
  background: var(--dsw-alias-markdown-code-block) !important;
163
163
  }
164
164
 
165
- .xeLGkW_block :where(pre) code {
165
+ .folzqq_block :where(pre) code {
166
166
  font: inherit;
167
167
  background: none;
168
168
  padding: 0;
169
169
  }
170
170
 
171
- .xeLGkW_plain {
171
+ .folzqq_plain {
172
172
  color: var(--dsw-alias-label-primary);
173
173
  }
174
- .Ns7beW_markdown {
174
+ .vrxa6G_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
- .Ns7beW_markdown strong {
181
+ .vrxa6G_markdown strong {
182
182
  font-weight: 600;
183
183
  }
184
184
 
185
- .Ns7beW_markdown h1 {
185
+ .vrxa6G_markdown h1 {
186
186
  font: var(--dsw-font-markdown-h1);
187
187
  margin: 32px 0 16px;
188
188
  }
189
189
 
190
- .Ns7beW_markdown h2 {
190
+ .vrxa6G_markdown h2 {
191
191
  font: var(--dsw-font-markdown-h2);
192
192
  margin: 32px 0 16px;
193
193
  }
194
194
 
195
- .Ns7beW_markdown h3 {
195
+ .vrxa6G_markdown h3 {
196
196
  font: var(--dsw-font-markdown-h3);
197
197
  margin: 32px 0 16px;
198
198
  }
199
199
 
200
- .Ns7beW_markdown h4 {
200
+ .vrxa6G_markdown h4 {
201
201
  font: var(--dsw-font-markdown-h4);
202
202
  margin: 16px 0;
203
203
  }
204
204
 
205
- .Ns7beW_markdown :where(h5, h6) {
205
+ .vrxa6G_markdown :where(h5, h6) {
206
206
  font: var(--dsw-font-markdown-base-strong);
207
207
  margin: 16px 0;
208
208
  }
209
209
 
210
- .Ns7beW_markdown :where(h1, h2, h3, h4, h5, h6) strong {
210
+ .vrxa6G_markdown :where(h1, h2, h3, h4, h5, h6) strong {
211
211
  font-weight: inherit;
212
212
  }
213
213
 
214
- .Ns7beW_markdown p {
214
+ .vrxa6G_markdown p {
215
215
  margin: 16px 0;
216
216
  }
217
217
 
218
- .Ns7beW_markdown :where(h4, h5, h6) + :where(ul, ol) {
218
+ .vrxa6G_markdown :where(h4, h5, h6) + :where(ul, ol) {
219
219
  margin-top: 8px;
220
220
  }
221
221
 
222
- .Ns7beW_markdown :where(h4, h5, h6):has( + :where(ul, ol)) {
222
+ .vrxa6G_markdown :where(h4, h5, h6):has( + :where(ul, ol)) {
223
223
  margin-bottom: 8px;
224
224
  }
225
225
 
226
- .Ns7beW_markdown a {
227
- color: var(--dsw-alias-state-business-primary);
226
+ .vrxa6G_markdown a {
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;
230
230
  border-width: 2px 3px;
231
231
  margin-left: -3px;
232
232
  margin-right: -3px;
233
+ font-weight: 500;
233
234
  text-decoration: none;
234
235
  position: relative;
235
236
  }
236
237
 
237
- .Ns7beW_markdown a:hover, .Ns7beW_markdown a:focus {
238
- text-decoration: underline var(--dsw-alias-state-business-primary);
238
+ .vrxa6G_markdown a:hover, .vrxa6G_markdown a:focus {
239
+ text-decoration: underline dotted var(--dsw-alias-link);
240
+ text-underline-offset: 3px;
239
241
  outline: none;
240
242
  }
241
243
 
242
- .Ns7beW_markdown a:focus-visible {
244
+ .vrxa6G_markdown a:focus-visible {
243
245
  box-shadow: 0 0 0 2px var(--dsw-alias-state-business-primary);
244
246
  }
245
247
 
246
- .Ns7beW_markdown :where(ul, ol) {
248
+ .vrxa6G_linkIcon {
249
+ vertical-align: -.25em;
250
+ flex: none;
251
+ width: 1.1em;
252
+ height: 1.1em;
253
+ margin-right: 5px;
254
+ }
255
+
256
+ .vrxa6G_markdown :where(ul, ol) {
247
257
  margin: 16px 0;
248
258
  padding-left: 18px;
249
259
  }
250
260
 
251
- .Ns7beW_markdown li:not(:first-child) {
261
+ .vrxa6G_markdown li:not(:first-child) {
252
262
  margin-top: 6px;
253
263
  }
254
264
 
255
- .Ns7beW_markdown li > :where(ul, ol) {
265
+ .vrxa6G_markdown li > :where(ul, ol) {
256
266
  margin-top: 4px;
257
267
  }
258
268
 
259
- .Ns7beW_markdown li::marker {
269
+ .vrxa6G_markdown li::marker {
260
270
  color: var(--dsw-alias-label-secondary);
261
271
  line-height: 24px;
262
272
  }
263
273
 
264
- .Ns7beW_markdown :where(ul, ol) ol {
274
+ .vrxa6G_markdown :where(ul, ol) ol {
265
275
  padding-left: 0;
266
276
  list-style-position: inside;
267
277
  }
268
278
 
269
- .Ns7beW_markdown :where(ul, ol) ol li p {
279
+ .vrxa6G_markdown :where(ul, ol) ol li p {
270
280
  display: inline;
271
281
  }
272
282
 
273
- .Ns7beW_markdown li > p {
283
+ .vrxa6G_markdown li > p {
274
284
  margin: 8px 0;
275
285
  }
276
286
 
277
- .Ns7beW_markdown li > :first-child {
287
+ .vrxa6G_markdown li > :first-child {
278
288
  margin-top: 0;
279
289
  }
280
290
 
281
- .Ns7beW_markdown li > :last-child:not(.md-code-block) {
291
+ .vrxa6G_markdown li > :last-child:not(.md-code-block) {
282
292
  margin-bottom: 0;
283
293
  }
284
294
 
285
- .Ns7beW_markdown hr {
295
+ .vrxa6G_markdown hr {
286
296
  background: var(--dsw-alias-border-l2);
287
297
  border: none;
288
- height: 1px;
298
+ height: .5px;
289
299
  margin: 32px 0;
290
300
  display: block;
291
301
  }
292
302
 
293
- .Ns7beW_markdown blockquote {
303
+ .vrxa6G_markdown blockquote {
294
304
  border-left: 2px solid var(--dsw-alias-label-caption);
295
305
  margin: 16px 0 0;
296
306
  padding-left: 14px;
297
307
  }
298
308
 
299
- .Ns7beW_markdown pre {
309
+ .vrxa6G_markdown pre {
300
310
  font-family: var(--ds-font-family-code);
301
311
  margin: 16px 0;
302
312
  overflow: auto;
303
313
  }
304
314
 
305
- .Ns7beW_markdown :not(pre) > code {
315
+ .vrxa6G_markdown :not(pre) > code {
306
316
  box-sizing: border-box;
307
317
  font: var(--dsw-font-markdown-code);
308
318
  font-family: var(--ds-font-family-code);
309
319
  background-color: var(--dsw-alias-markdown-inline-code);
320
+ border: .5px solid var(--dsw-alias-border-l1);
310
321
  border-radius: 6px;
311
322
  align-items: center;
312
323
  padding: 0 5px;
@@ -314,56 +325,56 @@
314
325
  font-size: .875em !important;
315
326
  }
316
327
 
317
- .Ns7beW_markdown :where(h1, h2, h3, h4, h5, h6) code {
328
+ .vrxa6G_markdown :where(h1, h2, h3, h4, h5, h6) code {
318
329
  font: inherit;
319
330
  font-family: var(--ds-font-family-code);
320
331
  }
321
332
 
322
- .Ns7beW_markdown .katex-display {
333
+ .vrxa6G_markdown .katex-display {
323
334
  max-width: 100%;
324
335
  overflow: auto hidden;
325
336
  }
326
337
 
327
- .Ns7beW_markdown input[type="checkbox"] {
338
+ .vrxa6G_markdown input[type="checkbox"] {
328
339
  accent-color: var(--dsw-alias-label-secondary);
329
340
  margin: 0 8px 0 0;
330
341
  }
331
342
 
332
- .Ns7beW_tableScroll {
343
+ .vrxa6G_tableScroll {
333
344
  overscroll-behavior-x: contain;
334
345
  max-width: 100%;
335
346
  overflow-x: auto;
336
347
  }
337
348
 
338
- .Ns7beW_tableScroll.md-table-wide {
349
+ .vrxa6G_tableScroll.md-table-wide {
339
350
  padding-bottom: var(--dsh-scrollbar-width, 8px);
340
351
  overflow-x: hidden;
341
352
  }
342
353
 
343
- .Ns7beW_tableScroll.md-table-wide:hover, .Ns7beW_tableScroll.md-table-wide:focus-visible {
354
+ .vrxa6G_tableScroll.md-table-wide:hover, .vrxa6G_tableScroll.md-table-wide:focus-visible {
344
355
  padding-bottom: 0;
345
356
  overflow-x: auto;
346
357
  }
347
358
 
348
- .Ns7beW_tableScroll:focus-visible {
359
+ .vrxa6G_tableScroll:focus-visible {
349
360
  box-shadow: 0 0 0 2px var(--dsw-alias-state-business-primary);
350
361
  outline: none;
351
362
  }
352
363
 
353
- .Ns7beW_tableScroll table {
364
+ .vrxa6G_tableScroll table {
354
365
  border-collapse: collapse;
355
366
  width: max-content;
356
367
  max-width: max-content;
357
368
  }
358
369
 
359
- .Ns7beW_tableFill table {
370
+ .vrxa6G_tableFill table {
360
371
  width: 100%;
361
372
  max-width: none;
362
373
  }
363
374
 
364
- .Ns7beW_tableScroll th {
375
+ .vrxa6G_tableScroll th {
365
376
  text-align: start;
366
- border-bottom: 1px solid var(--dsw-alias-border-l3);
377
+ border-bottom: .5px solid var(--dsw-alias-border-l3);
367
378
  font: var(--dsw-font-markdown-table-head);
368
379
  border-top: none;
369
380
  min-width: 100px;
@@ -371,40 +382,40 @@
371
382
  padding: 10px 16px;
372
383
  }
373
384
 
374
- .Ns7beW_tableScroll td {
375
- border-bottom: 1px solid var(--dsw-alias-border-l2);
385
+ .vrxa6G_tableScroll td {
386
+ border-bottom: .5px solid var(--dsw-alias-border-l2);
376
387
  font: var(--dsw-font-markdown-table);
377
388
  min-width: 100px;
378
389
  max-width: min(30vw, 320px);
379
390
  padding: 10px 16px;
380
391
  }
381
392
 
382
- .Ns7beW_tableScroll th:first-child, .Ns7beW_tableScroll td:first-child {
393
+ .vrxa6G_tableScroll th:first-child, .vrxa6G_tableScroll td:first-child {
383
394
  padding-left: 0;
384
395
  }
385
396
 
386
- .Ns7beW_tableScroll td:last-child {
397
+ .vrxa6G_tableScroll td:last-child {
387
398
  padding-right: 0;
388
399
  }
389
400
 
390
- .Ns7beW_tableScroll table code {
401
+ .vrxa6G_tableScroll table code {
391
402
  font-size: 11px;
392
403
  }
393
404
 
394
- .Ns7beW_markdown > :first-child, .Ns7beW_markdown p:first-child {
405
+ .vrxa6G_markdown > :first-child, .vrxa6G_markdown p:first-child {
395
406
  margin-top: 0 !important;
396
407
  }
397
408
 
398
- .Ns7beW_markdown > :last-child, .Ns7beW_markdown p:last-child {
409
+ .vrxa6G_markdown > :last-child, .vrxa6G_markdown p:last-child {
399
410
  margin-bottom: 0 !important;
400
411
  }
401
412
 
402
- .Ns7beW_imageAlt {
413
+ .vrxa6G_imageAlt {
403
414
  color: var(--dsw-alias-label-tertiary);
404
415
  font-style: italic;
405
416
  }
406
417
 
407
- .Ns7beW_image {
418
+ .vrxa6G_image {
408
419
  background: var(--dsw-alias-bg-base);
409
420
  object-fit: contain;
410
421
  border-radius: 8px;
@@ -415,19 +426,20 @@
415
426
  display: block;
416
427
  }
417
428
 
418
- .Ns7beW_fileMention {
429
+ .vrxa6G_fileMention {
419
430
  font: inherit;
420
- color: var(--dsw-alias-state-business-primary);
431
+ color: var(--dsw-alias-link);
421
432
  cursor: pointer;
422
433
  background: none;
423
434
  border: none;
424
435
  margin: 0;
425
436
  padding: 0;
437
+ font-weight: 500;
426
438
  text-decoration: none;
427
439
  }
428
440
 
429
- .Ns7beW_fileMention:hover, .Ns7beW_fileMention:focus {
430
- text-decoration: underline var(--dsw-alias-state-business-primary);
441
+ .vrxa6G_fileMention:hover, .vrxa6G_fileMention:focus {
442
+ text-decoration: underline dotted var(--dsw-alias-link);
431
443
  text-underline-offset: 3px;
432
444
  outline: none;
433
445
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/lifeboat",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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.8"
34
+ "@dsh-plus/shared": "0.1.9"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@deepseek-ai/cordis": "^4.0.1",
38
- "@deepseek-ai/dsh-home-paths": "^0.1.2-alpha.2",
39
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
40
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
38
+ "@deepseek-ai/dsh-home-paths": "^0.1.3-alpha.2",
39
+ "@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
40
+ "@deepseek-ai/dsh-settings": "^0.1.3-alpha.2",
41
41
  "@deepseek-ai/schemastery": "^3.18.1"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@deepseek-ai/cordis": "4.0.2",
45
- "@deepseek-ai/dsh-home-paths": "0.1.2-alpha.2",
46
- "@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.2",
47
- "@deepseek-ai/dsh-llm": "0.1.2-alpha.2",
48
- "@deepseek-ai/dsh-settings": "0.1.2-alpha.2",
45
+ "@deepseek-ai/dsh-home-paths": "0.1.3-alpha.2",
46
+ "@deepseek-ai/dsh-host-webserver": "0.1.3-alpha.2",
47
+ "@deepseek-ai/dsh-llm": "0.1.3-alpha.2",
48
+ "@deepseek-ai/dsh-settings": "0.1.3-alpha.2",
49
49
  "@deepseek-ai/schemastery": "3.18.2",
50
50
  "@types/node": "26.4.0",
51
51
  "@types/react": "~18.3.31",
@@ -211,13 +211,25 @@ export interface FallbackDeps {
211
211
  writeState(state: FallbackStateT | null): Promise<void>
212
212
  }
213
213
 
214
- /** 装配 LLM 应急翻译:启动后及 settings/llm 变化时评估一次(并发合并)。 */
215
- export function installLlmFallback(ctx: Context, deps: FallbackDeps): void {
214
+ /** 启动宽限(ms):llm-pi 等兄弟插件的 adapter 注册晚于 lifeboat 就绪,
215
+ * 启动竞态下立即评估会把"还没加载"误判为"缺席"——每次重启都误发
216
+ * 「应急翻译 + 已还原」两条通知。宽限期内一律不评估,宽限结束自动评估
217
+ * 一次,事件订阅覆盖后续变化;真故障(llm-pi 缺席)在宽限后照常告警。 */
218
+ export const BOOT_GRACE_MS = 60_000
219
+
220
+ /** 装配 LLM 应急翻译:启动宽限结束后及 settings/llm 变化时评估一次(并发合并)。 */
221
+ export function installLlmFallback(
222
+ ctx: Context,
223
+ deps: FallbackDeps,
224
+ graceMs: number = BOOT_GRACE_MS,
225
+ ): void {
216
226
  const logger = ctx.logger('lifeboat')
217
227
  const settings = ctx.settings as SettingsProvider
218
228
  const writes = settings as unknown as SettingsWrite
219
229
  const settingsFile = dshHomePath('settings.yaml')
220
230
  let inFlight: Promise<void> | undefined
231
+ const startedAt = Date.now()
232
+ const withinGrace = (): boolean => Date.now() - startedAt < graceMs
221
233
 
222
234
  const listProviderIds = (): Set<string> => new Set(ctx.llm.listProviders().map((p) => p.id))
223
235
 
@@ -300,6 +312,7 @@ export function installLlmFallback(ctx: Context, deps: FallbackDeps): void {
300
312
  }
301
313
 
302
314
  const run = (): void => {
315
+ if (withinGrace()) return
303
316
  if (inFlight !== undefined) return
304
317
  inFlight = evaluate()
305
318
  .catch((error: unknown) => {
@@ -312,9 +325,11 @@ export function installLlmFallback(ctx: Context, deps: FallbackDeps): void {
312
325
  })
313
326
  }
314
327
 
315
- // 首次评估延迟到启动收尾:lifeboat 首位加载时兄弟插件的 adapter 尚未注册,
316
- // 立刻评估会把"还没加载"误判为"缺席"。事件订阅覆盖后续变化。
317
- const bootTimer = setTimeout(run, 10_000)
328
+ // 首次评估延迟到启动宽限结束:lifeboat 首位加载时兄弟插件的 adapter 尚未
329
+ // 注册(llm-pi 就绪可能远超 10s),提前评估会把"还没加载"误判为"缺席"
330
+ // (每次重启都误发应急翻译 + 还原通知)。宽限结束评估一次,事件订阅
331
+ // 覆盖后续变化;宽限内的 settings/llm 事件同样被时间闸跳过,由收尾评估兜底。
332
+ const bootTimer = setTimeout(run, graceMs)
318
333
  ctx.effect(() => () => clearTimeout(bootTimer), 'lifeboat: fallback boot timer')
319
334
  ctx.on('ready' as never, run)
320
335
  // 只响应与本判定相关的命名空间变化:journal 写自身命名空间,不过滤会自触发循环。