@dsh-plus/llm-pi 0.1.42 → 0.1.43

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
@@ -1,14 +1,11 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
3
3
  import { deepEqualJson } from "@deepseek-ai/dsh-util-values";
4
- import { hasVolatileRefs, pluginDataPath, unwrapVolatile } from "@dsh-plus/shared";
5
- import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
6
- import { request } from "node:http";
7
- import { request as request$1 } from "node:https";
4
+ import { hasVolatileRefs, unwrapVolatile } from "@dsh-plus/shared";
5
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
8
6
  import { dirname, join, resolve } from "node:path";
9
- import { HttpsProxyAgent } from "https-proxy-agent";
10
- import { credentialKeyId, credentialKeyScope, credentialRef, isCredentialKeySegment, isCredentialRefName } from "@deepseek-ai/dsh-credentials";
11
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
+ import { credentialKeyId, credentialKeyScope, credentialRef, isCredentialKeySegment, isCredentialRefName } from "@deepseek-ai/dsh-credentials";
12
9
  import * as vendoredAnonId from "@deepseek-ai/dsh-anonymous-user-id";
13
10
  import * as vendoredLlm from "@deepseek-ai/dsh-llm";
14
11
  import { LlmError } from "@deepseek-ai/dsh-llm";
@@ -30,13 +27,20 @@ import { homedir } from "node:os";
30
27
  *
31
28
  * 与官方 llm-pi-ai 的差异:
32
29
  * - compat 是开放 dict,物化时按协议校验(见 compat.ts),写入即拒绝未知键;
33
- * - provider/model 均支持 extends 继承(见 inherit.ts)。
30
+ * - provider/model 均支持 extends 继承(见 inherit.ts),继承源只有 pi-ai 内置目录;
31
+ * - `api` 不在 schema 里钉死枚举:合法协议集合由已安装官方适配器现场推导
32
+ * (见 official-surface/resolve-dsh),写时由 profiles.ts 以明确错误拒绝不支持者,
33
+ * 官方将来新增协议无需改本插件。
34
34
  * @module llm-pi/config
35
35
  */
36
36
  /** settings 命名空间(字面量即合法命名空间,0.1.2-alpha.2 起编译期校验;webui 配置卡片与插件运行期读取同一份)。 */
37
37
  const SETTINGS_NS = "dsh-plus-llm-pi";
38
- /** 本插件可为手写 route 提供的协议实现(与官方 PROTOCOLS 表一致)。 */
39
- const PROTOCOL_IDS = [
38
+ /**
39
+ * 官方适配器不可用/推导失败时的协议兜底三元组(自 0.1.2-alpha 线以来的稳定集)。
40
+ * 运行期生效集合来自已安装官方包的 `supportedProtocols()`(见 resolve-dsh.ts),
41
+ * 本常量只用于 schema 描述、浏览器半首帧兜底与推导失败回退。
42
+ */
43
+ const FALLBACK_PROTOCOLS = [
40
44
  "openai-completions",
41
45
  "openai-responses",
42
46
  "anthropic-messages"
@@ -101,10 +105,10 @@ const modelEntry = z.object({
101
105
  imageDetail: z.union(DEEPSEEK_IMAGE_DETAILS).description("(已移除)图片细节档位;0.1.2-alpha.1 起请改用 imagePixelBudget/imageMaxBytes")
102
106
  });
103
107
  const providerProfile = z.object({
104
- adapter: z.union(ADAPTER_KINDS).description("路由适配器:pi=PiAiAdapter(默认,三协议);deepseek=官方 DeepSeekAdapter(图片走 Files API 文件通道,失败自动降级 base64)").default("pi"),
105
- extends: z.string().description("provider 级继承:内置 provider id,提供 api/baseURL 默认值与模型 extends 的缺省查找源"),
108
+ adapter: z.union(ADAPTER_KINDS).description("路由适配器:pi=PiAiAdapter(默认,协议集合随已装官方适配器推导);deepseek=官方 DeepSeekAdapter(图片走 Files API 文件通道,失败自动降级 base64)").default("pi"),
109
+ extends: z.string().description("provider 级继承:pi-ai 内置 provider id,提供 api/baseURL 默认值与模型 extends 的缺省查找源"),
106
110
  displayName: z.string().description("选择器显示名;缺省为 route 键"),
107
- api: z.union(PROTOCOL_IDS).description("线协议;缺省逐模型取继承值的 api,全部一致时作为 route 协议"),
111
+ api: z.string().description("线协议(合法集合由已装官方适配器运行期推导,见配置页「内置模型目录」状态行);缺省逐模型取继承值的 api,全部一致时作为 route 协议"),
108
112
  baseURL: z.string().description("端点;缺省继承 extends 源 provider 的端点"),
109
113
  apiKeyEnv: z.string().role("credential-ref").description("凭据引用名(凭据服务/环境变量)"),
110
114
  headers: z.dict(z.string()).description("provider 请求头(Harness 署名头保留名优先)"),
@@ -147,13 +151,85 @@ const providerProfile = z.object({
147
151
  });
148
152
  const Config = z.object({
149
153
  enabled: z.boolean().description("总开关(关闭则不注册任何 route)").default(true).volatile(),
150
- catalogUrl: z.string().description("models.dev 目录数据端点").default("https://models.dev/api.json").volatile(),
151
- catalogRefreshHours: z.number().description("models.dev 自动拉取间隔小时数;0 = 不自动拉取(可手动拉取或读已有缓存)").default(0).volatile(),
152
- catalogProxy: z.string().description("拉取 models.dev 目录时的 HTTP 代理地址(如 http://127.0.0.1:7890);留空直连").default("").volatile(),
153
154
  providers: z.dict(providerProfile).description("provider 路由表,键即 route 名").default({}).volatile()
154
155
  });
155
156
  //#endregion
156
157
  //#region src/catalog/builtin.ts
158
+ /** 已在 facts 显式取出的键:这些不再进 rest。 */
159
+ const FACT_KEYS = /* @__PURE__ */ new Set([
160
+ "id",
161
+ "provider",
162
+ "name",
163
+ "api",
164
+ "baseUrl",
165
+ "reasoning",
166
+ "input",
167
+ "contextWindow",
168
+ "maxTokens",
169
+ "thinkingLevelMap",
170
+ "compat"
171
+ ]);
172
+ /** JSON 值浅拷贝(对象/数组一层;条目要经 settings 序列化,不可留上游引用)。 */
173
+ function cloneJsonValue(value) {
174
+ if (Array.isArray(value)) return [...value];
175
+ if (typeof value === "object" && value !== null) return { ...value };
176
+ return value;
177
+ }
178
+ function asRecord(value) {
179
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
180
+ }
181
+ /** 读模态数组:只保留 text/image 两种已知值,其余丢弃(防上游改成别的枚举)。 */
182
+ function readInput(value) {
183
+ if (!Array.isArray(value)) return ["text"];
184
+ const out = value.filter((m) => m === "text" || m === "image");
185
+ return out.length > 0 ? out : ["text"];
186
+ }
187
+ /** 读 thinkingLevelMap:值为字符串或 null,其余(含 undefined 键)丢弃。 */
188
+ function readThinkingLevelMap(value) {
189
+ const record = asRecord(value);
190
+ if (record === void 0) return void 0;
191
+ const out = {};
192
+ for (const [level, wire] of Object.entries(record)) if (typeof wire === "string") out[level] = wire;
193
+ else if (wire === null) out[level] = null;
194
+ return Object.keys(out).length > 0 ? out : void 0;
195
+ }
196
+ /**
197
+ * 把运行期模型对象读成事实快照。入参用 unknown:pi-ai Model 的类型随版本变化,
198
+ * 继承/浏览都只依赖此处确认过的字段,未确认的一律进 rest 原样展示。
199
+ */
200
+ function builtinModelFacts(provider, raw) {
201
+ const model = asRecord(raw);
202
+ if (model === void 0) return void 0;
203
+ const id = model["id"];
204
+ if (typeof id !== "string" || id.length === 0) return void 0;
205
+ const rest = {};
206
+ for (const [key, value] of Object.entries(model)) {
207
+ if (FACT_KEYS.has(key) || value === void 0) continue;
208
+ rest[key] = cloneJsonValue(value);
209
+ }
210
+ const name = model["name"];
211
+ const api = model["api"];
212
+ const baseUrl = model["baseUrl"];
213
+ const contextWindow = model["contextWindow"];
214
+ const maxTokens = model["maxTokens"];
215
+ const thinkingLevelMap = readThinkingLevelMap(model["thinkingLevelMap"]);
216
+ const compat = asRecord(model["compat"]);
217
+ return {
218
+ provider,
219
+ id,
220
+ path: `${provider}/${id}`,
221
+ name: typeof name === "string" && name.length > 0 ? name : id,
222
+ api: typeof api === "string" ? api : "",
223
+ baseUrl: typeof baseUrl === "string" ? baseUrl : "",
224
+ reasoning: model["reasoning"] === true,
225
+ input: readInput(model["input"]),
226
+ contextWindow: typeof contextWindow === "number" ? contextWindow : 0,
227
+ maxTokens: typeof maxTokens === "number" ? maxTokens : 0,
228
+ ...thinkingLevelMap === void 0 ? {} : { thinkingLevelMap },
229
+ ...compat === void 0 ? {} : { compat: { ...compat } },
230
+ rest
231
+ };
232
+ }
157
233
  /** 内置 provider 的端点(provider 级 extends 的 baseURL 缺省值)。 */
158
234
  function builtinProviderBaseUrl(kit, provider) {
159
235
  return kit.builtinProviders().find((p) => p.id === provider)?.baseUrl;
@@ -165,43 +241,289 @@ function builtinProviderBaseUrl(kit, provider) {
165
241
  function hasBuiltinProvider(kit, provider) {
166
242
  return kit.getBuiltinProviders().includes(provider);
167
243
  }
168
- /** 内置 provider 的全部模型 id(UI extends 选择器用)。 */
244
+ /** 内置 provider 的全部模型 id(UI 选择器/浏览用)。 */
169
245
  function builtinModelIds(kit, provider) {
170
246
  if (!hasBuiltinProvider(kit, provider)) return [];
171
247
  return kit.getBuiltinModels(provider).map((m) => m.id);
172
248
  }
173
- /** 查单个内置模型为继承 base;未命中返回 undefined。 */
174
- function builtinModelBase(kit, provider, modelId) {
249
+ /** 内置目录的全部 provider 条目(含 pi-ai 给的显示名与端点)。 */
250
+ function builtinProviderEntries(kit) {
251
+ const catalog = kit.builtinProviders();
252
+ const byId = new Map(catalog.map((p) => [p.id, p]));
253
+ return kit.getBuiltinProviders().map((id) => {
254
+ const provider = byId.get(id);
255
+ const baseUrl = provider?.baseUrl;
256
+ return {
257
+ id,
258
+ name: provider?.name ?? id,
259
+ ...typeof baseUrl === "string" && baseUrl.length > 0 ? { baseUrl } : {},
260
+ modelCount: kit.getBuiltinModels(id).length
261
+ };
262
+ });
263
+ }
264
+ /** 取某 provider 下某模型的事实快照;provider/模型不存在返回 undefined。 */
265
+ function builtinModelFactsOf(kit, provider, modelId) {
175
266
  if (!hasBuiltinProvider(kit, provider)) return void 0;
176
267
  const model = kit.getBuiltinModels(provider).find((m) => m.id === modelId);
177
- if (model === void 0) return void 0;
268
+ return model === void 0 ? void 0 : builtinModelFacts(provider, model);
269
+ }
270
+ /** 事实快照 → 继承 base(`extra` 只包含官方 schema 接受的其余模型级键)。 */
271
+ function modelBaseOf(kit, facts) {
272
+ const extra = {};
273
+ for (const field of kit.officialModelFields) {
274
+ if (FACT_KEYS.has(field) || field === "reasoningEfforts") continue;
275
+ const value = facts.rest[field];
276
+ if (value !== void 0) extra[field] = cloneJsonValue(value);
277
+ }
178
278
  return {
179
- name: model.name,
180
- api: model.api,
181
- baseUrl: model.baseUrl,
182
- input: [...model.input],
183
- reasoning: model.reasoning,
184
- ...model.thinkingLevelMap === void 0 ? {} : { thinkingLevelMap: { ...model.thinkingLevelMap } },
185
- ...model.compat === void 0 ? {} : { compat: { ...model.compat } },
186
- contextWindow: model.contextWindow,
187
- maxTokens: model.maxTokens,
188
- cost: { ...model.cost },
189
- ...model.headers === void 0 ? {} : { headers: { ...model.headers } }
279
+ name: facts.name,
280
+ api: facts.api,
281
+ baseUrl: facts.baseUrl,
282
+ input: [...facts.input],
283
+ reasoning: facts.reasoning,
284
+ ...facts.thinkingLevelMap === void 0 ? {} : { thinkingLevelMap: { ...facts.thinkingLevelMap } },
285
+ ...facts.compat === void 0 ? {} : { compat: { ...facts.compat } },
286
+ contextWindow: facts.contextWindow,
287
+ maxTokens: facts.maxTokens,
288
+ ...Object.keys(extra).length === 0 ? {} : { extra }
190
289
  };
191
290
  }
291
+ /** 查单个内置模型为继承 base;未命中返回 undefined。 */
292
+ function builtinModelBase(kit, provider, modelId) {
293
+ const facts = builtinModelFactsOf(kit, provider, modelId);
294
+ return facts === void 0 ? void 0 : modelBaseOf(kit, facts);
295
+ }
192
296
  /**
193
297
  * provider 级 extends 的全量模型继承:route 不写 models 时,
194
298
  * 以继承源 provider 的全部内置模型作为条目(每个条目 base 即其自身)。
195
299
  */
196
300
  function inheritedCatalogEntries(kit, provider) {
197
301
  if (!hasBuiltinProvider(kit, provider)) return [];
198
- return kit.getBuiltinModels(provider).map((model) => ({
199
- id: model.id,
200
- base: builtinModelBase(kit, provider, model.id) ?? {}
201
- }));
302
+ return kit.getBuiltinModels(provider).map((model) => {
303
+ const facts = builtinModelFacts(provider, model);
304
+ return {
305
+ id: model.id,
306
+ base: facts === void 0 ? {} : modelBaseOf(kit, facts)
307
+ };
308
+ });
309
+ }
310
+ /** 全量快照按 kit 备忘(同一 kit 实例的目录在进程内不变)。 */
311
+ const snapshotCache = /* @__PURE__ */ new WeakMap();
312
+ /** 归一化:小写 + 去掉分隔符(`/ - _ . 空格`),用于子序列匹配。 */
313
+ function compact(text) {
314
+ return text.toLowerCase().replace(/[\s/\-_.]+/g, "");
315
+ }
316
+ /** 按分隔符切词(小写),用于词首命中判定。 */
317
+ function words(text) {
318
+ return text.toLowerCase().split(/[\s/\-_.]+/).filter((word) => word.length > 0);
319
+ }
320
+ /**
321
+ * 模糊匹配得分:越小越相关;不匹配返回 undefined。
322
+ * 分级:完全相等 0 < 整体前缀 1 < 词首前缀 2 < 子串 3 < 归一化相等 4 <
323
+ * 归一化前缀 5 < 首字母缩写 6+(同一档内以命中跨度加权,越紧凑越小)。
324
+ *
325
+ * 缩写匹配要求首字符落在**词首**(`deepseek-flash` 的 `d`、`flash` 的 `f`):
326
+ * 否则任何长路径都会命中任意短查询(实测 `kimi` 会命中
327
+ * `cloudflare-ai-gateway/workers-ai/@cf/ibm-granite/...`——wokers 里的 k 起头也能凑出
328
+ * 子序列),搜索质量崩坏。跳字过多(超过 needle 长度 4 倍)同样判不命中。
329
+ */
330
+ function fuzzyScore(query, target) {
331
+ const needle = query.toLowerCase();
332
+ if (needle.length === 0) return 0;
333
+ const haystack = target.toLowerCase();
334
+ if (haystack === needle) return 0;
335
+ if (haystack.startsWith(needle)) return 1;
336
+ if (words(target).some((word) => word.startsWith(needle))) return 2;
337
+ if (haystack.includes(needle)) return 3;
338
+ const compactNeedle = compact(query);
339
+ const compactTarget = compact(target);
340
+ if (compactNeedle.length === 0) return 0;
341
+ if (compactTarget === compactNeedle) return 4;
342
+ if (compactTarget.startsWith(compactNeedle)) return 5;
343
+ if (compactNeedle.length === 1) return 6;
344
+ const first = needle[0];
345
+ const spanCap = compactNeedle.length * 4 + 4;
346
+ let best;
347
+ for (const start of wordStarts(target)) {
348
+ if (haystack[start] !== first) continue;
349
+ let cursor = 1;
350
+ let last = start;
351
+ for (let i = start + 1; i < haystack.length && cursor < compactNeedle.length; i += 1) {
352
+ const ch = haystack[i];
353
+ if (/[\s/\-_.]/.test(ch)) continue;
354
+ if (ch === compactNeedle[cursor]) {
355
+ cursor += 1;
356
+ last = i;
357
+ }
358
+ }
359
+ if (cursor < compactNeedle.length) continue;
360
+ const span = last - start + 1;
361
+ if (span > spanCap) continue;
362
+ const score = 6 + span / spanCap;
363
+ if (best === void 0 || score < best) best = score;
364
+ }
365
+ return best;
366
+ }
367
+ /** 词首下标(按分隔符切分的每个词的首字符位置)。 */
368
+ function wordStarts(text) {
369
+ const out = [];
370
+ let atStart = true;
371
+ for (let i = 0; i < text.length; i += 1) {
372
+ const ch = text[i];
373
+ if (/[\s/\-_.]/.test(ch)) {
374
+ atStart = true;
375
+ continue;
376
+ }
377
+ if (atStart) {
378
+ out.push(i);
379
+ atStart = false;
380
+ }
381
+ }
382
+ return out;
383
+ }
384
+ /** 单条记录的多字段得分:取最相关字段;全部字段都不匹配返回 undefined。 */
385
+ function scoreOf(query, info) {
386
+ let best;
387
+ for (const field of [
388
+ info.path,
389
+ info.id,
390
+ info.name,
391
+ info.provider
392
+ ]) {
393
+ const score = fuzzyScore(query, field);
394
+ if (score === void 0) continue;
395
+ const weighted = field === info.path ? score : score + 2;
396
+ if (best === void 0 || weighted < best) best = weighted;
397
+ }
398
+ return best;
399
+ }
400
+ /** 多词 AND:每个词都要有命中,得分取各词之和(词少者更相关由长度自然体现)。 */
401
+ function multiScore(query, info) {
402
+ const tokens = query.split(/\s+/).filter((token) => token.length > 0);
403
+ if (tokens.length === 0) return 0;
404
+ let total = 0;
405
+ for (const token of tokens) {
406
+ const score = scoreOf(token, info);
407
+ if (score === void 0) return void 0;
408
+ total += score;
409
+ }
410
+ return total;
411
+ }
412
+ function toInfo(kit, facts) {
413
+ return {
414
+ provider: facts.provider,
415
+ id: facts.id,
416
+ path: facts.path,
417
+ name: facts.name,
418
+ api: facts.api,
419
+ baseUrl: facts.baseUrl,
420
+ contextWindow: facts.contextWindow,
421
+ maxTokens: facts.maxTokens,
422
+ input: facts.input,
423
+ reasoning: facts.reasoning,
424
+ ...facts.thinkingLevelMap === void 0 ? {} : { thinkingLevelMap: facts.thinkingLevelMap },
425
+ ...facts.compat === void 0 ? {} : { compat: facts.compat },
426
+ rest: facts.rest,
427
+ servable: kit.protocols.includes(facts.api)
428
+ };
429
+ }
430
+ /** 全量模型条目(按目录顺序:provider 排序即 pi-ai 目录顺序,模型同)。 */
431
+ function listModelInfos(kit) {
432
+ const cached = snapshotCache.get(kit);
433
+ if (cached !== void 0) return cached;
434
+ const out = [];
435
+ for (const provider of kit.getBuiltinProviders()) for (const model of kit.getBuiltinModels(provider)) {
436
+ const facts = builtinModelFacts(provider, model);
437
+ if (facts !== void 0) out.push(toInfo(kit, facts));
438
+ }
439
+ snapshotCache.set(kit, out);
440
+ return out;
441
+ }
442
+ /** 供应商条目(含模型数与显示名)。 */
443
+ function listProviders(kit) {
444
+ return builtinProviderEntries(kit);
445
+ }
446
+ /**
447
+ * 紧凑协议索引 `{provider: {modelId: api}}`:浏览器半据此推断某个 route 当前
448
+ * 实际生效的协议(route 级 `api` 缺省时由模型继承值决定),用于一键添加前的冲突检查。
449
+ */
450
+ function buildApiIndex(infos) {
451
+ const out = {};
452
+ for (const info of infos) {
453
+ const perProvider = out[info.provider] ?? {};
454
+ perProvider[info.id] = info.api;
455
+ out[info.provider] = perProvider;
456
+ }
457
+ return out;
458
+ }
459
+ function matchesFilters(info, query) {
460
+ if (query.provider !== void 0 && query.provider.length > 0 && info.provider !== query.provider) return false;
461
+ if (query.api !== void 0 && query.api.length > 0 && info.api !== query.api) return false;
462
+ if (query.reasoning === true && !info.reasoning) return false;
463
+ if (query.image === true && !info.input.includes("image")) return false;
464
+ return true;
465
+ }
466
+ function clampLimit(value) {
467
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) return 40;
468
+ return Math.min(Math.floor(value), 100);
469
+ }
470
+ /**
471
+ * 搜索 + 过滤 + 分页。无搜索串时按目录顺序(provider/id)返回;
472
+ * 有搜索串时按相关度排序,同分按路径字典序稳定。
473
+ */
474
+ function browseModels(kit, query = {}) {
475
+ const all = listModelInfos(kit);
476
+ const servableOnly = query.servableOnly !== false;
477
+ const limit = clampLimit(query.limit);
478
+ const offset = Math.max(0, Math.floor(query.offset ?? 0));
479
+ const q = (query.q ?? "").trim();
480
+ let servableHidden = 0;
481
+ const matched = [];
482
+ for (const info of all) {
483
+ if (!info.servable) {
484
+ if (servableOnly) servableHidden += 1;
485
+ if (servableOnly) continue;
486
+ }
487
+ if (!matchesFilters(info, query)) continue;
488
+ const score = q.length === 0 ? 0 : multiScore(q, info);
489
+ if (score === void 0) continue;
490
+ matched.push({
491
+ info,
492
+ score
493
+ });
494
+ }
495
+ if (q.length > 0) matched.sort((left, right) => left.score === right.score ? left.info.path.length - right.info.path.length || left.info.path.localeCompare(right.info.path) : left.score - right.score);
496
+ return {
497
+ total: matched.length,
498
+ servableHidden,
499
+ offset,
500
+ limit,
501
+ items: matched.slice(offset, offset + limit).map((entry) => entry.info)
502
+ };
202
503
  }
203
504
  //#endregion
204
- //#region src/compat-gates.ts
505
+ //#region src/official-surface.ts
506
+ /**
507
+ * 官方适配面的**自动继承**:从已安装的官方副本(`dsh-llm-pi-ai` + `pi-ai`)
508
+ * 现场推导三样东西,取代早期的手抄镜像:
509
+ *
510
+ * 1. **线协议集合**:官方包根导出的 `supportedProtocols()`(官方 PROTOCOLS 表
511
+ * 的唯一投影)。官方新增协议时本插件自动可用,不再需要改代码;
512
+ * 2. **compat 门控与取值约束**:门控取自运行期 bundle 的 `COMPAT_GATES`
513
+ * (事实源:运行期拒绝哪些字段由它决定),取值约束取自导出的 Config schema
514
+ * 的 `providers.*.compat` 节点(boolean / number.step(1) / enum / 对象);
515
+ * 3. **模型条目字段集**:Config schema 的 `providers.*.models` 键集——继承时按此
516
+ * 透传 pi-ai 目录的同名字段,官方将来新增模型级字段即自动跟随。
517
+ *
518
+ * 历史教训(0.1.5-rc.1):compat 门控曾是官方 catalog.ts 的手抄镜像,官方扩容
519
+ * offer 字段而旧表漏收,导致官方可配字段被本插件**误拒**(静默功能缺失、无报错)。
520
+ * 现改为现场推导,`tests/official-surface.test.ts` 直读官方副本守门推导正确性。
521
+ *
522
+ * 推导失败(官方改布局/改打包形态)时回退到 FALLBACK_*:compat 表用最后已知快照
523
+ * 且**放宽未知键**(见 compat.ts:宁可放行交给官方自身校验,也不误拒官方新字段),
524
+ * 协议回退三元组、模型字段回退已知七键,各自给出诊断——绝不因推导失败弄挂启动。
525
+ * @module llm-pi/official-surface
526
+ */
205
527
  /** 从 `{` 起做括号配对,返回内部文本;未配对返回 undefined。 */
206
528
  function balancedBody(text, openIndex) {
207
529
  const start = text.indexOf("{", openIndex);
@@ -291,6 +613,43 @@ function deriveSpecs(module) {
291
613
  }
292
614
  return out;
293
615
  }
616
+ /**
617
+ * 从官方适配器模块推导线协议集合:官方包根导出的 `supportedProtocols()`
618
+ * (`Object.keys(PROTOCOLS)` 的投影,顺序即官方表顺序)。
619
+ * 非函数/返回空/含非字符串项都视为推导失败,调用方回退内置三元组。
620
+ */
621
+ function deriveProtocols(module) {
622
+ const reader = module["supportedProtocols"];
623
+ if (typeof reader !== "function") return void 0;
624
+ const value = reader();
625
+ if (!Array.isArray(value) || value.length === 0) return void 0;
626
+ const ids = value.filter((id) => typeof id === "string" && id.length > 0);
627
+ return ids.length === value.length ? ids : void 0;
628
+ }
629
+ /**
630
+ * 从官方 Config schema 推导模型条目字段集(`providers.*.models` 的元素键集)。
631
+ * 这些是官方适配器**接受**的模型级键;继承时按此透传 pi-ai 目录的同名字段。
632
+ */
633
+ function deriveModelEntryFields(module) {
634
+ const fields = (module["Config"]?.dict?.["providers"]?.inner?.dict?.["models"])?.inner?.dict;
635
+ if (fields === void 0) return void 0;
636
+ const keys = Object.keys(fields);
637
+ return keys.length > 0 ? keys : void 0;
638
+ }
639
+ /** 推导失败时的模型条目字段兜底(0.2.x 线实测键集)。 */
640
+ const FALLBACK_MODEL_ENTRY_FIELDS = [
641
+ "id",
642
+ "name",
643
+ "contextWindow",
644
+ "maxTokens",
645
+ "input",
646
+ "reasoningEfforts",
647
+ "compat"
648
+ ];
649
+ /** 协议推导失败时的兜底三元组(转出为可变数组,便于调用方直接持有)。 */
650
+ function fallbackProtocols() {
651
+ return [...FALLBACK_PROTOCOLS];
652
+ }
294
653
  /** 官方门控表不可解析时的最后已知快照(0.1.5-rc.2 实测值)。 */
295
654
  const FALLBACK_TABLE = {
296
655
  source: "fallback",
@@ -418,6 +777,13 @@ function compatFieldsOf(api) {
418
777
  if (gate === void 0) return [];
419
778
  return Object.entries(gate).flatMap(([field, disposition]) => disposition === "offer" ? [field] : []);
420
779
  }
780
+ /**
781
+ * 生效表里出现过的协议键(运行期协议列表为空时的渲染兜底:
782
+ * 套件异常也要让表单能画出来)。
783
+ */
784
+ function compatProtocols() {
785
+ return Object.keys(active.gates);
786
+ }
421
787
  /** 某协议某 offer 字段的取值约束(UI 渲染开关/下拉用)。 */
422
788
  function compatFieldSpec(api, field) {
423
789
  return active.gates[api]?.[field] === "offer" ? active.specs[field] : void 0;
@@ -455,14 +821,16 @@ function checkValue(field, spec, value, where) {
455
821
  */
456
822
  function validateCompat(api, compat, where) {
457
823
  if (compat === void 0) return;
458
- const gate = active.gates[api];
459
- if (gate === void 0) throw new Error(`${where}: 协议 ${JSON.stringify(api)} 无 compat 字段表(支持:${Object.keys(active.gates).join(", ")})`);
824
+ const table = active;
825
+ const gate = table.gates[api];
826
+ if (gate === void 0) throw new Error(`${where}: 协议 ${JSON.stringify(api)} 无 compat 字段表(可配协议:${Object.keys(table.gates).join(", ")})`);
460
827
  const offered = compatFieldsOf(api);
828
+ const permissive = table.source === "fallback";
461
829
  for (const [key, value] of Object.entries(compat)) {
462
830
  const disposition = gate[key];
463
831
  if (disposition !== "offer") {
464
832
  if (disposition === "withhold") throw new Error(`${where}: compat.${key} 官方按协议 withhold(内置目录已为对应厂商设置该开关);请以目录 provider 名作为 route 名(继承目录值),或移除该字段`);
465
- throw new Error(`${where}: compat.${key} 不是 ${api} 协议的合法字段(可配置字段:${offered.join(", ")};官方全部可配字段:${allOfferedFields().join(", ")})`);
833
+ if (!permissive) throw new Error(`${where}: compat.${key} 不是 ${api} 协议的合法字段(可配置字段:${offered.join(", ")};官方全部可配字段:${allOfferedFields().join(", ")})`);
466
834
  }
467
835
  if (value === void 0 || value === null) throw new Error(`${where}: compat.${key} 未设置值;给出值或移除该键(留空不会生效)`);
468
836
  const spec = active.specs[key];
@@ -484,13 +852,28 @@ function mergeCompat(...layers) {
484
852
  //#endregion
485
853
  //#region src/config-api.ts
486
854
  const ROUTE_CATALOG = "/dsh-plus/llm-pi/catalog";
855
+ const ROUTE_MODELS = `${ROUTE_CATALOG}/models`;
856
+ /** 查询参数里的布尔开关:'1'/'true' 为真。 */
857
+ function boolParam(value, fallback) {
858
+ if (value === null) return fallback;
859
+ return value === "1" || value === "true";
860
+ }
861
+ /** 查询参数里的整数;缺失/非法返回 undefined(由消费方取默认)。 */
862
+ function intParam(value) {
863
+ if (value === null || value.trim() === "") return void 0;
864
+ const parsed = Number(value);
865
+ return Number.isFinite(parsed) ? parsed : void 0;
866
+ }
487
867
  /**
488
868
  * 服务端推导的 compat 字段表(协议 → 字段 → 取值约束)。浏览器半读不到官方包,
489
869
  * 由本端点下发,UI 渲染与服务端校验同源(不再各存一份手抄镜像表)。
870
+ * 协议列表取运行期生效集合(不能服务的协议不渲染控件);运行期列表为空时退回
871
+ * 生效表自身的协议键,保证套件异常时表单仍可编辑。
490
872
  */
491
- function compatTablePayload() {
873
+ function compatTablePayload(protocols) {
874
+ const list = protocols.length > 0 ? [...protocols] : compatProtocols();
492
875
  const fields = {};
493
- for (const api of PROTOCOL_IDS) {
876
+ for (const api of list) {
494
877
  const perField = {};
495
878
  for (const field of compatFieldsOf(api)) {
496
879
  const spec = compatFieldSpec(api, field);
@@ -512,37 +895,31 @@ function sendJson(res, status, body) {
512
895
  res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
513
896
  res.end(JSON.stringify(body));
514
897
  }
515
- /** 目录查询/手动拉取:GET ?provider=&source= → 该源模型 id 列表;POST /refresh → 立即拉取。 */
516
- function handleCatalog(runtime, req, res) {
517
- const kitSource = runtime.kitInfo().source;
518
- if (req.method === "POST" && req.url?.endsWith("/refresh")) {
519
- runtime.modelsDev.refresh().then(() => {
520
- sendJson(res, 200, { status: runtime.modelsDev.status() });
521
- });
522
- return;
523
- }
524
- const url = new URL(req.url ?? "", "http://localhost");
525
- const provider = url.searchParams.get("provider") ?? "";
526
- const source = url.searchParams.get("source") ?? "builtin";
527
- const compat = compatTablePayload();
528
- if (source === "models-dev") {
529
- sendJson(res, 200, {
530
- providers: runtime.modelsDev.providerIds(),
531
- models: provider.length > 0 ? runtime.modelsDev.modelIds(provider) : [],
532
- status: runtime.modelsDev.status(),
533
- kitSource,
534
- compat
535
- });
536
- return;
537
- }
898
+ /** 运行期事实 + 目录索引(卡片挂载时拉一次:版本、协议、目录规模、compat 字段表)。 */
899
+ function handleMeta(runtime, res) {
900
+ const kit = runtime.kitInfo();
538
901
  sendJson(res, 200, {
539
- providers: runtime.kit.getBuiltinProviders(),
540
- models: provider.length > 0 ? builtinModelIds(runtime.kit, provider) : [],
541
- kitSource,
542
- compat
902
+ kit,
903
+ providers: runtime.providerEntries(),
904
+ apis: runtime.apiIndex(),
905
+ compat: compatTablePayload(kit.protocols)
543
906
  });
544
907
  }
545
- /** 注册目录路由(webServer 缺失时由调用方保证不调用)。 */
908
+ /** 目录搜索:q(模糊)/provider/api/reasoning/image/servable/offset/limit。 */
909
+ function handleModels(runtime, url, res) {
910
+ const limit = intParam(url.searchParams.get("limit"));
911
+ sendJson(res, 200, browseModels(runtime.kit, {
912
+ q: url.searchParams.get("q") ?? "",
913
+ provider: url.searchParams.get("provider") ?? "",
914
+ api: url.searchParams.get("api") ?? "",
915
+ reasoning: boolParam(url.searchParams.get("reasoning"), false),
916
+ image: boolParam(url.searchParams.get("image"), false),
917
+ servableOnly: boolParam(url.searchParams.get("servable"), true),
918
+ offset: intParam(url.searchParams.get("offset")) ?? 0,
919
+ ...limit === void 0 ? {} : { limit }
920
+ }));
921
+ }
922
+ /** 目录路由(webServer 缺失时由调用方保证不调用)。 */
546
923
  function registerCatalogApi(ctx, runtime) {
547
924
  const logger = ctx.logger("llm-pi");
548
925
  const guard = (handler) => {
@@ -560,195 +937,20 @@ function registerCatalogApi(ctx, runtime) {
560
937
  ctx.webServer.register({
561
938
  kind: "prefix",
562
939
  path: ROUTE_CATALOG,
563
- handler: guard((req, res) => handleCatalog(runtime, req, res))
564
- });
565
- }
566
- //#endregion
567
- //#region src/catalog/models-dev.ts
568
- /**
569
- * models.dev 兜底目录源:继承解析的第二级数据源。
570
- *
571
- * 只用于内置目录尚未收录的新模型/新供应商。数据是公开快照
572
- *(默认 https://models.dev/api.json),**默认不自动拉取**(catalogRefreshHours=0);
573
- * 拉取方式二选一:
574
- * - 配置 catalogRefreshHours > 0:启动/过期后后台自动刷新;
575
- * - 配置卡片「手动拉取」或 POST /catalog/refresh:立即拉取。
576
- * 拉取可经 catalogProxy 代理(HTTP 代理,如 http://127.0.0.1:7890)。
577
- * 成功落盘缓存($DSH_HOME/dsh-plus/llm-pi/models-dev.json);任何失败都退化为
578
- * "仅内置目录",绝不阻塞 route 注册。
579
- *
580
- * 保守原则:models.dev 数据未经 pi 官方校正(无 compat/thinkingLevelMap),
581
- * 且模态声明不被采信——继承自本源的模型 input 一律走 text-only 兜底,
582
- * 视觉等模态须用户在条目上显式声明(防 over-claiming 导致会话重复失败请求)。
583
- * @module llm-pi/catalog/models-dev
584
- */
585
- const FETCH_TIMEOUT_MS = 2e4;
586
- /** 目录文档体上限(api.json 全量约 1-2MB,放宽到 20MB 防未来膨胀)。 */
587
- const MAX_RESPONSE_BYTES$1 = 20971520;
588
- /**
589
- * 极简 JSON GET(node:http(s) 实现):支持 HTTP 代理(仅 https 目标)与超时。
590
- * 不跟随重定向(models.dev 直链无重定向;自定义端点需自行保证可直达)。
591
- */
592
- function fetchJson(url, proxy, timeoutMs) {
593
- return new Promise((resolve, reject) => {
594
- const target = new URL(url);
595
- const req = (target.protocol === "https:" ? request$1 : request)(url, {
596
- agent: target.protocol === "https:" && proxy.length > 0 ? new HttpsProxyAgent(proxy) : void 0,
597
- timeout: timeoutMs,
598
- headers: { accept: "application/json" }
599
- }, (response) => {
600
- const chunks = [];
601
- let size = 0;
602
- let settled = false;
603
- const fail = (error) => {
604
- if (settled) return;
605
- settled = true;
606
- reject(error);
607
- };
608
- response.on("data", (chunk) => {
609
- size += chunk.length;
610
- if (size > MAX_RESPONSE_BYTES$1) {
611
- req.destroy(/* @__PURE__ */ new Error("响应超过 20MB 上限"));
612
- return;
613
- }
614
- chunks.push(chunk);
615
- });
616
- response.on("end", () => {
617
- if (settled) return;
618
- settled = true;
619
- resolve({
620
- status: response.statusCode ?? 0,
621
- body: Buffer.concat(chunks).toString("utf8")
622
- });
623
- });
624
- response.on("error", fail);
625
- });
626
- req.on("timeout", () => req.destroy(/* @__PURE__ */ new Error(`请求超时(${timeoutMs}ms)`)));
627
- req.on("error", reject);
628
- req.end();
940
+ handler: guard((req, res) => {
941
+ const url = new URL(req.url ?? "", "http://localhost");
942
+ if (url.pathname === ROUTE_MODELS) {
943
+ handleModels(runtime, url, res);
944
+ return;
945
+ }
946
+ if (url.pathname !== ROUTE_CATALOG) {
947
+ sendJson(res, 404, { error: `未知端点 ${url.pathname}` });
948
+ return;
949
+ }
950
+ handleMeta(runtime, res);
951
+ })
629
952
  });
630
953
  }
631
- function isDocument(value) {
632
- return typeof value === "object" && value !== null && !Array.isArray(value);
633
- }
634
- /** 单个 models.dev 模型条目 → 继承 base(仅采信名称/容量/推理能力)。 */
635
- function toModelBase(entry) {
636
- const base = {};
637
- if (typeof entry.name === "string" && entry.name.length > 0) base.name = entry.name;
638
- const context = entry.limit?.context;
639
- if (typeof context === "number" && Number.isInteger(context) && context > 0) base.contextWindow = context;
640
- const output = entry.limit?.output;
641
- if (typeof output === "number" && Number.isInteger(output) && output > 0) base.maxTokens = output;
642
- base.reasoning = entry.reasoning === true;
643
- return base;
644
- }
645
- var ModelsDevSource = class {
646
- cacheFile;
647
- url;
648
- ttlHours;
649
- proxy;
650
- log;
651
- document;
652
- fetchedAt = null;
653
- lastError = null;
654
- refreshing;
655
- constructor(cacheFile, url, ttlHours, log, proxy = "") {
656
- this.cacheFile = cacheFile;
657
- this.url = url;
658
- this.ttlHours = ttlHours;
659
- this.log = log;
660
- this.proxy = proxy;
661
- }
662
- /** 配置变更时更新端点/TTL/代理并触发刷新(去抖由 refresh 的进行中复用承担)。 */
663
- reconfigure(url, ttlHours, proxy) {
664
- if (url === this.url && ttlHours === this.ttlHours && proxy === this.proxy) return;
665
- this.url = url;
666
- this.ttlHours = ttlHours;
667
- this.proxy = proxy;
668
- if (ttlHours > 0 && this.isStale()) this.refresh();
669
- }
670
- /** 是否已有可用数据(缓存或拉取成功);与自动拉取开关无关。 */
671
- get enabled() {
672
- return this.document !== void 0;
673
- }
674
- /** 加载缓存,并在启用自动拉取且缓存过期时后台刷新;构造后调用一次,永不抛错。 */
675
- async ensureLoaded() {
676
- this.loadCache();
677
- if (this.ttlHours > 0 && this.isStale()) await this.refresh();
678
- }
679
- /** 强制刷新(手动拉取/配置变更触发);ttlHours=0 时同样生效(手动拉取不受自动开关限制)。 */
680
- async refresh() {
681
- this.refreshing ??= this.doFetch().finally(() => {
682
- this.refreshing = void 0;
683
- });
684
- await this.refreshing;
685
- }
686
- /** 查继承 base;未命中/未启用返回 undefined。 */
687
- lookup(provider, modelId) {
688
- const entry = this.document?.[provider]?.models?.[modelId];
689
- if (entry === void 0) return void 0;
690
- return toModelBase(entry);
691
- }
692
- /** 全部 provider id(UI extends 选择器用)。 */
693
- providerIds() {
694
- return Object.keys(this.document ?? {});
695
- }
696
- /** 某 provider 的模型 id 列表(UI extends 选择器用)。 */
697
- modelIds(provider) {
698
- return Object.keys(this.document?.[provider]?.models ?? {});
699
- }
700
- status() {
701
- const providers = Object.keys(this.document ?? {});
702
- const models = providers.reduce((total, p) => total + Object.keys(this.document?.[p]?.models ?? {}).length, 0);
703
- return {
704
- fetchedAt: this.fetchedAt,
705
- providers: providers.length,
706
- models,
707
- error: this.lastError
708
- };
709
- }
710
- isStale() {
711
- if (this.fetchedAt === null) return true;
712
- const ageMs = Date.now() - Date.parse(this.fetchedAt);
713
- return !Number.isFinite(ageMs) || ageMs > this.ttlHours * 36e5;
714
- }
715
- loadCache() {
716
- try {
717
- const raw = JSON.parse(readFileSync(this.cacheFile, "utf8"));
718
- if (!isDocument(raw.data) || typeof raw.fetchedAt !== "string") throw new Error("缓存形状非法");
719
- this.document = raw.data;
720
- this.fetchedAt = raw.fetchedAt;
721
- } catch {}
722
- }
723
- async doFetch() {
724
- try {
725
- const response = await fetchJson(this.url, this.proxy, FETCH_TIMEOUT_MS);
726
- if (response.status < 200 || response.status >= 300) throw new Error(`HTTP ${response.status}`);
727
- const data = JSON.parse(response.body);
728
- if (!isDocument(data)) throw new Error("响应不是 models.dev 目录文档");
729
- this.document = data;
730
- this.fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
731
- this.lastError = null;
732
- this.persistCache(data);
733
- } catch (error) {
734
- this.lastError = error instanceof Error ? error.message : String(error);
735
- this.log(`models.dev 目录拉取失败(沿用缓存/仅内置目录):${this.lastError}`);
736
- }
737
- }
738
- persistCache(data) {
739
- try {
740
- mkdirSync(dirname(this.cacheFile), { recursive: true });
741
- const tmp = `${this.cacheFile}.tmp`;
742
- writeFileSync(tmp, JSON.stringify({
743
- fetchedAt: this.fetchedAt,
744
- data
745
- }), "utf8");
746
- renameSync(tmp, this.cacheFile);
747
- } catch (error) {
748
- this.log(`models.dev 缓存写入失败:${error instanceof Error ? error.message : String(error)}`);
749
- }
750
- }
751
- };
752
954
  //#endregion
753
955
  //#region src/deepseek-routes.ts
754
956
  /** 凭据解析(逐行对齐官方 llm-deepseek resolveApiKey):凭据服务优先,启动环境兜底。 */
@@ -1083,6 +1285,131 @@ async function discoverModels(request, deps) {
1083
1285
  }
1084
1286
  }
1085
1287
  //#endregion
1288
+ //#region src/kit-meta.ts
1289
+ /**
1290
+ * 运行期套件元信息:插件实际在用哪一份 pi-ai / 官方适配器,以及版本是否落在
1291
+ * 本插件验证过的区间内。
1292
+ *
1293
+ * 存在的理由:插件**按文件路径**动态加载已装 dsh 树里的 pi-ai(版本随 dsh 升级
1294
+ * 自动变化,见 resolve-dsh.ts),因此"当前生效版本"不是 manifest 上写的版本。
1295
+ * 配置页把它显示出来,版本漂移一眼可见;`VERIFIED_PI_AI_RANGE` 只用于**提示**,
1296
+ * 永不阻断——能不能用由运行期形状自检(assertKitShape)与逐项降级决定。
1297
+ * @module llm-pi/kit-meta
1298
+ */
1299
+ /** 本插件开发/测试覆盖过的 pi-ai 区间(semver range,仅用于状态行提示)。 */
1300
+ const VERIFIED_PI_AI_RANGE = ">=0.85.1 <0.88.0";
1301
+ /** 读 package.json 的 version;文件不存在/非 JSON/无 version 都返回 undefined。 */
1302
+ function readPackageVersion(manifestPath) {
1303
+ try {
1304
+ const version = JSON.parse(readFileSync(manifestPath, "utf8"))?.version;
1305
+ return typeof version === "string" && version.length > 0 ? version : void 0;
1306
+ } catch {
1307
+ return;
1308
+ }
1309
+ }
1310
+ /** 读包目录的版本(`<dir>/package.json`)。 */
1311
+ function packageVersionOf(packageDir) {
1312
+ return readPackageVersion(join(packageDir, "package.json"));
1313
+ }
1314
+ /** 只读包名(判断安装树根是不是 dsh 本体)。 */
1315
+ function readPackageName(manifestPath) {
1316
+ try {
1317
+ const name = JSON.parse(readFileSync(manifestPath, "utf8"))?.name;
1318
+ return typeof name === "string" ? name : void 0;
1319
+ } catch {
1320
+ return;
1321
+ }
1322
+ }
1323
+ /** 收集 dsh 安装树内实际加载的那几个包的版本。 */
1324
+ function collectTreeVersions(root) {
1325
+ const nm = join(root, "node_modules");
1326
+ const piAi = packageVersionOf(join(nm, "@earendil-works", "pi-ai"));
1327
+ const adapter = packageVersionOf(join(nm, "@deepseek-ai", "dsh-llm-pi-ai"));
1328
+ const dsh = readPackageName(join(root, "package.json")) === "@deepseek-ai/dsh" ? packageVersionOf(root) : void 0;
1329
+ return {
1330
+ ...piAi === void 0 ? {} : { piAi },
1331
+ ...adapter === void 0 ? {} : { piAiAdapter: adapter },
1332
+ ...dsh === void 0 ? {} : { dsh }
1333
+ };
1334
+ }
1335
+ /**
1336
+ * 由模块入口文件向上找包目录(最多 3 层,兼容 `<pkg>/{lib,dist}/index.js`
1337
+ * 与根目录入口两种布局);找不到返回 undefined。
1338
+ */
1339
+ function packageDirOf(entryFile) {
1340
+ let dir = dirname(entryFile);
1341
+ for (let depth = 0; depth < 3; depth += 1) {
1342
+ if (readPackageName(join(dir, "package.json")) !== void 0) return dir;
1343
+ const parent = dirname(dir);
1344
+ if (parent === dir) return void 0;
1345
+ dir = parent;
1346
+ }
1347
+ }
1348
+ /**
1349
+ * 解析已安装包的目录(本进程内解析,只用于读 package.json)。
1350
+ * 拿不到即返回 undefined(该版本号留空,不影响运行)。
1351
+ */
1352
+ function installedPackageDir(specifier) {
1353
+ try {
1354
+ return packageDirOf(fileURLToPath(import.meta.resolve(specifier)));
1355
+ } catch {
1356
+ return;
1357
+ }
1358
+ }
1359
+ /** 收集插件 vendored 兜底副本的版本(回退路径下"生效版本"即这两份)。 */
1360
+ function collectVendoredVersions() {
1361
+ const piAiDir = installedPackageDir("@earendil-works/pi-ai");
1362
+ const adapterDir = installedPackageDir("@deepseek-ai/dsh-llm-pi-ai");
1363
+ const piAi = piAiDir === void 0 ? void 0 : packageVersionOf(piAiDir);
1364
+ const adapter = adapterDir === void 0 ? void 0 : packageVersionOf(adapterDir);
1365
+ return {
1366
+ ...piAi === void 0 ? {} : { piAi },
1367
+ ...adapter === void 0 ? {} : { piAiAdapter: adapter }
1368
+ };
1369
+ }
1370
+ /** 已验证区间的解析端点(与 {@link VERIFIED_PI_AI_RANGE} 同步维护)。 */
1371
+ const VERIFIED_MIN = [
1372
+ 0,
1373
+ 85,
1374
+ 1
1375
+ ];
1376
+ const VERIFIED_MAX_EXCLUSIVE = [
1377
+ 0,
1378
+ 88,
1379
+ 0
1380
+ ];
1381
+ /** 解析 `x.y.z[-pre][+build]` 的数值三元组;形状不符返回 undefined。 */
1382
+ function numericTriple(version) {
1383
+ const parts = (version.split(/[-+]/, 1)[0] ?? "").split(".");
1384
+ if (parts.length !== 3) return void 0;
1385
+ const [a, b, c] = parts.map((part) => /^\d+$/.test(part) ? Number.parseInt(part, 10) : NaN);
1386
+ if (a === void 0 || b === void 0 || c === void 0) return void 0;
1387
+ if (!Number.isInteger(a) || !Number.isInteger(b) || !Number.isInteger(c)) return void 0;
1388
+ return [
1389
+ a,
1390
+ b,
1391
+ c
1392
+ ];
1393
+ }
1394
+ function compareTriple(left, right) {
1395
+ for (let i = 0; i < 3; i += 1) {
1396
+ const diff = left[i] - right[i];
1397
+ if (diff !== 0) return diff;
1398
+ }
1399
+ return 0;
1400
+ }
1401
+ /**
1402
+ * 生效 pi-ai 版本不在已验证区间时的提示文案(**提示而非阻断**)。
1403
+ * 区间内或版本号形状无法解析时返回 undefined。
1404
+ */
1405
+ function piAiVersionNotice(version) {
1406
+ if (version === void 0) return void 0;
1407
+ const parsed = numericTriple(version);
1408
+ if (parsed === void 0) return void 0;
1409
+ if (compareTriple(parsed, VERIFIED_MIN) < 0) return `当前生效 pi-ai ${version} 低于本插件验证过的下限 ${VERIFIED_PI_AI_RANGE}`;
1410
+ if (compareTriple(parsed, VERIFIED_MAX_EXCLUSIVE) >= 0) return `当前生效 pi-ai ${version} 超出本插件验证过的区间 ${VERIFIED_PI_AI_RANGE};已启用现场推导与形状自检,若出现异常请先看诊断行`;
1411
+ }
1412
+ //#endregion
1086
1413
  //#region src/inherit.ts
1087
1414
  var ExtendsError = class extends Error {};
1088
1415
  /** 解析 "provider/model" 或裸 "model" 引用。 */
@@ -1097,42 +1424,37 @@ function parseExtendsRef(raw) {
1097
1424
  model
1098
1425
  };
1099
1426
  }
1100
- function lookup(kit, modelsDev, provider, model) {
1101
- const builtin = builtinModelBase(kit, provider, model);
1102
- if (builtin !== void 0) return {
1103
- base: builtin,
1104
- source: "builtin",
1105
- sourceProvider: provider
1106
- };
1107
- const dev = modelsDev?.lookup(provider, model);
1108
- if (dev !== void 0) return {
1109
- base: dev,
1110
- source: "models-dev",
1111
- sourceProvider: provider
1112
- };
1113
- }
1114
1427
  /**
1115
1428
  * 解析一个模型条目的继承 base。
1116
1429
  * @throws ExtendsError 显式 extends 引用不存在(写入时拒绝,指明引用名)。
1117
1430
  */
1118
- function resolveModelBase(route, profile, entry, kit, modelsDev) {
1431
+ function resolveModelBase(route, profile, entry, kit) {
1119
1432
  const where = `provider "${route}" model "${entry.id}"`;
1120
1433
  if (entry.extends === void 0) {
1121
1434
  if (profile.extends === void 0) return {
1122
1435
  base: {},
1123
1436
  source: "none"
1124
1437
  };
1125
- return lookup(kit, modelsDev, profile.extends, entry.id) ?? {
1438
+ const base = builtinModelBase(kit, profile.extends, entry.id);
1439
+ return base === void 0 ? {
1126
1440
  base: {},
1127
1441
  source: "none"
1442
+ } : {
1443
+ base,
1444
+ source: "builtin",
1445
+ sourceProvider: profile.extends
1128
1446
  };
1129
1447
  }
1130
1448
  const ref = parseExtendsRef(entry.extends);
1131
1449
  const provider = ref.provider ?? profile.extends;
1132
1450
  if (provider === void 0) throw new ExtendsError(`${where}: extends ${JSON.stringify(entry.extends)} 是裸模型 id,但本 route 未配置 provider 级 extends 查找源`);
1133
- const hit = lookup(kit, modelsDev, provider, ref.model);
1134
- if (hit === void 0) throw new ExtendsError(`${where}: extends 引用 "${provider}/${ref.model}" 在内置目录与 models.dev 快照中都不存在`);
1135
- return hit;
1451
+ const base = builtinModelBase(kit, provider, ref.model);
1452
+ if (base === void 0) throw new ExtendsError(`${where}: extends 引用 "${provider}/${ref.model}" 不在 pi-ai ${kit.versions.piAi ?? "?"} 的内置目录中(可继承的 provider/模型见配置页「内置模型目录」,或用裸 model id 配合 route 级 extends)`);
1453
+ return {
1454
+ base,
1455
+ source: "builtin",
1456
+ sourceProvider: provider
1457
+ };
1136
1458
  }
1137
1459
  //#endregion
1138
1460
  //#region src/profiles-deepseek.ts
@@ -1339,6 +1661,26 @@ function invalid(provider, detail) {
1339
1661
  function declaredInput(configured) {
1340
1662
  return configured === void 0 || configured.length === 0 ? void 0 : [...configured];
1341
1663
  }
1664
+ /**
1665
+ * 归一化条目里"本层不处理、需原样进 pi-ai Model"的字段(继承自目录的未来
1666
+ * 模型级字段,如 samplingParams)。官方 schema 当前的模型条目键(id/name/
1667
+ * contextWindow/maxTokens/input/reasoningEfforts/compat)在此全部排除:
1668
+ * 它们要么由本层显式翻译,要么不能出现在 pi-ai Model 上。
1669
+ */
1670
+ function extraEntryFields(entry) {
1671
+ const known = /* @__PURE__ */ new Set([
1672
+ "id",
1673
+ "name",
1674
+ "contextWindow",
1675
+ "maxTokens",
1676
+ "input",
1677
+ "reasoningEfforts",
1678
+ "compat"
1679
+ ]);
1680
+ const out = {};
1681
+ for (const [key, value] of Object.entries(entry)) if (!known.has(key) && value !== void 0) out[key] = value;
1682
+ return out;
1683
+ }
1342
1684
  /** 显式 reasoningEfforts 字典的合法性校验(官方 resolveModelReasoning 同款规则)。 */
1343
1685
  function validateReasoningEfforts(provider, entryId, efforts) {
1344
1686
  if (Object.keys(efforts).length === 0) invalid(provider, `model "${entryId}" 的 reasoningEfforts 为空:声明档位、置 false 或缺省继承`);
@@ -1418,6 +1760,7 @@ function materializeModel(route, profile, entry, base, routeApi, defaultInput, d
1418
1760
  const compat = mergeCompat(base.api === api ? base.compat : void 0, profile.compat, entry.compat);
1419
1761
  return {
1420
1762
  entry: {
1763
+ ...base.extra ?? {},
1421
1764
  id: entry.id,
1422
1765
  ...name === entry.id ? {} : { name },
1423
1766
  ...contextWindow === 262144 ? {} : { contextWindow },
@@ -1445,7 +1788,7 @@ function materializeRouteModels(route, profile, deps, defaultInput) {
1445
1788
  seen.add(entry.id);
1446
1789
  let base;
1447
1790
  try {
1448
- base = resolveModelBase(route, profile, entry, deps.kit, deps.modelsDev).base;
1791
+ base = resolveModelBase(route, profile, entry, deps.kit).base;
1449
1792
  } catch (error) {
1450
1793
  if (!deps.lenient || !(error instanceof ExtendsError)) throw error;
1451
1794
  deps.warn?.(`llm-pi: provider "${route}" model "${entry.id}" 的 extends 引用当前不可解析(${error.message});已降级为手写条目`);
@@ -1504,6 +1847,10 @@ function normalizeRoute(route, profile, deps) {
1504
1847
  if ((profile.adapter ?? "pi") === "deepseek") return void 0;
1505
1848
  if (isDraftRoute(profile)) return void 0;
1506
1849
  if (route.length === 0) throw new Error("llm-pi: provider 名不能为空");
1850
+ if (profile.extends !== void 0 && !hasBuiltinProvider(deps.kit, profile.extends)) {
1851
+ if (!deps.lenient) invalid(route, `extends ${JSON.stringify(profile.extends)} 不是 pi-ai ${deps.kit.versions.piAi ?? "?"} 的内置 provider;可用:${deps.kit.getBuiltinProviders().join(", ")}`);
1852
+ deps.warn?.(`llm-pi: provider "${route}" 的 extends ${JSON.stringify(profile.extends)} 不是当前 pi-ai 的内置 provider,已忽略该继承源`);
1853
+ }
1507
1854
  if (profile.baseURL !== void 0 && profile.baseURL.length === 0) invalid(route, "baseURL 为空");
1508
1855
  if (profile.displayName !== void 0 && profile.displayName.length === 0) invalid(route, "displayName 为空");
1509
1856
  const streamIdleTimeoutMs = profile.streamIdleTimeoutMs ?? 3e5;
@@ -1519,7 +1866,7 @@ function normalizeRoute(route, profile, deps) {
1519
1866
  const materialized = materializeRouteModels(route, profile, deps, defaultInput);
1520
1867
  if (materialized === null) return void 0;
1521
1868
  const { api, baseUrl, models, configuredMaxTokens } = materialized;
1522
- if (deps.kit.protocolFactories[api] === void 0) invalid(route, `api ${JSON.stringify(api)} 本插件无法服务(支持:openai-completions/openai-responses/anthropic-messages)`);
1869
+ if (deps.kit.protocolFactories[api] === void 0) invalid(route, `api ${JSON.stringify(api)} 本插件无法服务(当前生效 pi-ai ${deps.kit.versions.piAi ?? "?"} 支持的协议:${deps.kit.protocols.join("/")})`);
1523
1870
  const displayName = profile.displayName ?? route;
1524
1871
  return {
1525
1872
  profile: {
@@ -1613,9 +1960,9 @@ function resolveProfilesFallback(providers, deps) {
1613
1960
  if (defaultInput.length === 0) invalid(provider, "defaultInput 至少要声明一种模态");
1614
1961
  const displayName = source.displayName ?? provider;
1615
1962
  const api = source.api;
1616
- if (api === void 0) invalid(provider, "需要 api:本插件仅支持 openai-completions/openai-responses/anthropic-messages");
1963
+ if (api === void 0) invalid(provider, `需要 api:本插件只服务官方适配器声明的协议(${deps.protocols.join("/")})`);
1617
1964
  const factory = deps.protocolFactories[api];
1618
- if (factory === void 0) invalid(provider, `api ${JSON.stringify(api)} 本插件无法服务(支持:openai-completions/openai-responses/anthropic-messages)`);
1965
+ if (factory === void 0) invalid(provider, `api ${JSON.stringify(api)} 本插件无法服务(当前支持的协议:${deps.protocols.join("/")})`);
1619
1966
  if (source.baseURL === void 0) invalid(provider, "需要 baseURL:官方 schema 无模型级端点,端点只能 route 级表达");
1620
1967
  const configuredMaxTokens = /* @__PURE__ */ new Map();
1621
1968
  const modelErrors = /* @__PURE__ */ new Map();
@@ -1630,6 +1977,7 @@ function resolveProfilesFallback(providers, deps) {
1630
1977
  if (!Number.isInteger(maxTokens) || maxTokens <= 0) invalid(provider, `model "${entry.id}" maxTokens 必须是正整数`);
1631
1978
  if (entry.maxTokens !== void 0) configuredMaxTokens.set(entry.id, entry.maxTokens);
1632
1979
  return {
1980
+ ...extraEntryFields(entry),
1633
1981
  id: entry.id,
1634
1982
  name: entry.name ?? entry.id,
1635
1983
  api,
@@ -1813,10 +2161,17 @@ function authContextFrom(ctx) {
1813
2161
  * 桌面端 Electron 下 argv[1] 探测走不通)→ realpath(argv[1]) 向上找到同时
1814
2162
  * 含 node_modules/@deepseek-ai/dsh-llm-pi-ai 与 node_modules/@earendil-works/pi-ai
1815
2163
  * 的目录,按文件路径动态 import——与 dsh 官方插件共享同一模块实例;
1816
- * 2. vendored:回退到本插件 dependencies 里的固定版本副本(裸 import)。
2164
+ * 2. vendored:回退到本插件 devDependencies 里的副本(裸 import)。
2165
+ *
2166
+ * 两条路径产物都过形状自检(assertKitShape):上游重构导致形状漂移时抛错,
2167
+ * 由调用方决定回退或放弃注册——绝不让坏套件进入消息链路。
1817
2168
  *
1818
- * 两条路径产物都过形状自检(assertKitShape):上游重构导致形状漂移时
1819
- * 抛错,由调用方决定回退或放弃注册——绝不让坏套件进入消息链路。
2169
+ * **自动跟随(版本不参与分支)**:插件不认识"某个 pi-ai 版本",只认识形状与
2170
+ * 现场推导出的适配面。协议集合取自已装官方包的 `supportedProtocols()`,每个协议
2171
+ * 的惰性工厂按 `pi-ai/dist/api/<api>.lazy.js`(api id 即模块名,pi-ai 包导出约定)
2172
+ * 动态加载;缺失的协议逐项跳过并记诊断,其余协议照常。compat 门控/取值约束与
2173
+ * 模型条目字段集同样现场推导(official-surface.ts)。生效版本只用于状态行显示,
2174
+ * 超出已验证区间也只是提示(kit-meta.ts),永不阻断。
1820
2175
  *
1821
2176
  * 0.1.2-alpha 线的新面(0.1.2-alpha.2 复核包根仍未导出、仅 src 子路径有):
1822
2177
  * - resolveProfiles(config.ts):官方解析链,dev 布局树可经 src 子路径复用;
@@ -1843,7 +2198,9 @@ function assertKitShape(kit, origin) {
1843
2198
  if (typeof kit.createProvider !== "function") problems.push("pi-ai createProvider 缺失");
1844
2199
  if (typeof kit.getBuiltinModels !== "function") problems.push("pi-ai getBuiltinModels 缺失");
1845
2200
  if (typeof kit.builtinProviders !== "function") problems.push("pi-ai builtinProviders 缺失");
1846
- for (const [api, factory] of Object.entries(kit.protocolFactories)) if (typeof factory !== "function") problems.push(`协议工厂 ${api} 缺失`);
2201
+ if (kit.protocols.length === 0) problems.push("没有可服务的线协议(协议工厂全部加载失败)");
2202
+ for (const api of kit.protocols) if (typeof kit.protocolFactories[api] !== "function") problems.push(`协议 ${api} 的工厂缺失`);
2203
+ if (kit.officialModelFields.length === 0) problems.push("官方模型条目字段集为空");
1847
2204
  if (typeof kit.LlmError !== "function") problems.push("dsh-llm LlmError 缺失");
1848
2205
  if (typeof kit.resolveRetryPolicy !== "function") problems.push("dsh-llm resolveRetryPolicy 缺失");
1849
2206
  if (typeof kit.resolveProfiles !== "function") problems.push("resolveProfiles 缺失");
@@ -1872,27 +2229,21 @@ function findDshTreeRoot(startDir) {
1872
2229
  dir = parent;
1873
2230
  }
1874
2231
  }
1875
- /** 从 dsh 安装树按文件路径动态 import 全部套件模块(与官方插件同实例)。 */
2232
+ /** 从 dsh 安装树按文件路径动态 import 核心套件模块(与官方插件同实例)。 */
1876
2233
  async function importTreeModules(root) {
1877
2234
  const nm = join(root, "node_modules");
1878
2235
  const load = (absPath) => import(pathToFileURL(absPath).href);
1879
- const [piAiAdapter, llm, piAi, catalog, completions, responses, anthropic] = await Promise.all([
2236
+ const [piAiAdapter, llm, piAi, catalog] = await Promise.all([
1880
2237
  load(join(nm, "@deepseek-ai", "dsh-llm-pi-ai", "lib", "index.js")),
1881
2238
  load(join(nm, "@deepseek-ai", "dsh-llm", "lib", "index.js")),
1882
2239
  load(join(nm, "@earendil-works", "pi-ai", "dist", "index.js")),
1883
- load(join(nm, "@earendil-works", "pi-ai", "dist", "providers", "all.js")),
1884
- load(join(nm, "@earendil-works", "pi-ai", "dist", "api", "openai-completions.lazy.js")),
1885
- load(join(nm, "@earendil-works", "pi-ai", "dist", "api", "openai-responses.lazy.js")),
1886
- load(join(nm, "@earendil-works", "pi-ai", "dist", "api", "anthropic-messages.lazy.js"))
2240
+ load(join(nm, "@earendil-works", "pi-ai", "dist", "providers", "all.js"))
1887
2241
  ]);
1888
2242
  return {
1889
2243
  piAiAdapter,
1890
2244
  llm,
1891
2245
  piAi,
1892
- catalog,
1893
- completions,
1894
- responses,
1895
- anthropic
2246
+ catalog
1896
2247
  };
1897
2248
  }
1898
2249
  /**
@@ -1932,58 +2283,153 @@ async function importTreeDeepseek(root) {
1932
2283
  return { problem: error instanceof Error ? error.message : String(error) };
1933
2284
  }
1934
2285
  }
1935
- function protocolFactoriesOf(mods) {
2286
+ /**
2287
+ * 取惰性协议模块里的工厂:约定每个 `api/<api>.lazy.js` 只导出一个 `*Api`
2288
+ * 工厂函数。名字不符时退化为"唯一函数导出",多函数且无 `*Api` 命名则判失败
2289
+ * (宁可跳过该协议,也不猜错工厂)。
2290
+ */
2291
+ function protocolFactoryOf(module) {
2292
+ const functions = Object.entries(module).filter((entry) => typeof entry[1] === "function");
2293
+ const named = functions.filter(([name]) => /Api$/.test(name));
2294
+ if (named.length === 1) return named[0]?.[1];
2295
+ if (functions.length === 1) return functions[0]?.[1];
2296
+ }
2297
+ /** pi-ai 的惰性协议实现目录(dsh 树布局)。 */
2298
+ function piAiApiDir(root) {
2299
+ return join(root, "node_modules", "@earendil-works", "pi-ai", "dist", "api");
2300
+ }
2301
+ /** 内置三元组的静态加载(协议推导失败或全部工厂加载失败时的兜底)。 */
2302
+ async function loadFallbackTreeFactories(root, diagnostics) {
2303
+ const dir = piAiApiDir(root);
2304
+ const out = {};
2305
+ for (const api of fallbackProtocols()) {
2306
+ const file = join(dir, `${api}.lazy.js`);
2307
+ if (!existsSync(file)) continue;
2308
+ try {
2309
+ const factory = protocolFactoryOf(await import(pathToFileURL(file).href));
2310
+ if (factory !== void 0) out[api] = factory;
2311
+ } catch {}
2312
+ }
2313
+ if (Object.keys(out).length < fallbackProtocols().length) diagnostics.push(`pi-ai 协议兜底三元组未能全部加载(${Object.keys(out).join("/") || "空"});请检查 dsh 安装树里 @earendil-works/pi-ai/dist/api 的形态`);
2314
+ return out;
2315
+ }
2316
+ /**
2317
+ * 按官方声明的协议集合加载 pi-ai 惰性工厂(api id 即模块名)。
2318
+ * 逐项失败只跳过该协议并记诊断;一个都加载不到时回退内置三元组。
2319
+ */
2320
+ async function loadTreeProtocols(root, derived, diagnostics) {
2321
+ const dir = piAiApiDir(root);
2322
+ const out = {};
2323
+ for (const api of derived) {
2324
+ const file = join(dir, `${api}.lazy.js`);
2325
+ if (!existsSync(file)) {
2326
+ diagnostics.push(`pi-ai 未提供协议 "${api}" 的惰性实现(缺 ${file});该协议不可用,其余协议不受影响`);
2327
+ continue;
2328
+ }
2329
+ try {
2330
+ const factory = protocolFactoryOf(await import(pathToFileURL(file).href));
2331
+ if (factory === void 0) {
2332
+ diagnostics.push(`协议 "${api}" 的实现模块未导出唯一 *Api 工厂;该协议不可用`);
2333
+ continue;
2334
+ }
2335
+ out[api] = factory;
2336
+ } catch (error) {
2337
+ diagnostics.push(`协议 "${api}" 的实现加载失败(${error instanceof Error ? error.message : String(error)});该协议不可用`);
2338
+ }
2339
+ }
2340
+ if (Object.keys(out).length > 0) return {
2341
+ protocols: derived.filter((api) => out[api] !== void 0),
2342
+ protocolFactories: out
2343
+ };
2344
+ diagnostics.push("官方声明的协议一个都加载不到;回退内置三元组");
2345
+ const fallback = await loadFallbackTreeFactories(root, diagnostics);
1936
2346
  return {
1937
- "openai-completions": mods.completions["openAICompletionsApi"],
1938
- "openai-responses": mods.responses["openAIResponsesApi"],
1939
- "anthropic-messages": mods.anthropic["anthropicMessagesApi"]
2347
+ protocols: fallbackProtocols().filter((api) => fallback[api] !== void 0),
2348
+ protocolFactories: fallback
1940
2349
  };
1941
2350
  }
2351
+ function protocolFactoriesOf(protocols, factories) {
2352
+ const out = {};
2353
+ for (const api of protocols) {
2354
+ const factory = factories[api];
2355
+ if (factory !== void 0) out[api] = factory;
2356
+ }
2357
+ return out;
2358
+ }
1942
2359
  /** 插件等价解析链的套件依赖(与 PiAiAdapter 同源,杜绝跨源混用)。 */
1943
- function treeResolverDeps(mods, protocolFactories) {
2360
+ function treeResolverDeps(mods, protocolFactories, protocols) {
1944
2361
  return {
1945
2362
  createProvider: mods.piAi["createProvider"],
1946
2363
  protocolFactories,
2364
+ protocols,
1947
2365
  resolveRetryPolicy: mods.llm["resolveRetryPolicy"]
1948
2366
  };
1949
2367
  }
2368
+ /** 读内置目录数据生成时间(官方未导出/形状变化时省略)。 */
2369
+ function catalogGeneratedAtOf(catalog) {
2370
+ const reader = catalog["getBuiltinModelDataGeneratedAt"];
2371
+ if (typeof reader !== "function") return void 0;
2372
+ try {
2373
+ const value = reader();
2374
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2375
+ } catch {
2376
+ return;
2377
+ }
2378
+ }
1950
2379
  /**
1951
- * 从官方安装副本推导 compat 门控表(bundle 文本给分型、Config schema 给取值约束),
1952
- * 并安装为插件生效表。任一环节失败回退 FALLBACK_TABLE 并给诊断——绝不因推导失败
1953
- * 弄挂插件启动(宁可放宽校验,也不误拒官方可配字段)。
2380
+ * 从官方安装副本推导适配面并安装 compat 生效表(bundle 文本给分型、Config schema
2381
+ * 给取值约束与模型条目字段集、supportedProtocols() 给协议集合)。
2382
+ * 任一环节失败都逐项回退并记诊断——绝不因推导失败弄挂插件启动。
1954
2383
  */
1955
- function installOfficialCompatTable(bundlePath, module, diagnostics) {
1956
- const label = "compat 门控表";
2384
+ function installOfficialSurface(options) {
2385
+ const { bundlePath, adapterModule, origin, diagnostics } = options;
2386
+ const compatLabel = "compat 门控表";
1957
2387
  try {
1958
2388
  const gates = deriveGates(readFileSync(bundlePath, "utf8"));
1959
2389
  if (gates === void 0) {
1960
- diagnostics.push(`${label}未能从官方 bundle 解析 COMPAT_GATES(${bundlePath});使用内置快照(官方新增字段可能被误拒,请检查 dsh-llm-pi-ai 打包形态)`);
2390
+ diagnostics.push(`${compatLabel}未能从官方 bundle 解析 COMPAT_GATES(${bundlePath});使用内置快照(未知字段放行、withhold 仍拒绝;请检查 dsh-llm-pi-ai 打包形态)`);
1961
2391
  installCompatTable(FALLBACK_TABLE);
1962
- return;
1963
- }
1964
- installCompatTable({
2392
+ } else installCompatTable({
1965
2393
  gates,
1966
2394
  specs: {
1967
2395
  ...FALLBACK_TABLE.specs,
1968
- ...deriveSpecs(module)
2396
+ ...deriveSpecs(adapterModule)
1969
2397
  },
1970
2398
  source: "official"
1971
2399
  });
1972
2400
  } catch (error) {
1973
2401
  const reason = error instanceof Error ? error.message : String(error);
1974
- diagnostics.push(`${label}推导失败(${reason});使用内置快照`);
2402
+ diagnostics.push(`${compatLabel}推导失败(${reason});使用内置快照(未知字段放行)`);
1975
2403
  installCompatTable(FALLBACK_TABLE);
1976
2404
  }
2405
+ const derivedProtocols = deriveProtocols(adapterModule);
2406
+ if (derivedProtocols === void 0) diagnostics.push(`${origin}:官方 supportedProtocols() 不可用;协议集合回退内置三元组 ${fallbackProtocols().join("/")}`);
2407
+ const fields = deriveModelEntryFields(adapterModule);
2408
+ if (fields === void 0) diagnostics.push(`${origin}:官方 Config schema 的模型条目字段集不可推导;继承字段回退已知键 ${FALLBACK_MODEL_ENTRY_FIELDS.join("/")}(官方新增字段可能不被继承)`);
2409
+ const generatedAt = catalogGeneratedAtOf(options.catalogModule);
2410
+ return {
2411
+ protocols: derivedProtocols ?? fallbackProtocols(),
2412
+ protocolSource: derivedProtocols === void 0 ? "fallback" : "official",
2413
+ officialModelFields: fields ?? [...FALLBACK_MODEL_ENTRY_FIELDS],
2414
+ ...generatedAt === void 0 ? {} : { catalogGeneratedAt: generatedAt }
2415
+ };
1977
2416
  }
1978
- function kitFromTree(mods, auth, config) {
1979
- const protocolFactories = protocolFactoriesOf(mods);
1980
- const resolverDeps = treeResolverDeps(mods, protocolFactories);
1981
- const officialResolveProfiles = config?.["resolveProfiles"];
1982
- const officialCredentialStoreFrom = auth?.["credentialStoreFrom"];
1983
- const officialAuthContextFrom = auth?.["authContextFrom"];
2417
+ function kitFromTree(options) {
2418
+ const { mods, surface, protocols, protocolFactories, root, versions } = options;
2419
+ const resolverDeps = treeResolverDeps(mods, protocolFactories, protocols);
2420
+ const officialResolveProfiles = options.config?.["resolveProfiles"];
2421
+ const officialCredentialStoreFrom = options.auth?.["credentialStoreFrom"];
2422
+ const officialAuthContextFrom = options.auth?.["authContextFrom"];
1984
2423
  const recordKeyFor = mods.piAiAdapter["recordKeyFor"];
1985
2424
  return {
1986
2425
  source: "dsh-tree",
2426
+ root,
2427
+ versions,
2428
+ protocols,
2429
+ protocolSource: surface.protocolSource,
2430
+ protocolFactories,
2431
+ officialModelFields: surface.officialModelFields,
2432
+ ...surface.catalogGeneratedAt === void 0 ? {} : { catalogGeneratedAt: surface.catalogGeneratedAt },
1987
2433
  PiAiAdapter: mods.piAiAdapter["PiAiAdapter"],
1988
2434
  LlmError: mods.llm["LlmError"],
1989
2435
  resolveRetryPolicy: mods.llm["resolveRetryPolicy"],
@@ -1995,7 +2441,6 @@ function kitFromTree(mods, auth, config) {
1995
2441
  builtinProviders: mods.catalog["builtinProviders"],
1996
2442
  getBuiltinProviders: mods.catalog["getBuiltinProviders"],
1997
2443
  getBuiltinModels: mods.catalog["getBuiltinModels"],
1998
- protocolFactories,
1999
2444
  resolveProfiles: officialResolveProfiles !== void 0 ? officialResolveProfiles : (providers) => resolveProfilesFallback(providers, resolverDeps),
2000
2445
  auth: {
2001
2446
  credentialStoreFrom: officialCredentialStoreFrom !== void 0 ? officialCredentialStoreFrom : (ctx) => credentialStoreFrom(ctx, recordKeyFor),
@@ -2021,22 +2466,48 @@ function loadVendoredDeepseek() {
2021
2466
  function vendoredAdapterBundlePath() {
2022
2467
  return fileURLToPath(import.meta.resolve("@deepseek-ai/dsh-llm-pi-ai"));
2023
2468
  }
2469
+ /** vendored 副本的静态协议工厂表(devDependencies 钉版,故无需动态加载)。 */
2470
+ function vendoredProtocolFactories() {
2471
+ return {
2472
+ "openai-completions": openAICompletionsApi,
2473
+ "openai-responses": openAIResponsesApi,
2474
+ "anthropic-messages": anthropicMessagesApi
2475
+ };
2476
+ }
2024
2477
  /**
2025
2478
  * vendored 兜底副本套件(导出供单测直接使用,免走 dsh 树解析)。
2479
+ * 副本是**钉版**的,故协议工厂走静态导入,协议集合取"官方声明 ∩ 静态工厂";
2026
2480
  * npm 发布形态不携带 src/、lib 仅 index.js/invariant.js——resolveProfiles 与
2027
2481
  * auth 助手在此无条件走插件等价实现(内联,见文件头说明);compat 门控表则
2028
2482
  * 仍从 vendored 副本的 bundle + Config schema 现场推导(与 dsh 树同一推导链)。
2483
+ * @param diagnostics 可选诊断收集器(回退原因与逐项降级说明)。
2029
2484
  */
2030
- function loadVendoredKit() {
2031
- const deepseek = loadVendoredDeepseek();
2032
- const protocolFactories = {
2033
- "openai-completions": openAICompletionsApi,
2034
- "openai-responses": openAIResponsesApi,
2035
- "anthropic-messages": anthropicMessagesApi
2485
+ function loadVendoredKit(diagnostics = []) {
2486
+ const staticFactories = vendoredProtocolFactories();
2487
+ const surface = installOfficialSurface({
2488
+ bundlePath: vendoredAdapterBundlePath(),
2489
+ adapterModule: vendoredPiAiAdapter,
2490
+ catalogModule: vendoredCatalog,
2491
+ origin: "vendored 副本",
2492
+ diagnostics
2493
+ });
2494
+ const protocols = surface.protocols.filter((api) => staticFactories[api] !== void 0);
2495
+ const protocolFactories = protocolFactoriesOf(protocols, staticFactories);
2496
+ const resolverDeps = {
2497
+ createProvider: vendoredPiAi.createProvider,
2498
+ protocolFactories,
2499
+ protocols,
2500
+ resolveRetryPolicy: vendoredLlm.resolveRetryPolicy
2036
2501
  };
2037
- installOfficialCompatTable(vendoredAdapterBundlePath(), vendoredPiAiAdapter, []);
2502
+ const deepseek = loadVendoredDeepseek();
2038
2503
  const kit = {
2039
2504
  source: "vendored",
2505
+ versions: collectVendoredVersions(),
2506
+ protocols,
2507
+ protocolSource: protocols.length > 0 ? surface.protocolSource : "fallback",
2508
+ protocolFactories,
2509
+ officialModelFields: surface.officialModelFields,
2510
+ ...surface.catalogGeneratedAt === void 0 ? {} : { catalogGeneratedAt: surface.catalogGeneratedAt },
2040
2511
  PiAiAdapter: vendoredPiAiAdapter.PiAiAdapter,
2041
2512
  LlmError: vendoredLlm.LlmError,
2042
2513
  resolveRetryPolicy: vendoredLlm.resolveRetryPolicy,
@@ -2048,12 +2519,7 @@ function loadVendoredKit() {
2048
2519
  builtinProviders: vendoredCatalog.builtinProviders,
2049
2520
  getBuiltinProviders: vendoredCatalog.getBuiltinProviders,
2050
2521
  getBuiltinModels: vendoredCatalog.getBuiltinModels,
2051
- protocolFactories,
2052
- resolveProfiles: (providers) => resolveProfilesFallback(providers, {
2053
- createProvider: vendoredPiAi.createProvider,
2054
- protocolFactories,
2055
- resolveRetryPolicy: vendoredLlm.resolveRetryPolicy
2056
- }),
2522
+ resolveProfiles: (providers) => resolveProfilesFallback(providers, resolverDeps),
2057
2523
  auth: {
2058
2524
  credentialStoreFrom: (ctx) => credentialStoreFrom(ctx, vendoredPiAiAdapter.recordKeyFor),
2059
2525
  authContextFrom
@@ -2085,7 +2551,7 @@ function dshTreeAnchor(installAnchor, argvEntry = process.argv[1]) {
2085
2551
  /**
2086
2552
  * 解析运行时套件:优先 dsh 安装树(自动跟随上游),失败回退 vendored 副本;
2087
2553
  * 两者都过不了形状自检时抛错(调用方应记日志并放弃注册 route)。
2088
- * 返回的 diagnostics 记录回退原因,供配置卡片与日志展示。
2554
+ * 返回的 diagnostics 记录回退原因与逐项降级说明,供配置卡片与日志展示。
2089
2555
  * @param installAnchor profile 的 dsh 安装锚点(`ctx.profileContext.installAnchor`)。
2090
2556
  */
2091
2557
  async function resolveDshKit(installAnchor) {
@@ -2095,10 +2561,26 @@ async function resolveDshKit(installAnchor) {
2095
2561
  const treePkgDir = join(anchor, "node_modules", "@deepseek-ai", "dsh-llm-pi-ai");
2096
2562
  try {
2097
2563
  const mods = await importTreeModules(anchor);
2564
+ const surface = installOfficialSurface({
2565
+ bundlePath: join(treePkgDir, "lib", "index.js"),
2566
+ adapterModule: mods.piAiAdapter,
2567
+ catalogModule: mods.catalog,
2568
+ origin: "dsh 安装树",
2569
+ diagnostics
2570
+ });
2571
+ const { protocols, protocolFactories } = await loadTreeProtocols(anchor, surface.protocols, diagnostics);
2098
2572
  const [auth, config] = await Promise.all([probeSubmodule(treePkgDir, "auth"), probeSubmodule(treePkgDir, "config")]);
2099
- const kit = kitFromTree(mods, auth, config);
2573
+ const kit = kitFromTree({
2574
+ mods,
2575
+ auth,
2576
+ config,
2577
+ surface,
2578
+ protocols,
2579
+ protocolFactories,
2580
+ root: anchor,
2581
+ versions: collectTreeVersions(anchor)
2582
+ });
2100
2583
  assertKitShape(kit, "dsh-tree");
2101
- installOfficialCompatTable(join(treePkgDir, "lib", "index.js"), mods.piAiAdapter, diagnostics);
2102
2584
  if (auth === void 0) diagnostics.push("dsh 树不含 dsh-llm-pi-ai/src(npm 发布形态);认证助手使用插件内联等价实现");
2103
2585
  if (config === void 0) diagnostics.push("dsh 树不含 dsh-llm-pi-ai/src(npm 发布形态);profile 解析使用插件等价实现(compat 门控对齐官方 catalog.ts)");
2104
2586
  const tree = await importTreeDeepseek(anchor);
@@ -2118,7 +2600,7 @@ async function resolveDshKit(installAnchor) {
2118
2600
  diagnostics.push(`dsh 安装树套件不可用(${anchor}):${error instanceof Error ? error.message : String(error)};回退 vendored 副本`);
2119
2601
  }
2120
2602
  } else diagnostics.push("未能从 profileContext.installAnchor / process.argv[1] 定位 dsh 安装树;回退 vendored 副本");
2121
- const kit = loadVendoredKit();
2603
+ const kit = loadVendoredKit(diagnostics);
2122
2604
  if (kit.deepseek === void 0) diagnostics.push("vendored 副本的 dsh-llm-deepseek 形状不兼容;adapter: deepseek 的 route 不可用");
2123
2605
  return {
2124
2606
  kit,
@@ -2146,31 +2628,32 @@ function makeResolveApiKey(ctx, kit) {
2146
2628
  throw new kit.LlmError(`llm-pi: provider route "${provider}" 的凭据引用 ${String(ref)} 未解析到值——请经凭据服务(web Models 页)存放或导出环境变量;仅当该 provider 应使用 pi-ai 自有环境发现时才移除 apiKeyEnv`, "MISSING_CREDENTIAL");
2147
2629
  };
2148
2630
  }
2149
- /** 启动插件运行时:解析套件、挂载注册/发现/settings 联动。 */
2150
- async function startRuntime(ctx, rawConfig) {
2151
- const logger = ctx.logger("llm-pi");
2152
- const fields = hasVolatileRefs(rawConfig) ? rawConfig : Config(rawConfig ?? {});
2631
+ /** 解析运行期套件并落启动日志(来源/生效版本/协议/逐项降级诊断)。 */
2632
+ async function loadRuntimeKit(ctx, logger) {
2153
2633
  const { kit, diagnostics } = await resolveDshKit(ctx.get("profileContext")?.installAnchor);
2154
2634
  for (const line of diagnostics) logger.warn(line);
2155
- logger.info(`运行时套件来源:${kit.source}`);
2156
- let snapshot = unwrapVolatile(fields);
2157
- const modelsDev = new ModelsDevSource(pluginDataPath("llm-pi", "models-dev.json"), snapshot.catalogUrl, snapshot.catalogRefreshHours, (message) => logger.warn(message), snapshot.catalogProxy ?? "");
2158
- modelsDev.ensureLoaded();
2159
- const current = () => snapshot;
2635
+ logger.info(`运行时套件来源:${kit.source};pi-ai ${kit.versions.piAi ?? "?"} / dsh-llm-pi-ai ${kit.versions.piAiAdapter ?? "?"};协议 ${kit.protocols.join("/")}`);
2636
+ const versionNotice = piAiVersionNotice(kit.versions.piAi);
2637
+ if (versionNotice !== void 0) logger.warn(versionNotice);
2638
+ return {
2639
+ kit,
2640
+ diagnostics
2641
+ };
2642
+ }
2643
+ /**
2644
+ * profiles / deepseek 路由解析器:按原始 config 对象 identity 备忘(官方同款模式)。
2645
+ * 运行期走 lenient:目录漂移时降级/跳过并告警,而非抛错弄挂整个 route。
2646
+ */
2647
+ function createProfileResolvers(options) {
2648
+ const { kit, current, logger } = options;
2160
2649
  let lastRaw;
2161
2650
  let memoized;
2162
2651
  let memoizedDeepseek;
2163
- const deps = {
2164
- kit,
2165
- modelsDev
2166
- };
2167
- /** 当前已解析 profiles,按原始 config identity 备忘(官方同款模式)。
2168
- * 运行期走 lenient:数据源漂移时降级/跳过并告警,而非抛错弄挂整个 route。 */
2169
2652
  const profiles = () => {
2170
2653
  const raw = current();
2171
2654
  if (raw === lastRaw && memoized !== void 0) return memoized;
2172
2655
  const next = raw.enabled ? buildProfiles(raw.providers, {
2173
- ...deps,
2656
+ kit,
2174
2657
  lenient: true,
2175
2658
  warn: (message) => logger.warn(message)
2176
2659
  }) : /* @__PURE__ */ new Map();
@@ -2183,13 +2666,19 @@ async function startRuntime(ctx, rawConfig) {
2183
2666
  memoized = next;
2184
2667
  return next;
2185
2668
  };
2186
- /** deepseek 路由物化表(与 profiles 同一次备忘窗口)。 */
2187
2669
  const deepseekRoutes = () => {
2188
2670
  profiles();
2189
2671
  return memoizedDeepseek ?? /* @__PURE__ */ new Map();
2190
2672
  };
2191
- profiles();
2192
- const adapter = new kit.PiAiAdapter({
2673
+ return {
2674
+ profiles,
2675
+ deepseekRoutes
2676
+ };
2677
+ }
2678
+ /** 构造 PiAiAdapter(官方给出的接缝全部接上)。 */
2679
+ function createAdapter(options) {
2680
+ const { ctx, kit, profiles, logger } = options;
2681
+ return new kit.PiAiAdapter({
2193
2682
  profiles,
2194
2683
  resolveApiKey: makeResolveApiKey(ctx, kit),
2195
2684
  resolveAttachments: () => ctx.get("attachments"),
@@ -2202,17 +2691,14 @@ async function startRuntime(ctx, rawConfig) {
2202
2691
  logger.warn(`llm-pi: 助手历史中 "${provider}/${model}" 的回放状态不可用,该消息按 provider 中立内容发送(${reason})`);
2203
2692
  }
2204
2693
  });
2205
- const storedApiKey = async (provider) => {
2206
- if (provider === void 0) return void 0;
2207
- const profile = profiles().get(provider);
2208
- if (profile === void 0) return void 0;
2209
- return makeResolveApiKey(ctx, kit)(provider, profile);
2210
- };
2211
- ctx.llm.registerModelDiscovery(SETTINGS_NS, (request) => discoverModels(request, {
2212
- kit,
2213
- configProviders: () => current().providers ?? {},
2214
- storedApiKey
2215
- }));
2694
+ }
2695
+ /**
2696
+ * 注册 handle 组。正常路径单个 handle 整批注册/替换;整批注册遇
2697
+ * DUPLICATE_ADAPTER(route 名与其他 adapter 冲突)时降级为逐个注册,
2698
+ * 跳过冲突 route——启动不再 fail-loud,其余 route 照常服务。
2699
+ */
2700
+ function createRegistrationController(options) {
2701
+ const { ctx, adapter, profiles, logger } = options;
2216
2702
  let registrations;
2217
2703
  let registeredFacts;
2218
2704
  const registerGroup = (routes, fallback) => {
@@ -2238,10 +2724,10 @@ async function startRuntime(ctx, rawConfig) {
2238
2724
  }
2239
2725
  };
2240
2726
  const ensureRegistration = () => {
2241
- const current2 = profiles();
2242
- const facts = registrationFacts(current2);
2727
+ const resolved = profiles();
2728
+ const facts = registrationFacts(resolved);
2243
2729
  if (deepEqualJson(facts, registeredFacts)) return;
2244
- const routes = [...current2.keys()];
2730
+ const routes = [...resolved.keys()];
2245
2731
  if (registrations === void 0) {
2246
2732
  if (routes.length === 0) {
2247
2733
  registeredFacts = facts;
@@ -2266,9 +2752,16 @@ async function startRuntime(ctx, rawConfig) {
2266
2752
  }
2267
2753
  registeredFacts = facts;
2268
2754
  };
2755
+ return { ensureRegistration };
2756
+ }
2757
+ /**
2758
+ * 可配置 provider 目录(pi 路由 + deepseek 路由 + 草稿路由)。
2759
+ * 草稿路由从原始配置直读——它们不进 adapter profiles。
2760
+ */
2761
+ function createDirectoryController(options) {
2762
+ const { ctx, kit, current, profiles, deepseekRoutes, logger } = options;
2269
2763
  let directory;
2270
2764
  let directoryFacts;
2271
- /** 草稿路由(无模型占位)从原始配置直读——它们不进 adapter profiles。 */
2272
2765
  const draftRoutes = () => {
2273
2766
  const providers = current().providers ?? {};
2274
2767
  return Object.entries(providers).filter(([, profile]) => isDraftRoute(profile)).map(([route, profile]) => ({
@@ -2289,6 +2782,62 @@ async function startRuntime(ctx, rawConfig) {
2289
2782
  }, entries, (message) => logger.warn(message));
2290
2783
  directoryFacts = entries;
2291
2784
  };
2785
+ return { ensureDirectory };
2786
+ }
2787
+ /** 跑一步热更新动作,失败只告警(保留此前注册/目录)。 */
2788
+ function attempt(logger, message, run) {
2789
+ try {
2790
+ run();
2791
+ } catch (error) {
2792
+ logger.error(message);
2793
+ logger.error(error);
2794
+ }
2795
+ }
2796
+ /** 启动插件运行时:解析套件、挂载注册/发现/settings 联动。 */
2797
+ async function startRuntime(ctx, rawConfig) {
2798
+ const logger = ctx.logger("llm-pi");
2799
+ const fields = hasVolatileRefs(rawConfig) ? rawConfig : Config(rawConfig ?? {});
2800
+ const { kit, diagnostics } = await loadRuntimeKit(ctx, logger);
2801
+ let snapshot = unwrapVolatile(fields);
2802
+ const current = () => snapshot;
2803
+ const deps = { kit };
2804
+ const { profiles, deepseekRoutes } = createProfileResolvers({
2805
+ kit,
2806
+ current,
2807
+ logger
2808
+ });
2809
+ profiles();
2810
+ const adapter = createAdapter({
2811
+ ctx,
2812
+ kit,
2813
+ profiles,
2814
+ logger
2815
+ });
2816
+ const storedApiKey = async (provider) => {
2817
+ if (provider === void 0) return void 0;
2818
+ const profile = profiles().get(provider);
2819
+ if (profile === void 0) return void 0;
2820
+ return makeResolveApiKey(ctx, kit)(provider, profile);
2821
+ };
2822
+ ctx.llm.registerModelDiscovery(SETTINGS_NS, (request) => discoverModels(request, {
2823
+ kit,
2824
+ configProviders: () => current().providers ?? {},
2825
+ storedApiKey
2826
+ }));
2827
+ const { ensureRegistration } = createRegistrationController({
2828
+ ctx,
2829
+ adapter,
2830
+ profiles,
2831
+ logger
2832
+ });
2833
+ const { ensureDirectory } = createDirectoryController({
2834
+ ctx,
2835
+ kit,
2836
+ current,
2837
+ profiles,
2838
+ deepseekRoutes,
2839
+ logger
2840
+ });
2292
2841
  const deepseekRegistrar = new DeepseekRouteRegistrar({
2293
2842
  ctx,
2294
2843
  kit,
@@ -2304,26 +2853,9 @@ async function startRuntime(ctx, rawConfig) {
2304
2853
  ensureDirectory();
2305
2854
  const reconfigure = () => {
2306
2855
  snapshot = unwrapVolatile(fields);
2307
- try {
2308
- ensureRegistration();
2309
- } catch (error) {
2310
- logger.error("llm-pi: 更新被拒,保留此前注册的 route");
2311
- logger.error(error);
2312
- }
2313
- try {
2314
- ensureDeepseek();
2315
- } catch (error) {
2316
- logger.error("llm-pi: deepseek route 更新失败,保留此前注册");
2317
- logger.error(error);
2318
- }
2319
- try {
2320
- ensureDirectory();
2321
- } catch (error) {
2322
- logger.error("llm-pi: 更新被拒,保留此前的 configurable-provider 目录");
2323
- logger.error(error);
2324
- }
2325
- const cfg = current();
2326
- modelsDev.reconfigure(cfg.catalogUrl, cfg.catalogRefreshHours, cfg.catalogProxy ?? "");
2856
+ attempt(logger, "llm-pi: 更新被拒,保留此前注册的 route", ensureRegistration);
2857
+ attempt(logger, "llm-pi: deepseek route 更新失败,保留此前注册", ensureDeepseek);
2858
+ attempt(logger, "llm-pi: 更新被拒,保留此前的 configurable-provider 目录", ensureDirectory);
2327
2859
  };
2328
2860
  ctx.events.on("loader/volatile-update", reconfigure);
2329
2861
  ctx.on("internal/config", function(_raw, next) {
@@ -2332,13 +2864,33 @@ async function startRuntime(ctx, rawConfig) {
2332
2864
  assertServiceable(unwrapVolatile(Config(candidate)), deps);
2333
2865
  return candidate;
2334
2866
  });
2335
- return {
2336
- currentConfig: () => current(),
2337
- kitInfo: () => ({
2867
+ /** 目录规模:按需从当前套件的 provider/模型清单汇总(不缓存,随套件固定)。 */
2868
+ const kitInfo = () => {
2869
+ const providers = listProviders(kit);
2870
+ const generatedAt = kit.catalogGeneratedAt;
2871
+ const notice = piAiVersionNotice(kit.versions.piAi);
2872
+ return {
2338
2873
  source: kit.source,
2874
+ ...kit.root === void 0 ? {} : { root: kit.root },
2875
+ versions: kit.versions,
2876
+ verifiedRange: VERIFIED_PI_AI_RANGE,
2877
+ ...notice === void 0 ? {} : { versionNotice: notice },
2878
+ protocols: kit.protocols,
2879
+ protocolSource: kit.protocolSource,
2880
+ catalog: {
2881
+ providers: providers.length,
2882
+ models: providers.reduce((total, entry) => total + entry.modelCount, 0),
2883
+ ...generatedAt === void 0 ? {} : { generatedAt }
2884
+ },
2885
+ compatSource: compatTableInfo().source,
2339
2886
  diagnostics
2340
- }),
2341
- modelsDev,
2887
+ };
2888
+ };
2889
+ return {
2890
+ currentConfig: () => current(),
2891
+ kitInfo,
2892
+ providerEntries: () => listProviders(kit),
2893
+ apiIndex: () => buildApiIndex(listModelInfos(kit)),
2342
2894
  kit
2343
2895
  };
2344
2896
  }