@dsh-plus/lifeboat 0.2.30 → 0.3.1
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 +61 -37
- package/lib/index.d.ts +0 -2
- package/lib/index.js +166 -242
- package/package.json +2 -2
- package/src/client/health-styles.ts +4 -2
- package/src/client/health-tab.tsx +63 -36
- package/src/config.ts +0 -14
- package/src/copy-guide.ts +153 -0
- package/src/health-api.ts +6 -11
- package/src/index.ts +22 -23
- package/src/official-copy-status.ts +100 -0
- package/src/state-file.ts +8 -26
- package/src/fallback-llm.ts +0 -355
package/src/state-file.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* journal
|
|
2
|
+
* journal 的文件持久化($DSH_HOME/dsh-plus/lifeboat/state.json)。
|
|
3
3
|
*
|
|
4
|
-
* 存储规范:运行期状态属非配置数据,不进 settings
|
|
4
|
+
* 存储规范:运行期状态属非配置数据,不进 settings 用户层;本模块自带实现,
|
|
5
5
|
* 维持零 dsh-plus 内部依赖铁律(不 import @dsh-plus/shared)。
|
|
6
6
|
* schema 刻意宽松:lifeboat 是最后防线,自身数据问题绝不能让它起不来。
|
|
7
7
|
* @module lifeboat/state-file
|
|
@@ -10,15 +10,14 @@
|
|
|
10
10
|
import { readFile, stat } from 'node:fs/promises'
|
|
11
11
|
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
|
|
12
12
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
|
13
|
-
import type {
|
|
13
|
+
import type { JournalEntryT } from './config.ts'
|
|
14
14
|
|
|
15
15
|
/** 状态文件落点:$DSH_HOME/dsh-plus/lifeboat/state.json。 */
|
|
16
16
|
export const STATE_FILE = dshHomePath('dsh-plus', 'lifeboat', 'state.json')
|
|
17
17
|
|
|
18
|
-
/**
|
|
18
|
+
/** 磁盘文档形状。 */
|
|
19
19
|
export interface StateDoc {
|
|
20
20
|
journal: JournalEntryT[]
|
|
21
|
-
llmFallback: FallbackStateT | null
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
/** journal 上限:只留最近 50 条,防长期运行膨胀。 */
|
|
@@ -35,41 +34,24 @@ function asEntry(value: unknown): JournalEntryT | null {
|
|
|
35
34
|
return { at: record['at'], kind: record['kind'], detail: String(record['detail'] ?? '') }
|
|
36
35
|
}
|
|
37
36
|
|
|
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
37
|
/** 未知输入收窄为完整状态文档;任何缺省回落到空文档。 */
|
|
56
38
|
export function normalizeState(raw: unknown): StateDoc {
|
|
57
|
-
if (typeof raw !== 'object' || raw === null) return { journal: []
|
|
39
|
+
if (typeof raw !== 'object' || raw === null) return { journal: [] }
|
|
58
40
|
const record = raw as Record<string, unknown>
|
|
59
41
|
const journal = Array.isArray(record['journal'])
|
|
60
42
|
? record['journal'].map(asEntry).filter((entry): entry is JournalEntryT => entry !== null)
|
|
61
43
|
: []
|
|
62
|
-
return { journal: journal.slice(-JOURNAL_CAP)
|
|
44
|
+
return { journal: journal.slice(-JOURNAL_CAP) }
|
|
63
45
|
}
|
|
64
46
|
|
|
65
47
|
/** 读取状态文件;缺失/损坏/超限时返回空文档(最后防线不得因自身数据起不来)。 */
|
|
66
48
|
export async function loadState(): Promise<StateDoc> {
|
|
67
49
|
try {
|
|
68
50
|
const info = await stat(STATE_FILE)
|
|
69
|
-
if (!info.isFile() || info.size > MAX_BYTES) return { journal: []
|
|
51
|
+
if (!info.isFile() || info.size > MAX_BYTES) return { journal: [] }
|
|
70
52
|
return normalizeState(JSON.parse(await readFile(STATE_FILE, 'utf-8')))
|
|
71
53
|
} catch {
|
|
72
|
-
return { journal: []
|
|
54
|
+
return { journal: [] }
|
|
73
55
|
}
|
|
74
56
|
}
|
|
75
57
|
|
package/src/fallback-llm.ts
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LLM 应急翻译:默认模型路由的 provider 无已注册 adapter 时(典型场景:llm-pi
|
|
3
|
-
* 被隔离/加载失败),把 settings.yaml 里 dsh-plus-llm-pi 的 provider 配置翻译为
|
|
4
|
-
* 官方 llm-pi-ai 命名空间的原生格式(键加 -fb 后缀避开 DUPLICATE_ADAPTER 冲突),
|
|
5
|
-
* 默认模型指到翻译路由;源 provider 恢复健康后按 journal 自动还原。
|
|
6
|
-
*
|
|
7
|
-
* 为什么读原始 settings.yaml 而不是 settings 服务:插件被禁用后其命名空间随之
|
|
8
|
-
* 注销,服务层读不到——但数据仍在文件里(只读,不修改)。
|
|
9
|
-
* 降级语义:翻译只覆盖官方物化的字段子集,dsh-plus 扩展的全量 compat 丢弃;
|
|
10
|
-
* 协议推断是启发式(显式 api > 路由名 > 模型继承源 > openai-completions 兜底)。
|
|
11
|
-
* @module lifeboat/fallback-llm
|
|
12
|
-
*/
|
|
13
|
-
import { readFile } from 'node:fs/promises'
|
|
14
|
-
|
|
15
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
16
|
-
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
|
17
|
-
import type {} from '@deepseek-ai/dsh-llm'
|
|
18
|
-
import { SettingsConflictError } from '@deepseek-ai/dsh-settings'
|
|
19
|
-
import { parseDocument } from 'yaml'
|
|
20
|
-
|
|
21
|
-
import type { FallbackStateT } from './config.ts'
|
|
22
|
-
import type { Alerter } from './notify.ts'
|
|
23
|
-
|
|
24
|
-
// 0.1.2-alpha.2 起 settings 命名空间直接以字面量使用(编译期校验语法),
|
|
25
|
-
// 不再需要 settingsNamespace() 品牌化。
|
|
26
|
-
const NS_PI_AI = 'llm-pi-ai'
|
|
27
|
-
const NS_AGENT_DEFAULT = 'agent-default-model'
|
|
28
|
-
const RAW_NS_LLM_PI = 'dsh-plus-llm-pi'
|
|
29
|
-
|
|
30
|
-
/** 翻译路由键后缀:与源路由错开,避免 llm-pi 恢复瞬间撞 DUPLICATE_ADAPTER。 */
|
|
31
|
-
export const FALLBACK_SUFFIX = '-fb'
|
|
32
|
-
|
|
33
|
-
/** 官方配置路径物化的 compat 子集(其余字段降级期丢弃)。 */
|
|
34
|
-
const COMPAT_WHITELIST = ['thinkingFormat', 'supportsReasoningEffort'] as const
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* master 官方 llm-pi-ai 的 COMPAT_GATES(llm-pi-ai/src/catalog.ts):每个 compat
|
|
38
|
-
* 字段按协议门控,写时经 assertOfferedCompatFields 拒绝不适配字段——thinkingFormat /
|
|
39
|
-
* supportsReasoningEffort 仅 openai-completions 提供。翻译按推断 api 过滤,避免
|
|
40
|
-
* 整段 compat 写入被官方校验拒绝。
|
|
41
|
-
*/
|
|
42
|
-
const COMPAT_OFFER_APIS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
43
|
-
thinkingFormat: new Set(['openai-completions']),
|
|
44
|
-
supportsReasoningEffort: new Set(['openai-completions']),
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** 官方 reasoning 词表(ModelThinkingLevel 的保守子集),不在其中的值丢弃。 */
|
|
48
|
-
const REASONING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'])
|
|
49
|
-
|
|
50
|
-
/** dsh-plus-llm-pi provider 配置的松散视图(原始文件读取,未经 schema 解析)。 */
|
|
51
|
-
export interface RawProvider {
|
|
52
|
-
displayName?: string
|
|
53
|
-
apiKeyEnv?: string
|
|
54
|
-
baseURL?: string
|
|
55
|
-
api?: string
|
|
56
|
-
headers?: Record<string, string>
|
|
57
|
-
reasoning?: string
|
|
58
|
-
thinkingBudgets?: Record<string, unknown>
|
|
59
|
-
defaultInput?: string[]
|
|
60
|
-
compat?: Record<string, unknown>
|
|
61
|
-
models?: RawModel[]
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface RawModel {
|
|
65
|
-
id: string
|
|
66
|
-
name?: string
|
|
67
|
-
extends?: string
|
|
68
|
-
contextWindow?: number
|
|
69
|
-
maxTokens?: number
|
|
70
|
-
input?: string[]
|
|
71
|
-
reasoningEfforts?: Record<string, string>
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** 官方 llm-pi-ai provider 档案的翻译目标形状(字段子集)。 */
|
|
75
|
-
export interface TranslatedProvider {
|
|
76
|
-
displayName?: string
|
|
77
|
-
apiKeyEnv?: string
|
|
78
|
-
baseURL?: string
|
|
79
|
-
api: string
|
|
80
|
-
headers?: Record<string, string>
|
|
81
|
-
reasoning?: string
|
|
82
|
-
thinkingBudgets?: Record<string, unknown>
|
|
83
|
-
defaultInput?: string[]
|
|
84
|
-
compat?: Record<string, unknown>
|
|
85
|
-
models?: Record<string, unknown>[]
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** 模型继承源(extends 的 provider 前缀)→ 协议的已知映射。 */
|
|
89
|
-
const EXTENDS_PROTOCOL: Record<string, string> = {
|
|
90
|
-
anthropic: 'anthropic-messages',
|
|
91
|
-
'kimi-coding': 'anthropic-messages',
|
|
92
|
-
deepseek: 'openai-completions',
|
|
93
|
-
openai: 'openai-completions',
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** 路由名里的协议线索。 */
|
|
97
|
-
const ROUTE_PROTOCOL: Record<string, string> = {
|
|
98
|
-
chat: 'openai-completions',
|
|
99
|
-
completions: 'openai-completions',
|
|
100
|
-
response: 'openai-responses',
|
|
101
|
-
responses: 'openai-responses',
|
|
102
|
-
anthropic: 'anthropic-messages',
|
|
103
|
-
claude: 'anthropic-messages',
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* 推断路由协议:显式 api > 路由名线索 > 首个模型的继承源 > openai-completions。
|
|
108
|
-
* 兜底选 openai-completions 因为多数网关提供 OpenAI 兼容面;推错时告警里注明。
|
|
109
|
-
*/
|
|
110
|
-
export function inferApi(routeKey: string, provider: RawProvider): string {
|
|
111
|
-
if (typeof provider.api === 'string' && provider.api.length > 0) return provider.api
|
|
112
|
-
const routeHint = ROUTE_PROTOCOL[routeKey.toLowerCase()]
|
|
113
|
-
if (routeHint !== undefined) return routeHint
|
|
114
|
-
const firstExtends = provider.models?.find((m) => typeof m.extends === 'string')?.extends
|
|
115
|
-
const source = firstExtends?.split('/')[0]
|
|
116
|
-
if (source !== undefined && EXTENDS_PROTOCOL[source] !== undefined)
|
|
117
|
-
return EXTENDS_PROTOCOL[source]
|
|
118
|
-
return 'openai-completions'
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** 单个 provider 翻译(纯函数)。 */
|
|
122
|
-
export function translateProvider(routeKey: string, raw: RawProvider): TranslatedProvider {
|
|
123
|
-
const out: TranslatedProvider = {
|
|
124
|
-
displayName: `${raw.displayName ?? routeKey} [fallback]`,
|
|
125
|
-
api: inferApi(routeKey, raw),
|
|
126
|
-
}
|
|
127
|
-
if (raw.apiKeyEnv !== undefined) out.apiKeyEnv = raw.apiKeyEnv
|
|
128
|
-
if (raw.baseURL !== undefined) out.baseURL = raw.baseURL
|
|
129
|
-
if (raw.headers !== undefined) out.headers = raw.headers
|
|
130
|
-
if (raw.reasoning !== undefined && REASONING_LEVELS.has(raw.reasoning))
|
|
131
|
-
out.reasoning = raw.reasoning
|
|
132
|
-
if (raw.thinkingBudgets !== undefined) out.thinkingBudgets = raw.thinkingBudgets
|
|
133
|
-
if (Array.isArray(raw.defaultInput) && raw.defaultInput.length > 0)
|
|
134
|
-
out.defaultInput = raw.defaultInput
|
|
135
|
-
if (raw.compat !== undefined) {
|
|
136
|
-
const api = inferApi(routeKey, raw)
|
|
137
|
-
const compat: Record<string, unknown> = {}
|
|
138
|
-
for (const key of COMPAT_WHITELIST) {
|
|
139
|
-
const value = raw.compat[key]
|
|
140
|
-
// 空值同样丢弃:官方写时拒绝 valueless 键(catalog.ts assertOfferedCompatFields)
|
|
141
|
-
if (value === undefined || value === null) continue
|
|
142
|
-
// 按推断 api 门控:仅写入该协议 offer 的字段,其余丢弃(官方写时拒绝不适配字段)
|
|
143
|
-
if (!COMPAT_OFFER_APIS[key]?.has(api)) continue
|
|
144
|
-
compat[key] = value
|
|
145
|
-
}
|
|
146
|
-
if (Object.keys(compat).length > 0) out.compat = compat
|
|
147
|
-
}
|
|
148
|
-
if (Array.isArray(raw.models) && raw.models.length > 0) {
|
|
149
|
-
out.models = raw.models.map((m) => {
|
|
150
|
-
const model: Record<string, unknown> = { id: m.id }
|
|
151
|
-
if (m.name !== undefined) model.name = m.name
|
|
152
|
-
if (m.contextWindow !== undefined) model.contextWindow = m.contextWindow
|
|
153
|
-
if (m.maxTokens !== undefined) model.maxTokens = m.maxTokens
|
|
154
|
-
if (Array.isArray(m.input) && m.input.length > 0) model.input = m.input
|
|
155
|
-
return model
|
|
156
|
-
})
|
|
157
|
-
}
|
|
158
|
-
return out
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** 整表翻译:键加 -fb 后缀(纯函数)。 */
|
|
162
|
-
export function translateProviders(
|
|
163
|
-
providers: Record<string, RawProvider>,
|
|
164
|
-
): Record<string, TranslatedProvider> {
|
|
165
|
-
const out: Record<string, TranslatedProvider> = {}
|
|
166
|
-
for (const [key, raw] of Object.entries(providers)) {
|
|
167
|
-
out[`${key}${FALLBACK_SUFFIX}`] = translateProvider(key, raw)
|
|
168
|
-
}
|
|
169
|
-
return out
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** 读 settings.yaml 原始文档的指定段落(只读;文件/段落缺失返回 undefined)。 */
|
|
173
|
-
export async function readRawSection(
|
|
174
|
-
settingsFile: string,
|
|
175
|
-
ns: string,
|
|
176
|
-
): Promise<Record<string, unknown> | undefined> {
|
|
177
|
-
const text = await readFile(settingsFile, 'utf-8')
|
|
178
|
-
const doc = parseDocument(text).toJS() as Record<string, unknown>
|
|
179
|
-
const section = doc[ns]
|
|
180
|
-
if (typeof section !== 'object' || section === null) return undefined
|
|
181
|
-
return section as Record<string, unknown>
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
interface SettingsWrite {
|
|
185
|
-
update(ns: string, patch: object): Promise<void>
|
|
186
|
-
mutate(
|
|
187
|
-
ns: string,
|
|
188
|
-
ops: readonly {
|
|
189
|
-
op: 'set' | 'unset'
|
|
190
|
-
path: readonly string[]
|
|
191
|
-
value?: unknown
|
|
192
|
-
}[],
|
|
193
|
-
): Promise<void>
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** 写 settings 带一次冲突重试(用户同时改配置时不抢,放弃并告警)。 */
|
|
197
|
-
async function writeWithRetry(write: () => Promise<void>): Promise<void> {
|
|
198
|
-
try {
|
|
199
|
-
await write()
|
|
200
|
-
} catch (error) {
|
|
201
|
-
if (!(error instanceof SettingsConflictError)) throw error
|
|
202
|
-
await write()
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export interface FallbackDeps {
|
|
207
|
-
journal: (kind: string, detail: string) => void
|
|
208
|
-
alert: Alerter
|
|
209
|
-
/** 持久化翻译状态(journal 命名空间内)。 */
|
|
210
|
-
readState(): FallbackStateT | null
|
|
211
|
-
writeState(state: FallbackStateT | null): Promise<void>
|
|
212
|
-
}
|
|
213
|
-
|
|
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 {
|
|
226
|
-
const logger = ctx.logger('lifeboat')
|
|
227
|
-
const settings = ctx.settings
|
|
228
|
-
const writes = settings as unknown as SettingsWrite
|
|
229
|
-
const settingsFile = dshHomePath('settings.yaml')
|
|
230
|
-
let inFlight: Promise<void> | undefined
|
|
231
|
-
const startedAt = Date.now()
|
|
232
|
-
const withinGrace = (): boolean => Date.now() - startedAt < graceMs
|
|
233
|
-
|
|
234
|
-
const listProviderIds = (): Set<string> => new Set(ctx.llm.listProviders().map((p) => p.id))
|
|
235
|
-
|
|
236
|
-
async function activate(provider: string, model: string): Promise<void> {
|
|
237
|
-
const section = await readRawSection(settingsFile, RAW_NS_LLM_PI).catch(() => undefined)
|
|
238
|
-
const providers = section?.providers as Record<string, RawProvider> | undefined
|
|
239
|
-
if (providers === undefined || providers[provider] === undefined) {
|
|
240
|
-
deps.alert(
|
|
241
|
-
'[DSH] 默认模型不可用且无配置可翻译',
|
|
242
|
-
`默认模型的 provider "${provider}" 没有已注册 adapter,且 ${RAW_NS_LLM_PI} 配置中找不到对应条目,无法应急翻译。请手动修复。`,
|
|
243
|
-
)
|
|
244
|
-
return
|
|
245
|
-
}
|
|
246
|
-
const translated = translateProviders(providers)
|
|
247
|
-
const fbProvider = `${provider}${FALLBACK_SUFFIX}`
|
|
248
|
-
try {
|
|
249
|
-
// 0.1.7 起写入校验经 internal/config waterfall(官方 llm-pi-ai 同款,
|
|
250
|
-
// 替代 0.1.6 installSection validate),落盘前拒绝不适配的翻译结果;
|
|
251
|
-
// update 无 expectedRevision 本就无条件写,不做冲突重试——失败一律分类为「翻译失败」。
|
|
252
|
-
await writes.update(NS_PI_AI, { providers: translated })
|
|
253
|
-
await writes.update(NS_AGENT_DEFAULT, { provider: fbProvider, model })
|
|
254
|
-
} catch (error) {
|
|
255
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
256
|
-
throw new Error(`翻译失败(官方 llm-pi-ai 拒绝写入翻译后的配置): ${message}`)
|
|
257
|
-
}
|
|
258
|
-
await deps.writeState({
|
|
259
|
-
active: true,
|
|
260
|
-
originalProvider: provider,
|
|
261
|
-
originalModel: model,
|
|
262
|
-
fallbackProvider: fbProvider,
|
|
263
|
-
providers: Object.keys(translated),
|
|
264
|
-
at: new Date().toISOString(),
|
|
265
|
-
})
|
|
266
|
-
deps.journal(
|
|
267
|
-
'llm-fallback',
|
|
268
|
-
`默认模型切换到 ${fbProvider}/${model}(翻译 ${Object.keys(translated).length} 个 provider)`,
|
|
269
|
-
)
|
|
270
|
-
deps.alert(
|
|
271
|
-
'[DSH] LLM 配置已应急翻译',
|
|
272
|
-
`默认模型的 provider "${provider}" 不可用(llm-pi 缺席?),已把 ${RAW_NS_LLM_PI} 配置翻译为官方 llm-pi-ai 路由(-fb 后缀)并切换默认模型为 ${fbProvider}/${model}。源插件恢复后将自动还原。`,
|
|
273
|
-
)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async function revert(state: FallbackStateT): Promise<void> {
|
|
277
|
-
const ops = state.providers.map((key) => ({
|
|
278
|
-
op: 'unset' as const,
|
|
279
|
-
path: ['providers', key],
|
|
280
|
-
}))
|
|
281
|
-
await writeWithRetry(() => writes.mutate(NS_PI_AI, ops))
|
|
282
|
-
await writeWithRetry(() =>
|
|
283
|
-
writes.update(NS_AGENT_DEFAULT, {
|
|
284
|
-
provider: state.originalProvider,
|
|
285
|
-
model: state.originalModel,
|
|
286
|
-
}),
|
|
287
|
-
)
|
|
288
|
-
await deps.writeState(null)
|
|
289
|
-
deps.journal(
|
|
290
|
-
'llm-fallback-revert',
|
|
291
|
-
`默认模型还原为 ${state.originalProvider}/${state.originalModel}`,
|
|
292
|
-
)
|
|
293
|
-
deps.alert(
|
|
294
|
-
'[DSH] LLM 应急翻译已还原',
|
|
295
|
-
`provider "${state.originalProvider}" 恢复健康,默认模型已还原,临时翻译路由已清除。`,
|
|
296
|
-
)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async function evaluate(): Promise<void> {
|
|
300
|
-
const state = deps.readState()
|
|
301
|
-
const ids = listProviderIds()
|
|
302
|
-
if (state?.active) {
|
|
303
|
-
if (ids.has(state.originalProvider)) await revert(state)
|
|
304
|
-
return
|
|
305
|
-
}
|
|
306
|
-
// 0.1.7 无 settings.get:describe() 直读本条目平面值(volatile 解包后视图)。
|
|
307
|
-
const def = ctx.settings.describe().find((row) => row.ns === NS_AGENT_DEFAULT)?.value as
|
|
308
|
-
| { provider?: string; model?: string }
|
|
309
|
-
| undefined
|
|
310
|
-
if (def?.provider === undefined || def.model === undefined) return
|
|
311
|
-
if (ids.has(def.provider)) return
|
|
312
|
-
await activate(def.provider, def.model)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const run = (): void => {
|
|
316
|
-
if (withinGrace()) return
|
|
317
|
-
if (inFlight !== undefined) return
|
|
318
|
-
inFlight = evaluate()
|
|
319
|
-
.catch((error: unknown) => {
|
|
320
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
321
|
-
logger.warn(`llm fallback 评估失败: ${message}`)
|
|
322
|
-
deps.journal('llm-fallback-error', message)
|
|
323
|
-
})
|
|
324
|
-
.finally(() => {
|
|
325
|
-
inFlight = undefined
|
|
326
|
-
})
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 首次评估延迟到启动宽限结束:lifeboat 首位加载时兄弟插件的 adapter 尚未
|
|
330
|
-
// 注册(llm-pi 就绪可能远超 10s),提前评估会把"还没加载"误判为"缺席"
|
|
331
|
-
// (每次重启都误发应急翻译 + 还原通知)。宽限结束评估一次,事件订阅
|
|
332
|
-
// 覆盖后续变化;宽限内的 settings/llm 事件同样被时间闸跳过,由收尾评估兜底。
|
|
333
|
-
const bootTimer = setTimeout(run, graceMs)
|
|
334
|
-
ctx.effect(() => () => clearTimeout(bootTimer), 'lifeboat: fallback boot timer')
|
|
335
|
-
// 走 events 的字符串重载而非 ctx.on:'ready' 不是平台声明的事件
|
|
336
|
-
// (cordis 4.0.2 内置仅 internal/*,dsh 0.1.5-rc.1 全树无任何声明或派发点),
|
|
337
|
-
// ctx.on 要求 keyof Events,只能靠 as never 硬塞。events.on(name: string|symbol)
|
|
338
|
-
// 是平台给动态事件名的正统入口,运行期同一事件总线、同一 fiber 归属、随 fiber
|
|
339
|
-
// 卸载自动移除(已实测)。注意:该监听目前**无生产者**,属遗留死监听,保留是
|
|
340
|
-
// 为不动运行期行为,待人工确认后决定是否删除。
|
|
341
|
-
ctx.events.on('ready', run)
|
|
342
|
-
// 只响应与本判定相关的命名空间变化:journal 写自身命名空间,不过滤会自触发循环。
|
|
343
|
-
const WATCHED_NS = new Set(['agent-default-model', 'llm-pi-ai', RAW_NS_LLM_PI])
|
|
344
|
-
// settings/document-updated 由 @deepseek-ai/dsh-settings 声明(本文件已导入
|
|
345
|
-
// 该包),0.1.7 起载荷为 (ns, revision);监听器按 unknown 收,保留原有
|
|
346
|
-
// typeof 字符串防御。watched 过滤避免 journal 自写自触发循环。
|
|
347
|
-
ctx.root.on('settings/document-updated', (ns: unknown) => {
|
|
348
|
-
if (typeof ns === 'string' && WATCHED_NS.has(ns)) run()
|
|
349
|
-
})
|
|
350
|
-
// 事件名已核对:0.1.2-alpha.2 的 dsh-llm 保留 'llm/adapters-updated'
|
|
351
|
-
// (packages/llm/llm/src/types.ts:23 module augmentation,index.ts:344 于每次
|
|
352
|
-
// adapter 注册/注销提交点 dispatch);@deepseek-ai/dsh-llm 的类型导入已携带该
|
|
353
|
-
// 声明,无需 as never。
|
|
354
|
-
ctx.root.on('llm/adapters-updated', run)
|
|
355
|
-
}
|