@dsh-plus/llm-pi 0.2.1 → 0.2.3

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 CHANGED
@@ -145,7 +145,7 @@ window.__ModuleLoader__.load({
145
145
  const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
146
146
  /**
147
147
  * 把一张配置卡片注册进插件页的两个配置槽位。
148
- * 两个槽位都由 0.2.1-alpha.1 的插件页声明,任一缺席即报错——不做版本探测。
148
+ * 两个槽位都由 0.2.1-alpha.2 的插件页声明,任一缺席即报错——不做版本探测。
149
149
  */
150
150
  function injectPluginConfigCard(slots, reg) {
151
151
  const base = {
@@ -314,7 +314,7 @@ window.__ModuleLoader__.load({
314
314
  /**
315
315
  * 配置分节样式工厂(各插件 client 的公共收编版)。
316
316
  *
317
- * 目标宿主 dsh 0.2.1-alpha.1:插件页把配置视图放进已有页面级内边距的容器里
317
+ * 目标宿主 dsh 0.2.1-alpha.2:插件页把配置视图放进已有页面级内边距的容器里
318
318
  * (`div.detailSections[data-plugin-config]` 或 `section.detailSection[data-plugin-config]`),
319
319
  * 外层无边框——故这里输出的是**无边框分节**(小节标题 13px/600、字段 12px 0、
320
320
  * 横向零内边距、页脚非 sticky),度量对齐官方设置页
@@ -1125,8 +1125,9 @@ ${extra}
1125
1125
  *
1126
1126
  * 状态行回答的是"我现在到底在跑哪一份 pi-ai"——插件按文件路径动态加载已装 dsh 树
1127
1127
  * 里的套件(见 resolve-dsh.ts),manifest 上写的版本**不是**生效版本。这里显示
1128
- * 生效版本、安装树、目录规模与数据日期、可服务协议、compat 表来源,以及逐项降级
1129
- * 诊断;版本超出验证区间时给出提示(提示而非阻断)。
1128
+ * 生效版本、安装树、目录规模与数据日期、可服务协议、compat 表来源,以及诊断
1129
+ * (按「形态说明 / 降级诊断」分块,避免恒定事实淹没真实降级);版本超出验证区间
1130
+ * 时给出提示(提示而非阻断)。
1130
1131
  * @module llm-pi/client/root-fields
1131
1132
  */
1132
1133
  /** 时间戳 → 本地日期(秒级精度足够)。 */
@@ -1154,6 +1155,21 @@ ${extra}
1154
1155
  if (copy.updatedAt === void 0) return t("officialCopyPending");
1155
1156
  return `${copy.path} · ${copy.routes} ${t("officialCopyRoutes")} · ${formatTime(copy.updatedAt, t("runtimeVersionUnknown"))}`;
1156
1157
  }
1158
+ /**
1159
+ * 诊断分块:形态说明与降级分开渲染——「官方 src 不随 npm 发布」这类恒定事实
1160
+ * 永不会消失,混在一起会把真正的降级淹掉。空块不渲染。
1161
+ */
1162
+ function DiagnosticBlock(props) {
1163
+ const messages = props.items.filter((item) => item.level === props.level).map((m) => m.message);
1164
+ if (messages.length === 0) return null;
1165
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1166
+ className: "lpc-catDetailLabel",
1167
+ children: props.label
1168
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1169
+ className: "lpc-catJson",
1170
+ children: messages.join("\n")
1171
+ })] });
1172
+ }
1157
1173
  function RootFields(props) {
1158
1174
  const { t, draft, disabled, kit, onEdit } = props;
1159
1175
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
@@ -1205,12 +1221,14 @@ ${extra}
1205
1221
  className: "lpc-invalid",
1206
1222
  children: `${t("runtimeNotice")}:${kit.versionNotice}`
1207
1223
  }) : null,
1208
- kit.diagnostics.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1209
- className: "lpc-catDetailLabel",
1210
- children: t("runtimeDiagnostics")
1211
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1212
- className: "lpc-catJson",
1213
- children: kit.diagnostics.join("\n")
1224
+ kit.diagnostics.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiagnosticBlock, {
1225
+ label: t("runtimeFormNotes"),
1226
+ level: "info",
1227
+ items: kit.diagnostics
1228
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiagnosticBlock, {
1229
+ label: t("runtimeDiagnostics"),
1230
+ level: "degradation",
1231
+ items: kit.diagnostics
1214
1232
  })] }) : null
1215
1233
  ] })
1216
1234
  ] });
@@ -2866,7 +2884,8 @@ ${extra}
2866
2884
  officialCopyPending: "尚未生成",
2867
2885
  officialCopyFailed: "生成失败:",
2868
2886
  officialCopyRoutes: "个 route",
2869
- runtimeDiagnostics: "诊断",
2887
+ runtimeFormNotes: "形态说明",
2888
+ runtimeDiagnostics: "降级诊断",
2870
2889
  runtimeVersionUnknown: "未知",
2871
2890
  runtimeCatalogUnit: "个 provider /",
2872
2891
  runtimeCatalogModels: "个模型",
@@ -3000,7 +3019,8 @@ ${extra}
3000
3019
  officialCopyPending: "not generated yet",
3001
3020
  officialCopyFailed: "generation failed: ",
3002
3021
  officialCopyRoutes: "routes",
3003
- runtimeDiagnostics: "Diagnostics",
3022
+ runtimeFormNotes: "Packaging notes",
3023
+ runtimeDiagnostics: "Degradations",
3004
3024
  runtimeVersionUnknown: "unknown",
3005
3025
  runtimeCatalogUnit: "providers /",
3006
3026
  runtimeCatalogModels: "models",
package/lib/index.js CHANGED
@@ -650,7 +650,7 @@ function fallbackProtocols() {
650
650
  return [...FALLBACK_PROTOCOLS];
651
651
  }
652
652
  /**
653
- * 官方门控表不可解析时的兜底快照(0.2.1-alpha.1 现场推导值)。
653
+ * 官方门控表不可解析时的兜底快照(0.2.1-alpha.2 现场推导值)。
654
654
  *
655
655
  * 只在官方改打包形态、`COMPAT_GATES` 解析失败时启用(此时未知键放行、只拦快照里
656
656
  * 明确 withhold 的键,见 compat.ts 的「语义分两档」)。快照过期会重新引入
@@ -1337,7 +1337,7 @@ async function discoverModels(request, deps) {
1337
1337
  * @module llm-pi/kit-meta
1338
1338
  */
1339
1339
  /** 本插件开发/测试覆盖过的 pi-ai 区间(semver range,仅用于状态行提示)。 */
1340
- const VERIFIED_PI_AI_RANGE = ">=0.85.1 <0.88.0";
1340
+ const VERIFIED_PI_AI_RANGE = ">=1.0.2 <2.0.0";
1341
1341
  /** 读 package.json 的 version;文件不存在/非 JSON/无 version 都返回 undefined。 */
1342
1342
  function readPackageVersion(manifestPath) {
1343
1343
  try {
@@ -1409,13 +1409,13 @@ function collectVendoredVersions() {
1409
1409
  }
1410
1410
  /** 已验证区间的解析端点(与 {@link VERIFIED_PI_AI_RANGE} 同步维护)。 */
1411
1411
  const VERIFIED_MIN = [
1412
+ 1,
1412
1413
  0,
1413
- 85,
1414
- 1
1414
+ 2
1415
1415
  ];
1416
1416
  const VERIFIED_MAX_EXCLUSIVE = [
1417
+ 2,
1417
1418
  0,
1418
- 88,
1419
1419
  0
1420
1420
  ];
1421
1421
  /** 解析 `x.y.z[-pre][+build]` 的数值三元组;形状不符返回 undefined。 */
@@ -2466,6 +2466,20 @@ function createOfficialCopyWriter(deps) {
2466
2466
  status: () => status
2467
2467
  };
2468
2468
  }
2469
+ //#endregion
2470
+ //#region src/auth-inline.ts
2471
+ /**
2472
+ * PiAiAdapter 认证注入的内联实现(官方 llm-pi-ai/src/auth.ts 的等价移植)。
2473
+ *
2474
+ * 官方 credentialStoreFrom/authContextFrom 仅在 @deepseek-ai/dsh-llm-pi-ai 的
2475
+ * src/auth.ts 子路径导出(包根只导出 recordKeyFor),而 npm 发布形态不携带
2476
+ * src/(lib/index.js 单 bundle)——源码布局优先经 src 子路径走官方实现
2477
+ * (见 resolve-dsh.ts 探测),本文件在其余形态(npm 布局的 dsh 树 /
2478
+ * vendored 兜底)下按官方语义逐行等价实现,并经 assertKitShape 自检兜底。
2479
+ * recordKeyFor 由调用方注入(与 PiAiAdapter 同源模块的包根导出,保证记录
2480
+ * 键格式与官方写入方一致);记录作用域不手抄,由它现场推导(见 recordScopeOf)。
2481
+ * @module llm-pi/auth-inline
2482
+ */
2469
2483
  /** 官方 jsonImage:pi-ai 凭据里显式 undefined 成员 JSON 序列化为缺省/数组 null。 */
2470
2484
  function jsonImage(value) {
2471
2485
  if (Array.isArray(value)) return value.map((entry) => entry === void 0 ? null : jsonImage(entry));
@@ -2504,12 +2518,26 @@ function writableStore(ctx) {
2504
2518
  if (credentials === void 0) throw new LlmError("llm-pi: 当前组合未挂载凭据服务(dsh-credentials-local),登录产生的凭据无处存储", "NO_CREDENTIAL_STORE");
2505
2519
  return credentials;
2506
2520
  }
2521
+ /** 作用域推导的探针 provider id(合法记录段,故 recordKeyFor 给出完整键)。 */
2522
+ const RECORD_SCOPE_PROBE_ID = "record-scope-probe";
2523
+ /**
2524
+ * 记录作用域的现场推导(**不手抄**官方常量):`recordKeyFor` 由与 PiAiAdapter
2525
+ * 同源的官方包根注入,其产物 `scope/id` 的 scope 段就是官方写入方使用的作用域。
2526
+ * 官方改作用域时此处自动跟随——硬编码镜像一旦落后,{@link credentialStoreFrom}
2527
+ * 的 list() 会把本插件自己的登录记录判成他人的而凭空消失,且无任何报错
2528
+ * (与 compat 门控表手抄镜像同一类故障,见 docs/repo/事故记录.md)。
2529
+ */
2530
+ function recordScopeOf(recordKeyFor) {
2531
+ return credentialKeyScope(recordKeyFor(RECORD_SCOPE_PROBE_ID));
2532
+ }
2507
2533
  /**
2508
2534
  * pi-ai `CredentialStore`(官方 credentialStoreFrom 等价实现)。
2509
2535
  * 路由键任意而记录 id 受语法约束:语法外的 id 读答"未存储"、删除无事可做、
2510
- * modify 拒绝(写不落地不能报成功)。
2536
+ * modify 拒绝(写不落地不能报成功)。list() 的归属判定用现场推导的作用域
2537
+ * ({@link recordScopeOf}),不比对硬编码常量。
2511
2538
  */
2512
2539
  function credentialStoreFrom(ctx, recordKeyFor) {
2540
+ const recordScope = recordScopeOf(recordKeyFor);
2513
2541
  return {
2514
2542
  async read(providerId) {
2515
2543
  const credentials = ctx.get("credentials");
@@ -2521,7 +2549,7 @@ function credentialStoreFrom(ctx, recordKeyFor) {
2521
2549
  const stored = await ctx.get("credentials")?.listRecords() ?? [];
2522
2550
  const mine = [];
2523
2551
  for (const entry of stored) {
2524
- if (credentialKeyScope(entry.key) !== "llm-pi-ai") continue;
2552
+ if (credentialKeyScope(entry.key) !== recordScope) continue;
2525
2553
  mine.push({
2526
2554
  providerId: credentialKeyId(entry.key),
2527
2555
  type: entry.kind === "api-key" ? "api_key" : "oauth"
@@ -2591,14 +2619,39 @@ function authContextFrom(ctx) {
2591
2619
  * 超出已验证区间也只是提示(kit-meta.ts),永不阻断。
2592
2620
  *
2593
2621
  * 仅 src 子路径导出、包根不导出的两块面:
2594
- * - resolveProfiles(config.ts):官方解析链,dev 布局树可经 src 子路径复用;
2622
+ * - resolveProfiles(config.ts):官方解析链,源码布局可经 src 子路径复用;
2595
2623
  * - credentialStoreFrom/authContextFrom(auth.ts):PiAiAdapter 必需 auth 注入。
2596
2624
  * npm 发布形态(lib/index.js 单 bundle + 无 src/)两者都拿不到:dsh 树优先
2597
2625
  * 探测 src/*(含 lib/* 候选),未命中时 resolveProfiles 走插件等价实现
2598
- * (profiles.ts,门控表对齐官方 catalog.ts)、auth 走内联等价实现
2599
- * (auth-inline.ts,recordKeyFor 自包根导出同源注入)——形状自检兜底。
2626
+ * (profiles.ts,语义对齐官方 config.ts:resolveProfiles)、auth 走内联等价实现
2627
+ * (auth-inline.ts,recordKeyFor 自包根导出同源注入、记录作用域由它现场推导)。
2628
+ * 这是**打包形态的恒定事实而非降级**,故按 info 档记入诊断(missingSourceNote),
2629
+ * 免得淹没同面板里真正的降级;等价实现由形状自检与单测兜底。
2600
2630
  * @module llm-pi/resolve-dsh
2601
2631
  */
2632
+ /** 生成一条降级诊断(推导失败 / 加载回退)。 */
2633
+ function degradation(message) {
2634
+ return {
2635
+ level: "degradation",
2636
+ message
2637
+ };
2638
+ }
2639
+ /**
2640
+ * 官方 `src/` 子路径未随 npm 发布携带时的**形态说明**(info 档)。
2641
+ *
2642
+ * 官方 `resolveProfiles`(src/config.ts)与认证助手(src/auth.ts)只在子路径导出、
2643
+ * 包根不导出,而发布清单只含 `lib/index.js` + `lib/types/**`——任何 npm 安装形态都
2644
+ * 拿不到,故这两块常走插件等价实现。此处逐面列出实际走等价实现的部分,并点明
2645
+ * **仍取自官方现场推导**的部分(compat 门控 / 协议 / 模型字段集),避免被读成
2646
+ * "自动跟随失败"。
2647
+ * @param surfaces 走等价实现的官方面名称(如 `profile 解析`)。
2648
+ */
2649
+ function missingSourceNote(surfaces) {
2650
+ return {
2651
+ level: "info",
2652
+ message: `官方 @deepseek-ai/dsh-llm-pi-ai 只经 src/ 子路径导出、npm 发布形态不携带 src/:${surfaces.join("、")}使用插件等价实现;compat 门控 / 协议集合 / 模型条目字段集仍取自官方安装副本现场推导`
2653
+ };
2654
+ }
2602
2655
  /** kit 必备形状清单:缺失即视为上游不兼容。 */
2603
2656
  function assertKitShape(kit, origin) {
2604
2657
  const problems = [];
@@ -2624,6 +2677,7 @@ function assertKitShape(kit, origin) {
2624
2677
  if (typeof kit.resolveProfiles !== "function") problems.push("resolveProfiles 缺失");
2625
2678
  if (typeof kit.resolveImageAttachmentAccess !== "function") problems.push("resolveImageAttachmentAccess 缺失");
2626
2679
  if (typeof kit.auth?.credentialStoreFrom !== "function" || typeof kit.auth?.authContextFrom !== "function") problems.push("auth 助手(credentialStoreFrom/authContextFrom)缺失");
2680
+ if (typeof kit.recordKeyFor !== "function") problems.push("recordKeyFor 缺失");
2627
2681
  if (problems.length > 0) throw new Error(`llm-pi: ${origin} 来源的运行时套件形状不兼容:${problems.join(";")}`);
2628
2682
  }
2629
2683
  /** deepseek 模块形状自检;返回问题清单(空 = 可用),由调用方决定降级。 */
@@ -2728,7 +2782,7 @@ async function loadFallbackTreeFactories(root, diagnostics) {
2728
2782
  if (factory !== void 0) out[api] = factory;
2729
2783
  } catch {}
2730
2784
  }
2731
- if (Object.keys(out).length < fallbackProtocols().length) diagnostics.push(`pi-ai 协议兜底三元组未能全部加载(${Object.keys(out).join("/") || "空"});请检查 dsh 安装树里 @earendil-works/pi-ai/dist/api 的形态`);
2785
+ if (Object.keys(out).length < fallbackProtocols().length) diagnostics.push(degradation(`pi-ai 协议兜底三元组未能全部加载(${Object.keys(out).join("/") || "空"});请检查 dsh 安装树里 @earendil-works/pi-ai/dist/api 的形态`));
2732
2786
  return out;
2733
2787
  }
2734
2788
  /**
@@ -2741,25 +2795,25 @@ async function loadTreeProtocols(root, derived, diagnostics) {
2741
2795
  for (const api of derived) {
2742
2796
  const file = join(dir, `${api}.lazy.js`);
2743
2797
  if (!existsSync(file)) {
2744
- diagnostics.push(`pi-ai 未提供协议 "${api}" 的惰性实现(缺 ${file});该协议不可用,其余协议不受影响`);
2798
+ diagnostics.push(degradation(`pi-ai 未提供协议 "${api}" 的惰性实现(缺 ${file});该协议不可用,其余协议不受影响`));
2745
2799
  continue;
2746
2800
  }
2747
2801
  try {
2748
2802
  const factory = protocolFactoryOf(await import(pathToFileURL(file).href));
2749
2803
  if (factory === void 0) {
2750
- diagnostics.push(`协议 "${api}" 的实现模块未导出唯一 *Api 工厂;该协议不可用`);
2804
+ diagnostics.push(degradation(`协议 "${api}" 的实现模块未导出唯一 *Api 工厂;该协议不可用`));
2751
2805
  continue;
2752
2806
  }
2753
2807
  out[api] = factory;
2754
2808
  } catch (error) {
2755
- diagnostics.push(`协议 "${api}" 的实现加载失败(${error instanceof Error ? error.message : String(error)});该协议不可用`);
2809
+ diagnostics.push(degradation(`协议 "${api}" 的实现加载失败(${error instanceof Error ? error.message : String(error)});该协议不可用`));
2756
2810
  }
2757
2811
  }
2758
2812
  if (Object.keys(out).length > 0) return {
2759
2813
  protocols: derived.filter((api) => out[api] !== void 0),
2760
2814
  protocolFactories: out
2761
2815
  };
2762
- diagnostics.push("官方声明的协议一个都加载不到;回退内置三元组");
2816
+ diagnostics.push(degradation("官方声明的协议一个都加载不到;回退内置三元组"));
2763
2817
  const fallback = await loadFallbackTreeFactories(root, diagnostics);
2764
2818
  return {
2765
2819
  protocols: fallbackProtocols().filter((api) => fallback[api] !== void 0),
@@ -2805,7 +2859,7 @@ function installOfficialSurface(options) {
2805
2859
  try {
2806
2860
  const gates = deriveGates(readFileSync(bundlePath, "utf8"));
2807
2861
  if (gates === void 0) {
2808
- diagnostics.push(`${compatLabel}未能从官方 bundle 解析 COMPAT_GATES(${bundlePath});使用内置快照(未知字段放行、withhold 仍拒绝;请检查 dsh-llm-pi-ai 打包形态)`);
2862
+ diagnostics.push(degradation(`${compatLabel}未能从官方 bundle 解析 COMPAT_GATES(${bundlePath});使用内置快照(未知字段放行、withhold 仍拒绝;请检查 dsh-llm-pi-ai 打包形态)`));
2809
2863
  installCompatTable(FALLBACK_TABLE);
2810
2864
  } else installCompatTable({
2811
2865
  gates,
@@ -2817,13 +2871,13 @@ function installOfficialSurface(options) {
2817
2871
  });
2818
2872
  } catch (error) {
2819
2873
  const reason = error instanceof Error ? error.message : String(error);
2820
- diagnostics.push(`${compatLabel}推导失败(${reason});使用内置快照(未知字段放行)`);
2874
+ diagnostics.push(degradation(`${compatLabel}推导失败(${reason});使用内置快照(未知字段放行)`));
2821
2875
  installCompatTable(FALLBACK_TABLE);
2822
2876
  }
2823
2877
  const derivedProtocols = deriveProtocols(adapterModule);
2824
- if (derivedProtocols === void 0) diagnostics.push(`${origin}:官方 supportedProtocols() 不可用;协议集合回退内置三元组 ${fallbackProtocols().join("/")}`);
2878
+ if (derivedProtocols === void 0) diagnostics.push(degradation(`${origin}:官方 supportedProtocols() 不可用;协议集合回退内置三元组 ${fallbackProtocols().join("/")}`));
2825
2879
  const fields = deriveModelEntryFields(adapterModule);
2826
- if (fields === void 0) diagnostics.push(`${origin}:官方 Config schema 的模型条目字段集不可推导;继承字段回退已知键 ${FALLBACK_MODEL_ENTRY_FIELDS.join("/")}(官方新增字段可能不被继承)`);
2880
+ if (fields === void 0) diagnostics.push(degradation(`${origin}:官方 Config schema 的模型条目字段集不可推导;继承字段回退已知键 ${FALLBACK_MODEL_ENTRY_FIELDS.join("/")}(官方新增字段可能不被继承)`));
2827
2881
  const generatedAt = catalogGeneratedAtOf(options.catalogModule);
2828
2882
  return {
2829
2883
  protocols: derivedProtocols ?? fallbackProtocols(),
@@ -2865,7 +2919,8 @@ function kitFromTree(options) {
2865
2919
  credentialStoreFrom: officialCredentialStoreFrom !== void 0 ? officialCredentialStoreFrom : (ctx) => credentialStoreFrom(ctx, recordKeyFor),
2866
2920
  authContextFrom: officialAuthContextFrom !== void 0 ? officialAuthContextFrom : authContextFrom
2867
2921
  },
2868
- resolveImageAttachmentAccess: mods.llm["resolveImageAttachmentAccess"]
2922
+ resolveImageAttachmentAccess: mods.llm["resolveImageAttachmentAccess"],
2923
+ recordKeyFor
2869
2924
  };
2870
2925
  }
2871
2926
  /** vendored 兜底副本的 deepseek 模块(形状自检不过时返回 undefined)。 */
@@ -2899,7 +2954,7 @@ function vendoredProtocolFactories() {
2899
2954
  * npm 发布形态不携带 src/、lib 仅 index.js/invariant.js——resolveProfiles 与
2900
2955
  * auth 助手在此无条件走插件等价实现(内联,见文件头说明);compat 门控表则
2901
2956
  * 仍从 vendored 副本的 bundle + Config schema 现场推导(与 dsh 树同一推导链)。
2902
- * @param diagnostics 可选诊断收集器(回退原因与逐项降级说明)。
2957
+ * @param diagnostics 可选诊断收集器(回退原因、形态说明与逐项降级)。
2903
2958
  */
2904
2959
  function loadVendoredKit(diagnostics = []) {
2905
2960
  const staticFactories = vendoredProtocolFactories();
@@ -2945,9 +3000,11 @@ function loadVendoredKit(diagnostics = []) {
2945
3000
  authContextFrom
2946
3001
  },
2947
3002
  resolveImageAttachmentAccess: vendoredLlm.resolveImageAttachmentAccess,
3003
+ recordKeyFor: vendoredPiAiAdapter.recordKeyFor,
2948
3004
  ...deepseek === void 0 ? {} : { deepseek }
2949
3005
  };
2950
3006
  assertKitShape(kit, "vendored");
3007
+ diagnostics.push(missingSourceNote(["profile 解析", "认证助手"]));
2951
3008
  return kit;
2952
3009
  }
2953
3010
  /**
@@ -2971,7 +3028,8 @@ function dshTreeAnchor(installAnchor, argvEntry = process.argv[1]) {
2971
3028
  /**
2972
3029
  * 解析运行时套件:优先 dsh 安装树(自动跟随上游),失败回退 vendored 副本;
2973
3030
  * 两者都过不了形状自检时抛错(调用方应记日志并放弃注册 route)。
2974
- * 返回的 diagnostics 记录回退原因与逐项降级说明,供配置卡片与日志展示。
3031
+ * 返回的 diagnostics 记录回退原因、打包形态说明(info)与逐项降级(degradation),
3032
+ * 供配置卡片与日志分档展示。
2975
3033
  * @param installAnchor profile 的 dsh 安装锚点(`ctx.profileContext.installAnchor`)。
2976
3034
  */
2977
3035
  async function resolveDshKit(installAnchor) {
@@ -3001,8 +3059,10 @@ async function resolveDshKit(installAnchor) {
3001
3059
  versions: collectTreeVersions(anchor)
3002
3060
  });
3003
3061
  assertKitShape(kit, "dsh-tree");
3004
- if (auth === void 0) diagnostics.push("dsh 树不含 dsh-llm-pi-ai/src(npm 发布形态);认证助手使用插件内联等价实现");
3005
- if (config === void 0) diagnostics.push("dsh 树不含 dsh-llm-pi-ai/src(npm 发布形态);profile 解析使用插件等价实现(compat 门控对齐官方 catalog.ts)");
3062
+ const missingSeams = [];
3063
+ if (config === void 0) missingSeams.push("profile 解析");
3064
+ if (auth === void 0) missingSeams.push("认证助手");
3065
+ if (missingSeams.length > 0) diagnostics.push(missingSourceNote(missingSeams));
3006
3066
  const tree = await importTreeDeepseek(anchor);
3007
3067
  if (tree.kit !== void 0) return {
3008
3068
  kit: {
@@ -3011,17 +3071,17 @@ async function resolveDshKit(installAnchor) {
3011
3071
  },
3012
3072
  diagnostics
3013
3073
  };
3014
- diagnostics.push(`dsh 安装树的 dsh-llm-deepseek 不可用(${tree.problem ?? "未知原因"});adapter: deepseek 的 route 不可用,pi 路由不受影响`);
3074
+ diagnostics.push(degradation(`dsh 安装树的 dsh-llm-deepseek 不可用(${tree.problem ?? "未知原因"});adapter: deepseek 的 route 不可用,pi 路由不受影响`));
3015
3075
  return {
3016
3076
  kit,
3017
3077
  diagnostics
3018
3078
  };
3019
3079
  } catch (error) {
3020
- diagnostics.push(`dsh 安装树套件不可用(${anchor}):${error instanceof Error ? error.message : String(error)};回退 vendored 副本`);
3080
+ diagnostics.push(degradation(`dsh 安装树套件不可用(${anchor}):${error instanceof Error ? error.message : String(error)};回退 vendored 副本`));
3021
3081
  }
3022
- } else diagnostics.push("未能从 profileContext.installAnchor / process.argv[1] 定位 dsh 安装树;回退 vendored 副本");
3082
+ } else diagnostics.push(degradation("未能从 profileContext.installAnchor / process.argv[1] 定位 dsh 安装树;回退 vendored 副本"));
3023
3083
  const kit = loadVendoredKit(diagnostics);
3024
- if (kit.deepseek === void 0) diagnostics.push("vendored 副本的 dsh-llm-deepseek 形状不兼容;adapter: deepseek 的 route 不可用");
3084
+ if (kit.deepseek === void 0) diagnostics.push(degradation("vendored 副本的 dsh-llm-deepseek 形状不兼容;adapter: deepseek 的 route 不可用"));
3025
3085
  return {
3026
3086
  kit,
3027
3087
  diagnostics
@@ -3048,10 +3108,11 @@ function makeResolveApiKey(ctx, kit) {
3048
3108
  throw new kit.LlmError(`llm-pi: provider route "${provider}" 的凭据引用 ${String(ref)} 未解析到值——请经凭据服务(web Models 页)存放或导出环境变量;仅当该 provider 应使用 pi-ai 自有环境发现时才移除 apiKeyEnv`, "MISSING_CREDENTIAL");
3049
3109
  };
3050
3110
  }
3051
- /** 解析运行期套件并落启动日志(来源/生效版本/协议/逐项降级诊断)。 */
3111
+ /** 解析运行期套件并落启动日志(来源/生效版本/协议/形态说明与逐项降级)。 */
3052
3112
  async function loadRuntimeKit(ctx, logger) {
3053
3113
  const { kit, diagnostics } = await resolveDshKit(ctx.get("profileContext")?.installAnchor);
3054
- for (const line of diagnostics) logger.warn(line);
3114
+ for (const item of diagnostics) if (item.level === "info") logger.info(item.message);
3115
+ else logger.warn(item.message);
3055
3116
  logger.info(`运行时套件来源:${kit.source};pi-ai ${kit.versions.piAi ?? "?"} / dsh-llm-pi-ai ${kit.versions.piAiAdapter ?? "?"};协议 ${kit.protocols.join("/")}`);
3056
3117
  const versionNotice = piAiVersionNotice(kit.versions.piAi);
3057
3118
  if (versionNotice !== void 0) logger.warn(versionNotice);
@@ -3317,8 +3378,8 @@ async function startRuntime(ctx, rawConfig) {
3317
3378
  officialCopy: copy,
3318
3379
  diagnostics: [
3319
3380
  ...diagnostics,
3320
- ...copy.warnings.map((warning) => `应急副本:${warning}`),
3321
- ...copy.error === void 0 ? [] : [`应急副本:生成失败(${copy.error})`]
3381
+ ...copy.warnings.map((warning) => degradation(`应急副本:${warning}`)),
3382
+ ...copy.error === void 0 ? [] : [degradation(`应急副本:生成失败(${copy.error})`)]
3322
3383
  ]
3323
3384
  };
3324
3385
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/llm-pi",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "dsh-plus service+ui plugin: 基于 PiAiAdapter 的自定义 LLM 路由(全量 compat、模型继承、pi-ai 内置目录;协议/compat/字段集现场推导,自动跟随已装 dsh)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -32,43 +32,43 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "yaml": "^2.9.0",
35
- "@dsh-plus/shared": "0.1.36"
35
+ "@dsh-plus/shared": "0.1.38"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.5-alpha.1",
39
- "@deepseek-ai/dsh-anonymous-user-id": "^0.2.1-alpha.1",
40
- "@deepseek-ai/dsh-credentials": "^0.2.1-alpha.1",
41
- "@deepseek-ai/dsh-home-paths": "^0.2.1-alpha.1",
42
- "@deepseek-ai/dsh-host-webserver": "^0.2.1-alpha.1",
43
- "@deepseek-ai/dsh-launch-environment": "^0.2.1-alpha.1",
44
- "@deepseek-ai/dsh-llm": "^0.2.1-alpha.1",
45
- "@deepseek-ai/dsh-llm-deepseek": "^0.2.1-alpha.1",
46
- "@deepseek-ai/dsh-llm-pi-ai": "^0.2.1-alpha.1",
47
- "@deepseek-ai/dsh-settings": "^0.2.1-alpha.1",
48
- "@deepseek-ai/dsh-util-values": "^0.2.1-alpha.1",
39
+ "@deepseek-ai/dsh-anonymous-user-id": "^0.2.1-alpha.2",
40
+ "@deepseek-ai/dsh-credentials": "^0.2.1-alpha.2",
41
+ "@deepseek-ai/dsh-home-paths": "^0.2.1-alpha.2",
42
+ "@deepseek-ai/dsh-host-webserver": "^0.2.1-alpha.2",
43
+ "@deepseek-ai/dsh-launch-environment": "^0.2.1-alpha.2",
44
+ "@deepseek-ai/dsh-llm": "^0.2.1-alpha.2",
45
+ "@deepseek-ai/dsh-llm-deepseek": "^0.2.1-alpha.2",
46
+ "@deepseek-ai/dsh-llm-pi-ai": "^0.2.1-alpha.2",
47
+ "@deepseek-ai/dsh-settings": "^0.2.1-alpha.2",
48
+ "@deepseek-ai/dsh-util-values": "^0.2.1-alpha.2",
49
49
  "@deepseek-ai/schemastery": "^3.18.5-alpha.1",
50
- "@earendil-works/pi-ai": ">=0.85.1"
50
+ "@earendil-works/pi-ai": ">=1.0.2"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@deepseek-ai/cordis": "4.0.5-alpha.1",
54
- "@deepseek-ai/dsh-anonymous-user-id": "0.2.1-alpha.1",
55
- "@deepseek-ai/dsh-app-boot": "0.2.1-alpha.1",
56
- "@deepseek-ai/dsh-atomic-write": "0.2.1-alpha.1",
57
- "@deepseek-ai/dsh-attachment": "0.2.1-alpha.1",
58
- "@deepseek-ai/dsh-credentials": "0.2.1-alpha.1",
59
- "@deepseek-ai/dsh-home-paths": "0.2.1-alpha.1",
60
- "@deepseek-ai/dsh-host-webserver": "0.2.1-alpha.1",
61
- "@deepseek-ai/dsh-launch-environment": "0.2.1-alpha.1",
62
- "@deepseek-ai/dsh-llm": "0.2.1-alpha.1",
63
- "@deepseek-ai/dsh-llm-deepseek": "0.2.1-alpha.1",
64
- "@deepseek-ai/dsh-llm-pi-ai": "0.2.1-alpha.1",
65
- "@deepseek-ai/dsh-settings": "0.2.1-alpha.1",
66
- "@deepseek-ai/dsh-timeout": "0.2.1-alpha.1",
67
- "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.1",
68
- "@deepseek-ai/dsh-util-crypto": "0.2.1-alpha.1",
69
- "@deepseek-ai/dsh-util-values": "0.2.1-alpha.1",
54
+ "@deepseek-ai/dsh-anonymous-user-id": "0.2.1-alpha.2",
55
+ "@deepseek-ai/dsh-app-boot": "0.2.1-alpha.2",
56
+ "@deepseek-ai/dsh-atomic-write": "0.2.1-alpha.2",
57
+ "@deepseek-ai/dsh-attachment": "0.2.1-alpha.2",
58
+ "@deepseek-ai/dsh-credentials": "0.2.1-alpha.2",
59
+ "@deepseek-ai/dsh-home-paths": "0.2.1-alpha.2",
60
+ "@deepseek-ai/dsh-host-webserver": "0.2.1-alpha.2",
61
+ "@deepseek-ai/dsh-launch-environment": "0.2.1-alpha.2",
62
+ "@deepseek-ai/dsh-llm": "0.2.1-alpha.2",
63
+ "@deepseek-ai/dsh-llm-deepseek": "0.2.1-alpha.2",
64
+ "@deepseek-ai/dsh-llm-pi-ai": "0.2.1-alpha.2",
65
+ "@deepseek-ai/dsh-settings": "0.2.1-alpha.2",
66
+ "@deepseek-ai/dsh-timeout": "0.2.1-alpha.2",
67
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2",
68
+ "@deepseek-ai/dsh-util-crypto": "0.2.1-alpha.2",
69
+ "@deepseek-ai/dsh-util-values": "0.2.1-alpha.2",
70
70
  "@deepseek-ai/schemastery": "3.18.5-alpha.1",
71
- "@earendil-works/pi-ai": "0.87.1",
71
+ "@earendil-works/pi-ai": "1.1.0",
72
72
  "@types/react": "~18.3.31",
73
73
  "react": "^18.2.0"
74
74
  },
@@ -3,11 +3,11 @@
3
3
  *
4
4
  * 官方 credentialStoreFrom/authContextFrom 仅在 @deepseek-ai/dsh-llm-pi-ai 的
5
5
  * src/auth.ts 子路径导出(包根只导出 recordKeyFor),而 npm 发布形态不携带
6
- * src/(lib/index.js 单 bundle)——dsh 树 dev 布局优先经 src 子路径走官方
7
- * 实现(见 resolve-dsh.ts 探测),本文件在其余形态(npm 布局的 dsh 树 /
6
+ * src/(lib/index.js 单 bundle)——源码布局优先经 src 子路径走官方实现
7
+ * (见 resolve-dsh.ts 探测),本文件在其余形态(npm 布局的 dsh 树 /
8
8
  * vendored 兜底)下按官方语义逐行等价实现,并经 assertKitShape 自检兜底。
9
9
  * recordKeyFor 由调用方注入(与 PiAiAdapter 同源模块的包根导出,保证记录
10
- * 键格式与官方写入方一致)。
10
+ * 键格式与官方写入方一致);记录作用域不手抄,由它现场推导(见 recordScopeOf)。
11
11
  * @module llm-pi/auth-inline
12
12
  */
13
13
  import { access } from 'node:fs/promises'
@@ -36,9 +36,6 @@ import type {
36
36
  CredentialStore,
37
37
  } from '@earendil-works/pi-ai'
38
38
 
39
- /** 官方 RECORD_SCOPE 常量(llm-pi-ai/src/auth.ts)——读写作用域必须与官方一致。 */
40
- export const INLINE_RECORD_SCOPE = 'llm-pi-ai'
41
-
42
39
  /** 官方 jsonImage:pi-ai 凭据里显式 undefined 成员 JSON 序列化为缺省/数组 null。 */
43
40
  function jsonImage(value: unknown): unknown {
44
41
  if (Array.isArray(value))
@@ -94,15 +91,31 @@ function writableStore(ctx: Context): CredentialProvider {
94
91
  return credentials
95
92
  }
96
93
 
94
+ /** 作用域推导的探针 provider id(合法记录段,故 recordKeyFor 给出完整键)。 */
95
+ const RECORD_SCOPE_PROBE_ID = 'record-scope-probe'
96
+
97
+ /**
98
+ * 记录作用域的现场推导(**不手抄**官方常量):`recordKeyFor` 由与 PiAiAdapter
99
+ * 同源的官方包根注入,其产物 `scope/id` 的 scope 段就是官方写入方使用的作用域。
100
+ * 官方改作用域时此处自动跟随——硬编码镜像一旦落后,{@link credentialStoreFrom}
101
+ * 的 list() 会把本插件自己的登录记录判成他人的而凭空消失,且无任何报错
102
+ * (与 compat 门控表手抄镜像同一类故障,见 docs/repo/事故记录.md)。
103
+ */
104
+ function recordScopeOf(recordKeyFor: (providerId: string) => CredentialKey): string {
105
+ return credentialKeyScope(recordKeyFor(RECORD_SCOPE_PROBE_ID))
106
+ }
107
+
97
108
  /**
98
109
  * pi-ai `CredentialStore`(官方 credentialStoreFrom 等价实现)。
99
110
  * 路由键任意而记录 id 受语法约束:语法外的 id 读答"未存储"、删除无事可做、
100
- * modify 拒绝(写不落地不能报成功)。
111
+ * modify 拒绝(写不落地不能报成功)。list() 的归属判定用现场推导的作用域
112
+ * ({@link recordScopeOf}),不比对硬编码常量。
101
113
  */
102
114
  export function credentialStoreFrom(
103
115
  ctx: Context,
104
116
  recordKeyFor: (providerId: string) => CredentialKey,
105
117
  ): CredentialStore {
118
+ const recordScope = recordScopeOf(recordKeyFor)
106
119
  return {
107
120
  async read(providerId) {
108
121
  const credentials = ctx.get('credentials')
@@ -114,7 +127,7 @@ export function credentialStoreFrom(
114
127
  const stored = (await ctx.get('credentials')?.listRecords()) ?? []
115
128
  const mine: CredentialInfo[] = []
116
129
  for (const entry of stored) {
117
- if (credentialKeyScope(entry.key) !== INLINE_RECORD_SCOPE) continue
130
+ if (credentialKeyScope(entry.key) !== recordScope) continue
118
131
  mine.push({
119
132
  providerId: credentialKeyId(entry.key),
120
133
  type: entry.kind === 'api-key' ? 'api_key' : 'oauth',
package/src/client/api.ts CHANGED
@@ -26,7 +26,14 @@ export interface WireKitInfo {
26
26
  warnings: string[]
27
27
  error?: string
28
28
  }
29
- diagnostics: string[]
29
+ diagnostics: WireDiagnostic[]
30
+ }
31
+
32
+ /** 套件诊断条目(服务端 KitDiagnostic 的镜像)。 */
33
+ export interface WireDiagnostic {
34
+ /** info = 打包形态说明(非缺陷);degradation = 回退与逐项降级。 */
35
+ level: 'info' | 'degradation'
36
+ message: string
30
37
  }
31
38
 
32
39
  /** 内置 provider 条目。 */
@@ -27,7 +27,8 @@ const ownZh = {
27
27
  officialCopyPending: '尚未生成',
28
28
  officialCopyFailed: '生成失败:',
29
29
  officialCopyRoutes: '个 route',
30
- runtimeDiagnostics: '诊断',
30
+ runtimeFormNotes: '形态说明',
31
+ runtimeDiagnostics: '降级诊断',
31
32
  runtimeVersionUnknown: '未知',
32
33
  runtimeCatalogUnit: '个 provider /',
33
34
  runtimeCatalogModels: '个模型',
@@ -168,7 +169,8 @@ const ownEn = {
168
169
  officialCopyPending: 'not generated yet',
169
170
  officialCopyFailed: 'generation failed: ',
170
171
  officialCopyRoutes: 'routes',
171
- runtimeDiagnostics: 'Diagnostics',
172
+ runtimeFormNotes: 'Packaging notes',
173
+ runtimeDiagnostics: 'Degradations',
172
174
  runtimeVersionUnknown: 'unknown',
173
175
  runtimeCatalogUnit: 'providers /',
174
176
  runtimeCatalogModels: 'models',
@@ -3,15 +3,16 @@
3
3
  *
4
4
  * 状态行回答的是"我现在到底在跑哪一份 pi-ai"——插件按文件路径动态加载已装 dsh 树
5
5
  * 里的套件(见 resolve-dsh.ts),manifest 上写的版本**不是**生效版本。这里显示
6
- * 生效版本、安装树、目录规模与数据日期、可服务协议、compat 表来源,以及逐项降级
7
- * 诊断;版本超出验证区间时给出提示(提示而非阻断)。
6
+ * 生效版本、安装树、目录规模与数据日期、可服务协议、compat 表来源,以及诊断
7
+ * (按「形态说明 / 降级诊断」分块,避免恒定事实淹没真实降级);版本超出验证区间
8
+ * 时给出提示(提示而非阻断)。
8
9
  * @module llm-pi/client/root-fields
9
10
  */
10
11
 
11
12
  import { CheckRow } from '@dsh-plus/shared/client'
12
13
  import type { ReactElement } from 'react'
13
14
 
14
- import type { WireKitInfo } from './api.ts'
15
+ import type { WireDiagnostic, WireKitInfo } from './api.ts'
15
16
  import type { Draft } from './draft.ts'
16
17
  import type { Translate } from './i18n.ts'
17
18
 
@@ -44,6 +45,25 @@ function officialCopyText(kit: WireKitInfo, t: Translate): string {
44
45
  return `${copy.path} · ${copy.routes} ${t('officialCopyRoutes')} · ${formatTime(copy.updatedAt, t('runtimeVersionUnknown'))}`
45
46
  }
46
47
 
48
+ /**
49
+ * 诊断分块:形态说明与降级分开渲染——「官方 src 不随 npm 发布」这类恒定事实
50
+ * 永不会消失,混在一起会把真正的降级淹掉。空块不渲染。
51
+ */
52
+ function DiagnosticBlock(props: {
53
+ label: string
54
+ level: WireDiagnostic['level']
55
+ items: readonly WireDiagnostic[]
56
+ }): ReactElement | null {
57
+ const messages = props.items.filter((item) => item.level === props.level).map((m) => m.message)
58
+ if (messages.length === 0) return null
59
+ return (
60
+ <>
61
+ <p className="lpc-catDetailLabel">{props.label}</p>
62
+ <pre className="lpc-catJson">{messages.join('\n')}</pre>
63
+ </>
64
+ )
65
+ }
66
+
47
67
  export interface RootFieldsProps {
48
68
  t: Translate
49
69
  draft: Draft
@@ -96,8 +116,12 @@ export function RootFields(props: RootFieldsProps): ReactElement {
96
116
  ) : null}
97
117
  {kit.diagnostics.length > 0 ? (
98
118
  <>
99
- <p className="lpc-catDetailLabel">{t('runtimeDiagnostics')}</p>
100
- <pre className="lpc-catJson">{kit.diagnostics.join('\n')}</pre>
119
+ <DiagnosticBlock label={t('runtimeFormNotes')} level="info" items={kit.diagnostics} />
120
+ <DiagnosticBlock
121
+ label={t('runtimeDiagnostics')}
122
+ level="degradation"
123
+ items={kit.diagnostics}
124
+ />
101
125
  </>
102
126
  ) : null}
103
127
  </>
package/src/kit-meta.ts CHANGED
@@ -24,7 +24,7 @@ export interface KitVersions {
24
24
  }
25
25
 
26
26
  /** 本插件开发/测试覆盖过的 pi-ai 区间(semver range,仅用于状态行提示)。 */
27
- export const VERIFIED_PI_AI_RANGE = '>=0.85.1 <0.88.0'
27
+ export const VERIFIED_PI_AI_RANGE = '>=1.0.2 <2.0.0'
28
28
 
29
29
  /** 读 package.json 的 version;文件不存在/非 JSON/无 version 都返回 undefined。 */
30
30
  export function readPackageVersion(manifestPath: string): string | undefined {
@@ -109,8 +109,8 @@ export function collectVendoredVersions(): KitVersions {
109
109
  }
110
110
 
111
111
  /** 已验证区间的解析端点(与 {@link VERIFIED_PI_AI_RANGE} 同步维护)。 */
112
- const VERIFIED_MIN: readonly [number, number, number] = [0, 85, 1]
113
- const VERIFIED_MAX_EXCLUSIVE: readonly [number, number, number] = [0, 88, 0]
112
+ const VERIFIED_MIN: readonly [number, number, number] = [1, 0, 2]
113
+ const VERIFIED_MAX_EXCLUSIVE: readonly [number, number, number] = [2, 0, 0]
114
114
 
115
115
  /** 解析 `x.y.z[-pre][+build]` 的数值三元组;形状不符返回 undefined。 */
116
116
  function numericTriple(version: string): [number, number, number] | undefined {
@@ -206,7 +206,7 @@ export function fallbackProtocols(): string[] {
206
206
  }
207
207
 
208
208
  /**
209
- * 官方门控表不可解析时的兜底快照(0.2.1-alpha.1 现场推导值)。
209
+ * 官方门控表不可解析时的兜底快照(0.2.1-alpha.2 现场推导值)。
210
210
  *
211
211
  * 只在官方改打包形态、`COMPAT_GATES` 解析失败时启用(此时未知键放行、只拦快照里
212
212
  * 明确 withhold 的键,见 compat.ts 的「语义分两档」)。快照过期会重新引入
@@ -20,12 +20,14 @@
20
20
  * 超出已验证区间也只是提示(kit-meta.ts),永不阻断。
21
21
  *
22
22
  * 仅 src 子路径导出、包根不导出的两块面:
23
- * - resolveProfiles(config.ts):官方解析链,dev 布局树可经 src 子路径复用;
23
+ * - resolveProfiles(config.ts):官方解析链,源码布局可经 src 子路径复用;
24
24
  * - credentialStoreFrom/authContextFrom(auth.ts):PiAiAdapter 必需 auth 注入。
25
25
  * npm 发布形态(lib/index.js 单 bundle + 无 src/)两者都拿不到:dsh 树优先
26
26
  * 探测 src/*(含 lib/* 候选),未命中时 resolveProfiles 走插件等价实现
27
- * (profiles.ts,门控表对齐官方 catalog.ts)、auth 走内联等价实现
28
- * (auth-inline.ts,recordKeyFor 自包根导出同源注入)——形状自检兜底。
27
+ * (profiles.ts,语义对齐官方 config.ts:resolveProfiles)、auth 走内联等价实现
28
+ * (auth-inline.ts,recordKeyFor 自包根导出同源注入、记录作用域由它现场推导)。
29
+ * 这是**打包形态的恒定事实而非降级**,故按 info 档记入诊断(missingSourceNote),
30
+ * 免得淹没同面板里真正的降级;等价实现由形状自检与单测兜底。
29
31
  * @module llm-pi/resolve-dsh
30
32
  */
31
33
  import { existsSync, readFileSync, realpathSync } from 'node:fs'
@@ -106,6 +108,43 @@ export interface AuthHelpers {
106
108
  authContextFrom(ctx: Context): AuthContext
107
109
  }
108
110
 
111
+ /**
112
+ * 套件诊断条目,配置页按档分块展示:
113
+ * - `info`:当前打包形态下的**恒定事实**,不是缺陷——官方 `src/` 只经子路径导出、
114
+ * npm 发布形态不携带,本插件因此走等价实现(见 {@link missingSourceNote});
115
+ * - `degradation`:推导/加载真的降级了(回退快照、协议缺失、树不可用等)。
116
+ * 分档是为了不让恒定事实淹没真实降级。
117
+ */
118
+ export interface KitDiagnostic {
119
+ level: 'info' | 'degradation'
120
+ message: string
121
+ }
122
+
123
+ /** 生成一条降级诊断(推导失败 / 加载回退)。 */
124
+ export function degradation(message: string): KitDiagnostic {
125
+ return { level: 'degradation', message }
126
+ }
127
+
128
+ /**
129
+ * 官方 `src/` 子路径未随 npm 发布携带时的**形态说明**(info 档)。
130
+ *
131
+ * 官方 `resolveProfiles`(src/config.ts)与认证助手(src/auth.ts)只在子路径导出、
132
+ * 包根不导出,而发布清单只含 `lib/index.js` + `lib/types/**`——任何 npm 安装形态都
133
+ * 拿不到,故这两块常走插件等价实现。此处逐面列出实际走等价实现的部分,并点明
134
+ * **仍取自官方现场推导**的部分(compat 门控 / 协议 / 模型字段集),避免被读成
135
+ * "自动跟随失败"。
136
+ * @param surfaces 走等价实现的官方面名称(如 `profile 解析`)。
137
+ */
138
+ export function missingSourceNote(surfaces: readonly string[]): KitDiagnostic {
139
+ return {
140
+ level: 'info',
141
+ message:
142
+ '官方 @deepseek-ai/dsh-llm-pi-ai 只经 src/ 子路径导出、npm 发布形态不携带 src/:' +
143
+ `${surfaces.join('、')}使用插件等价实现;` +
144
+ 'compat 门控 / 协议集合 / 模型条目字段集仍取自官方安装副本现场推导',
145
+ }
146
+ }
147
+
109
148
  /** 插件运行期所需的全部上游模块表面(单一来源,内部一致)。 */
110
149
  export interface DshKit {
111
150
  /** 解析来源:dsh 安装树 / 插件 vendored 兜底副本。 */
@@ -140,16 +179,22 @@ export interface DshKit {
140
179
  getBuiltinProviders: typeof vendoredCatalog.getBuiltinProviders
141
180
  getBuiltinModels: typeof vendoredCatalog.getBuiltinModels
142
181
  /**
143
- * profile 解析链:dsh 树 dev 布局(src/config.ts 存在)时为官方
144
- * resolveProfiles;npm 形态(无 src)与 vendored 兜底时为插件等价实现。
182
+ * profile 解析链:官方源码布局(`src/config.ts` 存在,如上游工作区)时为官方
183
+ * resolveProfiles;npm 发布形态(不携带 src/,包根也不导出)与 vendored 兜底时
184
+ * 为插件等价实现——该形态事实按 info 档记入诊断(见 {@link missingSourceNote})。
145
185
  */
146
186
  resolveProfiles: ResolveProfiles
147
187
  /**
148
- * PiAiAdapter 必需 auth 注入:dsh 树 dev 布局(src/auth.ts 存在)时为官方
188
+ * PiAiAdapter 必需 auth 注入:官方源码布局(`src/auth.ts` 存在)时为官方
149
189
  * credentialStoreFrom/authContextFrom;其余形态为内联等价实现
150
- * (auth-inline.ts,与官方同语义:记录作用域 llm-pi-ai + recordKeyFor)。
190
+ * (auth-inline.ts,记录键与作用域均取自官方 recordKeyFor)。
151
191
  */
152
192
  auth: AuthHelpers
193
+ /**
194
+ * 官方凭据记录键格式器(包根导出)。auth 助手按它对齐记录键,并**现场推导**
195
+ * 记录作用域(不再手抄官方常量,见 auth-inline.ts);缺失即形状不兼容。
196
+ */
197
+ recordKeyFor: (providerId: string) => CredentialKey
153
198
  /** 附件引用 → 当前模型工具执行世界的只读路径桥(官方 dsh-llm 导出)。 */
154
199
  resolveImageAttachmentAccess: typeof DshLlm.resolveImageAttachmentAccess
155
200
  /** deepseek 文件通道路由所需模块;同源自检失败时为 undefined(见 diagnostics)。 */
@@ -207,6 +252,7 @@ function assertKitShape(kit: DshKit, origin: string): void {
207
252
  ) {
208
253
  problems.push('auth 助手(credentialStoreFrom/authContextFrom)缺失')
209
254
  }
255
+ if (typeof kit.recordKeyFor !== 'function') problems.push('recordKeyFor 缺失')
210
256
  if (problems.length > 0) {
211
257
  throw new Error(`llm-pi: ${origin} 来源的运行时套件形状不兼容:${problems.join(';')}`)
212
258
  }
@@ -343,7 +389,7 @@ function piAiApiDir(root: string): string {
343
389
  /** 内置三元组的静态加载(协议推导失败或全部工厂加载失败时的兜底)。 */
344
390
  async function loadFallbackTreeFactories(
345
391
  root: string,
346
- diagnostics: string[],
392
+ diagnostics: KitDiagnostic[],
347
393
  ): Promise<Record<string, () => unknown>> {
348
394
  const dir = piAiApiDir(root)
349
395
  const out: Record<string, () => unknown> = {}
@@ -359,8 +405,10 @@ async function loadFallbackTreeFactories(
359
405
  }
360
406
  if (Object.keys(out).length < fallbackProtocols().length) {
361
407
  diagnostics.push(
362
- `pi-ai 协议兜底三元组未能全部加载(${Object.keys(out).join('/') || '空'});` +
363
- '请检查 dsh 安装树里 @earendil-works/pi-ai/dist/api 的形态',
408
+ degradation(
409
+ `pi-ai 协议兜底三元组未能全部加载(${Object.keys(out).join('/') || '空'});` +
410
+ '请检查 dsh 安装树里 @earendil-works/pi-ai/dist/api 的形态',
411
+ ),
364
412
  )
365
413
  }
366
414
  return out
@@ -373,7 +421,7 @@ async function loadFallbackTreeFactories(
373
421
  async function loadTreeProtocols(
374
422
  root: string,
375
423
  derived: string[],
376
- diagnostics: string[],
424
+ diagnostics: KitDiagnostic[],
377
425
  ): Promise<{ protocols: string[]; protocolFactories: Record<string, () => unknown> }> {
378
426
  const dir = piAiApiDir(root)
379
427
  const out: Record<string, () => unknown> = {}
@@ -381,27 +429,31 @@ async function loadTreeProtocols(
381
429
  const file = join(dir, `${api}.lazy.js`)
382
430
  if (!existsSync(file)) {
383
431
  diagnostics.push(
384
- `pi-ai 未提供协议 "${api}" 的惰性实现(缺 ${file});该协议不可用,其余协议不受影响`,
432
+ degradation(
433
+ `pi-ai 未提供协议 "${api}" 的惰性实现(缺 ${file});该协议不可用,其余协议不受影响`,
434
+ ),
385
435
  )
386
436
  continue
387
437
  }
388
438
  try {
389
439
  const factory = protocolFactoryOf((await import(pathToFileURL(file).href)) as never)
390
440
  if (factory === undefined) {
391
- diagnostics.push(`协议 "${api}" 的实现模块未导出唯一 *Api 工厂;该协议不可用`)
441
+ diagnostics.push(degradation(`协议 "${api}" 的实现模块未导出唯一 *Api 工厂;该协议不可用`))
392
442
  continue
393
443
  }
394
444
  out[api] = factory
395
445
  } catch (error) {
396
446
  diagnostics.push(
397
- `协议 "${api}" 的实现加载失败(${error instanceof Error ? error.message : String(error)});该协议不可用`,
447
+ degradation(
448
+ `协议 "${api}" 的实现加载失败(${error instanceof Error ? error.message : String(error)});该协议不可用`,
449
+ ),
398
450
  )
399
451
  }
400
452
  }
401
453
  if (Object.keys(out).length > 0) {
402
454
  return { protocols: derived.filter((api) => out[api] !== undefined), protocolFactories: out }
403
455
  }
404
- diagnostics.push('官方声明的协议一个都加载不到;回退内置三元组')
456
+ diagnostics.push(degradation('官方声明的协议一个都加载不到;回退内置三元组'))
405
457
  const fallback = await loadFallbackTreeFactories(root, diagnostics)
406
458
  return {
407
459
  protocols: fallbackProtocols().filter((api) => fallback[api] !== undefined),
@@ -465,7 +517,7 @@ function installOfficialSurface(options: {
465
517
  adapterModule: Record<string, unknown>
466
518
  catalogModule: Record<string, unknown>
467
519
  origin: string
468
- diagnostics: string[]
520
+ diagnostics: KitDiagnostic[]
469
521
  }): OfficialSurface {
470
522
  const { bundlePath, adapterModule, origin, diagnostics } = options
471
523
  const compatLabel = 'compat 门控表'
@@ -477,8 +529,10 @@ function installOfficialSurface(options: {
477
529
  // 快照模式下未知 compat 键**放行**(交给官方自身校验),只拦快照里明确的
478
530
  // withhold——表落后时"误拒官方新字段"比"多放一个键"严重得多。
479
531
  diagnostics.push(
480
- `${compatLabel}未能从官方 bundle 解析 COMPAT_GATES(${bundlePath});` +
481
- `使用内置快照(未知字段放行、withhold 仍拒绝;请检查 dsh-llm-pi-ai 打包形态)`,
532
+ degradation(
533
+ `${compatLabel}未能从官方 bundle 解析 COMPAT_GATES(${bundlePath});` +
534
+ `使用内置快照(未知字段放行、withhold 仍拒绝;请检查 dsh-llm-pi-ai 打包形态)`,
535
+ ),
482
536
  )
483
537
  installCompatTable(FALLBACK_TABLE)
484
538
  } else {
@@ -488,21 +542,27 @@ function installOfficialSurface(options: {
488
542
  }
489
543
  } catch (error) {
490
544
  const reason = error instanceof Error ? error.message : String(error)
491
- diagnostics.push(`${compatLabel}推导失败(${reason});使用内置快照(未知字段放行)`)
545
+ diagnostics.push(
546
+ degradation(`${compatLabel}推导失败(${reason});使用内置快照(未知字段放行)`),
547
+ )
492
548
  installCompatTable(FALLBACK_TABLE)
493
549
  }
494
550
 
495
551
  const derivedProtocols = deriveProtocols(adapterModule)
496
552
  if (derivedProtocols === undefined) {
497
553
  diagnostics.push(
498
- `${origin}:官方 supportedProtocols() 不可用;协议集合回退内置三元组 ${fallbackProtocols().join('/')}`,
554
+ degradation(
555
+ `${origin}:官方 supportedProtocols() 不可用;协议集合回退内置三元组 ${fallbackProtocols().join('/')}`,
556
+ ),
499
557
  )
500
558
  }
501
559
  const fields = deriveModelEntryFields(adapterModule)
502
560
  if (fields === undefined) {
503
561
  diagnostics.push(
504
- `${origin}:官方 Config schema 的模型条目字段集不可推导;` +
505
- `继承字段回退已知键 ${FALLBACK_MODEL_ENTRY_FIELDS.join('/')}(官方新增字段可能不被继承)`,
562
+ degradation(
563
+ `${origin}:官方 Config schema 的模型条目字段集不可推导;` +
564
+ `继承字段回退已知键 ${FALLBACK_MODEL_ENTRY_FIELDS.join('/')}(官方新增字段可能不被继承)`,
565
+ ),
506
566
  )
507
567
  }
508
568
  const generatedAt = catalogGeneratedAtOf(options.catalogModule)
@@ -570,6 +630,7 @@ function kitFromTree(options: {
570
630
  resolveImageAttachmentAccess: mods.llm[
571
631
  'resolveImageAttachmentAccess'
572
632
  ] as DshKit['resolveImageAttachmentAccess'],
633
+ recordKeyFor,
573
634
  }
574
635
  }
575
636
 
@@ -607,9 +668,9 @@ function vendoredProtocolFactories(): Record<string, () => unknown> {
607
668
  * npm 发布形态不携带 src/、lib 仅 index.js/invariant.js——resolveProfiles 与
608
669
  * auth 助手在此无条件走插件等价实现(内联,见文件头说明);compat 门控表则
609
670
  * 仍从 vendored 副本的 bundle + Config schema 现场推导(与 dsh 树同一推导链)。
610
- * @param diagnostics 可选诊断收集器(回退原因与逐项降级说明)。
671
+ * @param diagnostics 可选诊断收集器(回退原因、形态说明与逐项降级)。
611
672
  */
612
- export function loadVendoredKit(diagnostics: string[] = []): DshKit {
673
+ export function loadVendoredKit(diagnostics: KitDiagnostic[] = []): DshKit {
613
674
  const staticFactories = vendoredProtocolFactories()
614
675
  const surface = installOfficialSurface({
615
676
  bundlePath: vendoredAdapterBundlePath(),
@@ -658,9 +719,11 @@ export function loadVendoredKit(diagnostics: string[] = []): DshKit {
658
719
  authContextFrom: inlineAuthContextFrom,
659
720
  },
660
721
  resolveImageAttachmentAccess: vendoredLlm.resolveImageAttachmentAccess,
722
+ recordKeyFor: vendoredPiAiAdapter.recordKeyFor,
661
723
  ...(deepseek === undefined ? {} : { deepseek }),
662
724
  }
663
725
  assertKitShape(kit, 'vendored')
726
+ diagnostics.push(missingSourceNote(['profile 解析', '认证助手']))
664
727
  return kit
665
728
  }
666
729
 
@@ -689,14 +752,15 @@ export function dshTreeAnchor(
689
752
  /**
690
753
  * 解析运行时套件:优先 dsh 安装树(自动跟随上游),失败回退 vendored 副本;
691
754
  * 两者都过不了形状自检时抛错(调用方应记日志并放弃注册 route)。
692
- * 返回的 diagnostics 记录回退原因与逐项降级说明,供配置卡片与日志展示。
755
+ * 返回的 diagnostics 记录回退原因、打包形态说明(info)与逐项降级(degradation),
756
+ * 供配置卡片与日志分档展示。
693
757
  * @param installAnchor profile 的 dsh 安装锚点(`ctx.profileContext.installAnchor`)。
694
758
  */
695
759
  export async function resolveDshKit(installAnchor?: string | undefined): Promise<{
696
760
  kit: DshKit
697
- diagnostics: string[]
761
+ diagnostics: KitDiagnostic[]
698
762
  }> {
699
- const diagnostics: string[] = []
763
+ const diagnostics: KitDiagnostic[] = []
700
764
  const anchor = dshTreeAnchor(installAnchor)
701
765
  if (anchor !== undefined) {
702
766
  const treePkgDir = join(anchor, 'node_modules', '@deepseek-ai', 'dsh-llm-pi-ai')
@@ -729,39 +793,39 @@ export async function resolveDshKit(installAnchor?: string | undefined): Promise
729
793
  versions: collectTreeVersions(anchor),
730
794
  })
731
795
  assertKitShape(kit, 'dsh-tree')
732
- if (auth === undefined) {
733
- diagnostics.push(
734
- 'dsh 树不含 dsh-llm-pi-ai/src(npm 发布形态);认证助手使用插件内联等价实现',
735
- )
736
- }
737
- if (config === undefined) {
738
- diagnostics.push(
739
- 'dsh 树不含 dsh-llm-pi-ai/src(npm 发布形态);profile 解析使用插件等价实现(compat 门控对齐官方 catalog.ts)',
740
- )
741
- }
796
+ const missingSeams: string[] = []
797
+ if (config === undefined) missingSeams.push('profile 解析')
798
+ if (auth === undefined) missingSeams.push('认证助手')
799
+ if (missingSeams.length > 0) diagnostics.push(missingSourceNote(missingSeams))
742
800
  const tree = await importTreeDeepseek(anchor)
743
801
  if (tree.kit !== undefined) {
744
802
  return { kit: { ...kit, deepseek: tree.kit }, diagnostics }
745
803
  }
746
804
  diagnostics.push(
747
- `dsh 安装树的 dsh-llm-deepseek 不可用(${tree.problem ?? '未知原因'});` +
748
- 'adapter: deepseek 的 route 不可用,pi 路由不受影响',
805
+ degradation(
806
+ `dsh 安装树的 dsh-llm-deepseek 不可用(${tree.problem ?? '未知原因'});` +
807
+ 'adapter: deepseek 的 route 不可用,pi 路由不受影响',
808
+ ),
749
809
  )
750
810
  return { kit, diagnostics }
751
811
  } catch (error) {
752
812
  diagnostics.push(
753
- `dsh 安装树套件不可用(${anchor}):${error instanceof Error ? error.message : String(error)};回退 vendored 副本`,
813
+ degradation(
814
+ `dsh 安装树套件不可用(${anchor}):${error instanceof Error ? error.message : String(error)};回退 vendored 副本`,
815
+ ),
754
816
  )
755
817
  }
756
818
  } else {
757
819
  diagnostics.push(
758
- '未能从 profileContext.installAnchor / process.argv[1] 定位 dsh 安装树;回退 vendored 副本',
820
+ degradation(
821
+ '未能从 profileContext.installAnchor / process.argv[1] 定位 dsh 安装树;回退 vendored 副本',
822
+ ),
759
823
  )
760
824
  }
761
825
  const kit = loadVendoredKit(diagnostics)
762
826
  if (kit.deepseek === undefined) {
763
827
  diagnostics.push(
764
- 'vendored 副本的 dsh-llm-deepseek 形状不兼容;adapter: deepseek 的 route 不可用',
828
+ degradation('vendored 副本的 dsh-llm-deepseek 形状不兼容;adapter: deepseek 的 route 不可用'),
765
829
  )
766
830
  }
767
831
  return { kit, diagnostics }
package/src/service.ts CHANGED
@@ -47,7 +47,7 @@ import {
47
47
  } from './official-copy-writer.ts'
48
48
  import { assertServiceable, buildProfiles, isDraftRoute } from './profiles.ts'
49
49
  import { buildDeepseekRoutes, type ResolvedDeepseekRoute } from './profiles-deepseek.ts'
50
- import { type DshKit, resolveDshKit } from './resolve-dsh.ts'
50
+ import { type DshKit, degradation, type KitDiagnostic, resolveDshKit } from './resolve-dsh.ts'
51
51
 
52
52
  /** 配置页「运行期状态」所需的事实(纯 JSON,可直接进 HTTP 响应)。 */
53
53
  export interface LlmPiKitInfo {
@@ -71,8 +71,11 @@ export interface LlmPiKitInfo {
71
71
  compatSource: string
72
72
  /** 官方应急副本状态(路径/生成时间/覆盖 route/警告;error = 最近生成失败)。 */
73
73
  officialCopy: OfficialCopyStatus
74
- /** 回退与逐项降级诊断(有内容时界面显式展示)。 */
75
- diagnostics: string[]
74
+ /**
75
+ * 套件诊断(分档,界面分块展示):`info` = 打包形态说明(恒定事实、非缺陷),
76
+ * `degradation` = 回退与逐项降级。
77
+ */
78
+ diagnostics: KitDiagnostic[]
76
79
  }
77
80
 
78
81
  export interface LlmPiRuntime {
@@ -132,13 +135,16 @@ function makeResolveApiKey(ctx: Context, kit: DshKit) {
132
135
  }
133
136
  }
134
137
 
135
- /** 解析运行期套件并落启动日志(来源/生效版本/协议/逐项降级诊断)。 */
138
+ /** 解析运行期套件并落启动日志(来源/生效版本/协议/形态说明与逐项降级)。 */
136
139
  async function loadRuntimeKit(
137
140
  ctx: Context,
138
141
  logger: PluginLogger,
139
- ): Promise<{ kit: DshKit; diagnostics: string[] }> {
142
+ ): Promise<{ kit: DshKit; diagnostics: KitDiagnostic[] }> {
140
143
  const { kit, diagnostics } = await resolveDshKit(ctx.get('profileContext')?.installAnchor)
141
- for (const line of diagnostics) logger.warn(line)
144
+ for (const item of diagnostics) {
145
+ if (item.level === 'info') logger.info(item.message)
146
+ else logger.warn(item.message)
147
+ }
142
148
  logger.info(
143
149
  `运行时套件来源:${kit.source};pi-ai ${kit.versions.piAi ?? '?'} / ` +
144
150
  `dsh-llm-pi-ai ${kit.versions.piAiAdapter ?? '?'};协议 ${kit.protocols.join('/')}`,
@@ -479,8 +485,8 @@ export async function startRuntime(
479
485
  officialCopy: copy,
480
486
  diagnostics: [
481
487
  ...diagnostics,
482
- ...copy.warnings.map((warning) => `应急副本:${warning}`),
483
- ...(copy.error === undefined ? [] : [`应急副本:生成失败(${copy.error})`]),
488
+ ...copy.warnings.map((warning) => degradation(`应急副本:${warning}`)),
489
+ ...(copy.error === undefined ? [] : [degradation(`应急副本:生成失败(${copy.error})`)]),
484
490
  ],
485
491
  }
486
492
  }