@dsh-plus/llm-pi 0.1.45 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,10 +1,13 @@
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, unwrapVolatile } from "@dsh-plus/shared";
4
+ import { atomicWriteFile, hasVolatileRefs, isVolatileRef, unwrapVolatile } from "@dsh-plus/shared";
5
5
  import { existsSync, readFileSync, realpathSync } from "node:fs";
6
6
  import { dirname, join, resolve } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
+ import { access, stat } from "node:fs/promises";
9
+ import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
10
+ import { stringify } from "yaml";
8
11
  import { credentialKeyId, credentialKeyScope, credentialRef, isCredentialKeySegment, isCredentialRefName } from "@deepseek-ai/dsh-credentials";
9
12
  import * as vendoredAnonId from "@deepseek-ai/dsh-anonymous-user-id";
10
13
  import * as vendoredLlm from "@deepseek-ai/dsh-llm";
@@ -16,13 +19,12 @@ import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messag
16
19
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
17
20
  import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
18
21
  import * as vendoredCatalog from "@earendil-works/pi-ai/providers/all";
19
- import { access } from "node:fs/promises";
20
22
  import { homedir } from "node:os";
21
23
  //#endregion
22
24
  //#region src/config.ts
23
25
  /**
24
26
  * 配置单一事实源:cordis 行级 Config(组合默认值)与 settings namespace
25
- *(用户层,经 dsh-settings-file 持久化到 $DSH_HOME/settings.yaml)共用同一
27
+ *(用户层=profile 行级覆盖层 cordis.patch.yml,热生效)共用同一
26
28
  * schemastery schema。无密钥字段(apiKeyEnv 是凭据引用名而非密钥本身)。
27
29
  *
28
30
  * 与官方 llm-pi-ai 的差异:
@@ -33,10 +35,10 @@ import { homedir } from "node:os";
33
35
  * 官方将来新增协议无需改本插件。
34
36
  * @module llm-pi/config
35
37
  */
36
- /** settings 命名空间(字面量即合法命名空间,0.1.2-alpha.2 起编译期校验;webui 配置卡片与插件运行期读取同一份)。 */
38
+ /** settings 命名空间(字面量即合法命名空间,编译期校验;webui 配置卡片与插件运行期读取同一份)。 */
37
39
  const SETTINGS_NS = "dsh-plus-llm-pi";
38
40
  /**
39
- * 官方适配器不可用/推导失败时的协议兜底三元组(自 0.1.2-alpha 线以来的稳定集)。
41
+ * 官方适配器不可用/推导失败时的协议兜底三元组。
40
42
  * 运行期生效集合来自已安装官方包的 `supportedProtocols()`(见 resolve-dsh.ts),
41
43
  * 本常量只用于 schema 描述、浏览器半首帧兜底与推导失败回退。
42
44
  */
@@ -66,8 +68,6 @@ const DEEPSEEK_REASONING_EFFORTS = [
66
68
  "high",
67
69
  "max"
68
70
  ];
69
- /** 旧版 imageDetail 枚举(0.1.2-alpha.1 已移除该字段;仅用于 schema 透传陷阱的取值集)。 */
70
- const DEEPSEEK_IMAGE_DETAILS = ["auto", "low"];
71
71
  /** dsh-timeout 的定时器上限(与官方 MAX_TIMER_DELAY_MS 对齐)。 */
72
72
  const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
73
73
  /**
@@ -101,8 +101,7 @@ const modelEntry = z.object({
101
101
  reasoningEfforts: z.union([z.const(false), reasoningEfforts]).description("可选 reasoning 档位:false=非推理模型;dict=档位→线值映射;缺省继承内置目录能力"),
102
102
  compat: compatDict.description("模型级 compat(字段级合并,压过 route 级与继承值)"),
103
103
  imagePixelBudget: z.number().step(1).min(1).description("(仅 adapter: deepseek)单张请求图片的总像素预算;缺省继承官方目录"),
104
- imageMaxBytes: z.number().step(1).min(1).description("(仅 adapter: deepseek)单张请求图片的编码字节上限;缺省继承官方目录"),
105
- imageDetail: z.union(DEEPSEEK_IMAGE_DETAILS).description("(已移除)图片细节档位;0.1.2-alpha.1 起请改用 imagePixelBudget/imageMaxBytes")
104
+ imageMaxBytes: z.number().step(1).min(1).description("(仅 adapter: deepseek)单张请求图片的编码字节上限;缺省继承官方目录")
106
105
  });
107
106
  const providerProfile = z.object({
108
107
  adapter: z.union(ADAPTER_KINDS).description("路由适配器:pi=PiAiAdapter(默认,协议集合随已装官方适配器推导);deepseek=官方 DeepSeekAdapter(图片走 Files API 文件通道,失败自动降级 base64)").default("pi"),
@@ -132,7 +131,7 @@ const providerProfile = z.object({
132
131
  timeoutMs: z.natural().description("HTTP/provider 超时毫秒"),
133
132
  websocketConnectTimeoutMs: z.natural().description("WebSocket 连接超时毫秒"),
134
133
  streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).description("单支流式读取的最大空闲间隔毫秒"),
135
- maxRequestImageBytes: z.natural().description("单请求 base64 图片载荷上限字节;缺省 20MiB(0.1.2-alpha.1 起官方必需字段,旧配置免改)"),
134
+ maxRequestImageBytes: z.natural().description("单请求 base64 图片载荷上限字节;缺省 20MiB(官方必需字段,旧配置免改)"),
136
135
  requestImagePixelBudget: z.number().step(1).min(1).description("单请求每个确定性内联图片版本的像素总预算;缺省 2048x2048(官方同值)"),
137
136
  requestImageMaxBytes: z.number().step(1).min(1).description("内联图片版本编码字节目标;缺省 1MiB(官方同值)"),
138
137
  retryPolicy: z.any().description("provider 重试策略(dsh-llm RetryPolicy 形状,构建期校验)"),
@@ -515,7 +514,7 @@ function browseModels(kit, query = {}) {
515
514
  * 3. **模型条目字段集**:Config schema 的 `providers.*.models` 键集——继承时按此
516
515
  * 透传 pi-ai 目录的同名字段,官方将来新增模型级字段即自动跟随。
517
516
  *
518
- * 历史教训(0.1.5-rc.1):compat 门控曾是官方 catalog.ts 的手抄镜像,官方扩容
517
+ * 历史教训(详见 docs/repo/事故记录.md):compat 门控曾是官方的手抄镜像,官方扩容
519
518
  * offer 字段而旧表漏收,导致官方可配字段被本插件**误拒**(静默功能缺失、无报错)。
520
519
  * 现改为现场推导,`tests/official-surface.test.ts` 直读官方副本守门推导正确性。
521
520
  *
@@ -650,10 +649,18 @@ const FALLBACK_MODEL_ENTRY_FIELDS = [
650
649
  function fallbackProtocols() {
651
650
  return [...FALLBACK_PROTOCOLS];
652
651
  }
653
- /** 官方门控表不可解析时的最后已知快照(0.1.5-rc.2 实测值)。 */
652
+ /**
653
+ * 官方门控表不可解析时的兜底快照(0.2.1-alpha.1 现场推导值)。
654
+ *
655
+ * 只在官方改打包形态、`COMPAT_GATES` 解析失败时启用(此时未知键放行、只拦快照里
656
+ * 明确 withhold 的键,见 compat.ts 的「语义分两档」)。快照过期会重新引入
657
+ * 「误拒官方新字段」那类静默故障,故由 tests/official-surface.test.ts 的
658
+ * 「兜底快照不得过期」用例与现场推导逐协议比对守门。
659
+ */
654
660
  const FALLBACK_TABLE = {
655
661
  source: "fallback",
656
662
  gates: {
663
+ "mistral-conversations": { supportsMidConvoSystemMessages: "withhold" },
657
664
  "openai-completions": {
658
665
  supportsStore: "offer",
659
666
  supportsDeveloperRole: "offer",
@@ -679,7 +686,8 @@ const FALLBACK_TABLE = {
679
686
  zaiToolStream: "withhold",
680
687
  supportsOpenAIGrammarTools: "withhold",
681
688
  sendSessionAffinityHeaders: "withhold",
682
- deferredToolsMode: "withhold",
689
+ supportsMidConvoSystemMessages: "withhold",
690
+ supportsMidConvoToolAdditions: "withhold",
683
691
  sessionAffinityFormat: "withhold"
684
692
  },
685
693
  "openai-responses": {
@@ -691,7 +699,32 @@ const FALLBACK_TABLE = {
691
699
  supportsOpenAIGrammarTools: "withhold",
692
700
  supportsAdditionalTools: "withhold",
693
701
  supportsToolSearch: "withhold",
694
- supportsExplicitPromptCacheMode: "withhold"
702
+ supportsExplicitPromptCacheMode: "withhold",
703
+ supportsMidConvoSystemMessages: "withhold"
704
+ },
705
+ "azure-openai-responses": {
706
+ supportsDeveloperRole: "offer",
707
+ supportsMaxOutputTokens: "offer",
708
+ supportsStrictMode: "offer",
709
+ supportsLongCacheRetention: "offer",
710
+ sessionAffinityFormat: "withhold",
711
+ supportsOpenAIGrammarTools: "withhold",
712
+ supportsAdditionalTools: "withhold",
713
+ supportsToolSearch: "withhold",
714
+ supportsExplicitPromptCacheMode: "withhold",
715
+ supportsMidConvoSystemMessages: "withhold"
716
+ },
717
+ "openai-codex-responses": {
718
+ supportsDeveloperRole: "offer",
719
+ supportsMaxOutputTokens: "offer",
720
+ supportsStrictMode: "offer",
721
+ supportsLongCacheRetention: "offer",
722
+ sessionAffinityFormat: "withhold",
723
+ supportsOpenAIGrammarTools: "withhold",
724
+ supportsAdditionalTools: "withhold",
725
+ supportsToolSearch: "withhold",
726
+ supportsExplicitPromptCacheMode: "withhold",
727
+ supportsMidConvoSystemMessages: "withhold"
695
728
  },
696
729
  "anthropic-messages": {
697
730
  supportsEagerToolInputStreaming: "offer",
@@ -702,10 +735,13 @@ const FALLBACK_TABLE = {
702
735
  allowEmptySignature: "offer",
703
736
  supportsStrictTools: "offer",
704
737
  sendSessionAffinityHeaders: "withhold",
705
- supportsToolReferences: "withhold",
738
+ sessionAffinityFormat: "withhold",
739
+ supportsMidConvoSystemMessages: "withhold",
740
+ supportsMidConvoToolChanges: "withhold",
706
741
  supportsMidConvoEffort: "withhold",
707
742
  allowedFallbackModels: "withhold"
708
- }
743
+ },
744
+ "bedrock-converse-stream": { supportsStrictMode: "offer" }
709
745
  },
710
746
  specs: {
711
747
  supportsStore: "boolean",
@@ -771,6 +807,10 @@ function compatTableInfo() {
771
807
  problem: active.problem
772
808
  };
773
809
  }
810
+ /** 某协议某字段的可配性('offer'/'withhold';未列出 = 无此字段)。 */
811
+ function compatDispositionOf(api, field) {
812
+ return active.gates[api]?.[field];
813
+ }
774
814
  /** 某协议全部可配置(offer)的 compat 键(UI 渲染字段组与校验共用)。 */
775
815
  function compatFieldsOf(api) {
776
816
  const gate = active.gates[api];
@@ -815,7 +855,7 @@ function checkValue(field, spec, value, where) {
815
855
  }
816
856
  /**
817
857
  * 校验一份 compat 字典对指定协议合法(对齐官方门控语义 + schema 值约束):
818
- * - 未知键/withhold 字段拒绝(官方写时拒绝,替代旧版静默丢弃);
858
+ * - 未知键/withhold 字段拒绝(与官方写时拒绝同款);
819
859
  * - 值类型/枚举按官方 schema 校验;
820
860
  * - 无值键(null/undefined)拒绝(官方 assertOfferedCompatFields 同款)。
821
861
  */
@@ -1521,7 +1561,6 @@ function declaredModalities(configured) {
1521
1561
  /** 物化单个模型目录条目:官方 base 在下,条目显式字段逐字段覆盖。 */
1522
1562
  function materializeModel$1(route, entry, deps) {
1523
1563
  if (entry.reasoningEfforts !== void 0 || nonEmptyDict(entry.compat)) invalid$1(route, `model "${entry.id}" 的 reasoningEfforts/compat 仅 adapter: pi 可用`);
1524
- if (entry.imageDetail !== void 0) invalid$1(route, `model "${entry.id}" 的 imageDetail 已随 0.1.2-alpha.1 移除;请改用 imagePixelBudget/imageMaxBytes(低细节可配更小的 imagePixelBudget)并删除 imageDetail`);
1525
1564
  const base = resolveOfficialBase(route, entry, deps);
1526
1565
  const input = declaredModalities(entry.input) ?? base.input;
1527
1566
  const imageFields = {
@@ -1582,7 +1621,7 @@ function rawAdapterConfig(profile, baseURL, models) {
1582
1621
  /** 物化单个 deepseek route;lenient 下失败告警并返回 null(调用方跳过注册)。 */
1583
1622
  function buildRoute(route, profile, deps) {
1584
1623
  try {
1585
- if (deps.kit.deepseek === void 0) invalid$1(route, "使用 adapter: deepseek,但当前运行时套件不含 dsh-llm-deepseek(0.1.2-alpha.1 基线)");
1624
+ if (deps.kit.deepseek === void 0) invalid$1(route, "使用 adapter: deepseek,但当前运行时套件不含 dsh-llm-deepseek(本仓基线的必需套件)");
1586
1625
  rejectPiOnlyFields(route, profile);
1587
1626
  if (profile.extends !== void 0 && profile.extends !== "deepseek") invalid$1(route, `的 extends ${JSON.stringify(profile.extends)} 非法:adapter: deepseek 仅支持 "deepseek"(官方目录)`);
1588
1627
  if (profile.apiKeyEnv === void 0 || profile.apiKeyEnv.length === 0) invalid$1(route, "需要 apiKeyEnv:DeepSeekAdapter 无环境自发现,凭据引用必须显式配置");
@@ -1619,7 +1658,7 @@ function buildDeepseekRoutes(providers, deps) {
1619
1658
  //#endregion
1620
1659
  //#region src/profiles.ts
1621
1660
  /**
1622
- * profile 构建器(0.1.2-alpha.2 复核兼容):插件 Config → 官方 schema 形状的
1661
+ * profile 构建器:插件 Config → 官方 schema 形状的
1623
1662
  * 「前置数据源归一化」→ 官方 resolveProfiles(或插件等价兜底)→
1624
1663
  * ResolvedPiAiProviderProfile。
1625
1664
  *
@@ -1734,7 +1773,6 @@ function reasoningEffortsFromBase(provider, entry, base) {
1734
1773
  * 返回 null(调用方跳过该模型)。
1735
1774
  */
1736
1775
  function materializeModel(route, profile, entry, base, routeApi, defaultInput, deps) {
1737
- if (entry.imageDetail !== void 0) invalid(route, `model "${entry.id}" 的 imageDetail 已随 0.1.2-alpha.1 移除;请改用 imagePixelBudget/imageMaxBytes(仅 adapter: deepseek)并删除 imageDetail`);
1738
1776
  const api = profile.api ?? base.api ?? routeApi;
1739
1777
  if (api === void 0) {
1740
1778
  if (deps.lenient) {
@@ -2044,11 +2082,390 @@ function buildProfiles(providers, deps) {
2044
2082
  }
2045
2083
  return resolved;
2046
2084
  }
2085
+ /**
2086
+ * 应急副本用:逐 route 归一化为官方 schema 形状的 profile(与运行期同一条
2087
+ * normalizeRoute 链:extends 物化、单协议单端点约束、模型条目全显式)。
2088
+ *
2089
+ * 与运行期 buildProfiles 的两点差异:
2090
+ * - 不受 `enabled` 开关影响——副本是配置层事实,插件关闭时其 route 仍应可复制;
2091
+ * - 逐 route 隔离失败(不依赖 lenient 开关)——一个坏条目只丢它自己,
2092
+ * 不让整份应急副本变空。
2093
+ *
2094
+ * adapter: deepseek 的 route 与草稿路由不在此列(前者由 profiles-deepseek
2095
+ * 物化、后者本就不可服务),调用方按缺席另行告警。
2096
+ */
2097
+ function normalizeOfficialRoutes(providers, deps) {
2098
+ const out = /* @__PURE__ */ new Map();
2099
+ for (const [route, profile] of Object.entries(providers ?? {})) try {
2100
+ const normalized = normalizeRoute(route, profile, deps);
2101
+ if (normalized !== void 0) out.set(route, normalized.profile);
2102
+ } catch (error) {
2103
+ deps.warn?.(`llm-pi: provider "${route}" 无法物化为官方形状(${error instanceof Error ? error.message : String(error)}),已跳过应急副本的该 route`);
2104
+ }
2105
+ return out;
2106
+ }
2047
2107
  /** settings 写入校验钩子:完整试跑解析,非法配置在写入处拒绝。 */
2048
2108
  function assertServiceable(config, deps) {
2049
2109
  buildProfiles(config.providers, deps);
2050
2110
  buildDeepseekRoutes(config.providers, deps);
2051
2111
  }
2112
+ //#endregion
2113
+ //#region src/official-copy.ts
2114
+ /**
2115
+ * 官方应急副本的翻译与渲染(纯函数,零 IO)。
2116
+ *
2117
+ * 产出一份「官方可识别」的 profile patch 覆盖层:把 dsh-plus-llm-pi 的 route
2118
+ * 配置改写为官方 `llm-pi-ai` 的 Config 形状,另附一行禁用 dsh-plus-llm-pi 的
2119
+ * 覆盖(应用即切换,同名 route 不撞 DUPLICATE_ADAPTER)。写盘、逐 route 官方
2120
+ * 校验与状态管理见 official-copy-writer.ts。
2121
+ *
2122
+ * 三条翻译底线(对旧「出错自动改写」失效教训的根治):
2123
+ * 1. **只产出官方 schema 声明的字段**:route 键集从官方 Config schema 现场推导
2124
+ * (失败回退内置键集),模型条目按 kit.officialModelFields 过滤——官方拒收
2125
+ * 的字段根本不出现在副本里;
2126
+ * 2. **compat 只留 offer**:withhold 与未知键按生效门控表丢弃(目录继承值可能
2127
+ * 携带 withhold 键,写时会被官方拒绝);
2128
+ * 3. **继承全物化**:官方无 extends——模型条目来自 normalizeRoute 的全显式物化
2129
+ * 产物(单协议、单端点约束由该链保证,无需拆分 route)。
2130
+ *
2131
+ * deepseek 路由改写为 openai-completions(中继网关本就是 OpenAI 兼容面):
2132
+ * 文件通道与思考配置不迁移(官方 llm-pi-ai 无对应字段/无逐模型 reasoningEfforts),
2133
+ * 以 warning 明示,绝不静默丢弃。
2134
+ * @module llm-pi/official-copy
2135
+ */
2136
+ /** 副本文件在 Harness home(.dsh)根目录的落点;lifeboat 面板按同一路径消费(文件契约)。 */
2137
+ const OFFICIAL_COPY_PATH = dshHomePath("llm-pi.official-patch.yaml");
2138
+ /** deepseek 路由改写所用线协议:中继网关对外提供 OpenAI 兼容 chat completions 面。 */
2139
+ const DEEPSEEK_COPY_API = "openai-completions";
2140
+ /**
2141
+ * 官方 Config schema 布局不可识别时的 route 字段兜底(官方 PiAiProviderProfile
2142
+ * 最后已知键集)。与官方新增字段的失配只表现为「新字段不进副本」,不弄挂生成。
2143
+ */
2144
+ const FALLBACK_ROUTE_FIELDS = [
2145
+ "apiKeyEnv",
2146
+ "displayName",
2147
+ "api",
2148
+ "baseURL",
2149
+ "models",
2150
+ "modelOverrides",
2151
+ "compat",
2152
+ "defaultContextWindow",
2153
+ "defaultMaxTokens",
2154
+ "defaultInput",
2155
+ "headers",
2156
+ "reasoning",
2157
+ "thinkingBudgets",
2158
+ "cacheRetention",
2159
+ "transport",
2160
+ "timeoutMs",
2161
+ "websocketConnectTimeoutMs",
2162
+ "streamIdleTimeoutMs",
2163
+ "maxRequestImageBytes",
2164
+ "requestImagePixelBudget",
2165
+ "requestImageMaxBytes",
2166
+ "retryPolicy"
2167
+ ];
2168
+ /** deepseek 专有且官方 llm-pi-ai 无对应字段的 raw 键(出现即记 warning)。 */
2169
+ const DEEPSEEK_UNMIGRATED_FIELDS = [
2170
+ "thinking",
2171
+ "reasoningEffort",
2172
+ "maxRequestFilesBytes",
2173
+ "filesApiTimeoutMs",
2174
+ "fileExpiresAfterSeconds",
2175
+ "fileRefreshMarginSeconds",
2176
+ "maxImagesPerRequest",
2177
+ "imageOffloadByteQuantum",
2178
+ "inlineImageOffloadByteQuantum",
2179
+ "imageOffloadCountQuantum"
2180
+ ];
2181
+ /**
2182
+ * 从官方 Config schema 推导 route 字段键集(`providers.*` 的 dict 键);
2183
+ * 布局不可识别/键集为空返回 undefined,调用方回退内置键集并告警。
2184
+ */
2185
+ function deriveOfficialRouteFields(config) {
2186
+ const fields = config.dict?.["providers"]?.inner?.dict;
2187
+ if (fields === void 0) return void 0;
2188
+ const keys = Object.keys(fields);
2189
+ return keys.length > 0 ? keys : void 0;
2190
+ }
2191
+ /** compat 过滤:只保留该协议 offer 的字段(withhold/未知键丢弃);无保留项返回 undefined。 */
2192
+ function offerCompat(api, compat) {
2193
+ if (compat === void 0 || typeof compat !== "object") return void 0;
2194
+ const out = {};
2195
+ for (const [field, value] of Object.entries(compat)) {
2196
+ if (value === void 0 || value === null) continue;
2197
+ if (compatDispositionOf(api, field) === "offer") out[field] = value;
2198
+ }
2199
+ return Object.keys(out).length > 0 ? out : void 0;
2200
+ }
2201
+ /** 单个模型条目 → 官方键集内的条目(compat 另过 offer 门控)。 */
2202
+ function filterModel(api, model, modelFields) {
2203
+ const source = model;
2204
+ const out = {};
2205
+ for (const key of modelFields) {
2206
+ const value = source[key];
2207
+ if (value === void 0) continue;
2208
+ if (key === "compat") {
2209
+ const compat = offerCompat(api, value);
2210
+ if (compat !== void 0) out.compat = compat;
2211
+ continue;
2212
+ }
2213
+ out[key] = value;
2214
+ }
2215
+ return out;
2216
+ }
2217
+ /** 归一化 profile → 官方 route 字段内的条目(models 逐条过滤)。 */
2218
+ function filterRoute(profile, routeFields, modelFields) {
2219
+ const source = profile;
2220
+ const api = typeof source["api"] === "string" ? source["api"] : "";
2221
+ const out = {};
2222
+ for (const key of routeFields) {
2223
+ const value = source[key];
2224
+ if (value === void 0) continue;
2225
+ if (key === "models") {
2226
+ out.models = (Array.isArray(value) ? value : []).map((model) => filterModel(api, model, modelFields));
2227
+ continue;
2228
+ }
2229
+ out[key] = value;
2230
+ }
2231
+ return out;
2232
+ }
2233
+ /** deepseek 单模型 → 官方条目(inputModalities 改名 input;图像预算不迁移)。 */
2234
+ function deepseekModelEntry(model) {
2235
+ const out = { id: model.id };
2236
+ if (model.name !== void 0) out.name = model.name;
2237
+ if (model.contextWindow !== void 0) out.contextWindow = model.contextWindow;
2238
+ if (model.maxTokens !== void 0) out.maxTokens = model.maxTokens;
2239
+ if (model.inputModalities !== void 0) out.input = [...model.inputModalities];
2240
+ return out;
2241
+ }
2242
+ /**
2243
+ * deepseek 路由 → 官方 llm-pi-ai profile:连接事实取 resolveAdapterOptions 的
2244
+ * 校验产物(端点/凭据引用/限额),模型取官方目录物化结果。思考配置与文件通道
2245
+ * 字段不迁移(llm-pi-ai 无对应语义),逐项列入 unmirrored。
2246
+ */
2247
+ function deepseekCopyProfile(raw, resolved) {
2248
+ const connection = resolved.connection;
2249
+ const unmirrored = DEEPSEEK_UNMIGRATED_FIELDS.filter((key) => raw[key] !== void 0);
2250
+ if (raw.models?.some((m) => m.imagePixelBudget !== void 0 || m.imageMaxBytes !== void 0)) unmirrored.push("模型级 imagePixelBudget/imageMaxBytes");
2251
+ const profile = {
2252
+ displayName: resolved.displayName,
2253
+ api: DEEPSEEK_COPY_API,
2254
+ baseURL: connection.baseURL,
2255
+ apiKeyEnv: raw.apiKeyEnv,
2256
+ defaultContextWindow: connection.defaultContextWindow,
2257
+ defaultMaxTokens: connection.maxTokens,
2258
+ streamIdleTimeoutMs: connection.streamIdleTimeoutMs,
2259
+ models: connection.models.map(deepseekModelEntry)
2260
+ };
2261
+ if (raw.retryPolicy !== void 0) profile.retryPolicy = raw.retryPolicy;
2262
+ if (raw.maxInlineRequestImageBytes !== void 0) profile.maxRequestImageBytes = connection.maxInlineRequestImageBytes;
2263
+ return {
2264
+ profile,
2265
+ unmirrored
2266
+ };
2267
+ }
2268
+ /** 翻译整份配置为官方副本内容(纯函数;坏 route 的跳过告警由上游 warn 通道汇集)。 */
2269
+ function buildOfficialCopy(input) {
2270
+ const warnings = [];
2271
+ const providers = {};
2272
+ for (const [route, raw] of Object.entries(input.providers)) {
2273
+ if ((raw.adapter ?? "pi") === "deepseek") {
2274
+ const resolved = input.deepseekResolved.get(route);
2275
+ if (resolved === void 0) continue;
2276
+ const { profile, unmirrored } = deepseekCopyProfile(raw, resolved);
2277
+ providers[route] = profile;
2278
+ if (unmirrored.length > 0) warnings.push(`deepseek route "${route}" 的字段未迁移到 llm-pi-ai:${unmirrored.join("、")}`);
2279
+ continue;
2280
+ }
2281
+ const normalized = input.normalizedPi.get(route);
2282
+ if (normalized === void 0) {
2283
+ if (isDraftRoute(raw)) warnings.push(`草稿 route "${route}" 未纳入副本(尚无模型)`);
2284
+ continue;
2285
+ }
2286
+ providers[route] = filterRoute(normalized, input.routeFields, input.modelFields);
2287
+ }
2288
+ for (const [route, profile] of Object.entries(input.manualProviders ?? {})) if (profile !== null && typeof profile === "object") providers[route] = profile;
2289
+ return {
2290
+ providers,
2291
+ warnings
2292
+ };
2293
+ }
2294
+ /** 头注释:用途、两种应用方式、切换语义、回退方法与警告清单(确定性输出,mtime 即生成时间)。 */
2295
+ function renderHeader(copy, meta) {
2296
+ const profile = meta.profile ?? "<profile>";
2297
+ return [
2298
+ "# 官方应急 LLM 配置副本 —— @dsh-plus/llm-pi 自动生成(勿手改:下次配置变更会被覆盖;文件 mtime 即最近生成时间)",
2299
+ `# 来源 profile:${profile}`,
2300
+ "# 用途:dsh-plus-llm-pi 缺席/故障时,用本文件把其 route 以官方 llm-pi-ai 配置恢复 LLM 能力。",
2301
+ "# 应用(二选一,重启/重载后生效):",
2302
+ `# 1) 单次启动:dsh ${profile} --patch ${OFFICIAL_COPY_PATH}`,
2303
+ `# 2) 长期生效:把本文件条目并入该 profile 的 cordis.patch.yml(~/.dsh/profiles/${profile}/)`,
2304
+ "# 语义:应用即切换 —— 副本禁用 dsh-plus-llm-pi,并整行替换 llm-pi-ai 的 config",
2305
+ "# (官方 llm-pi-ai 现有 providers 已合并保留;同名 key 以手写条目优先)。",
2306
+ "# 回退:移除 --patch 参数或删除并入的条目(含 dsh-plus-llm-pi 的 disabled 行),重启/重载。",
2307
+ `# 本次内容:${Object.keys(copy.providers).length} 个官方 provider route。`,
2308
+ ...copy.warnings.map((warning) => `# 警告:${warning}`)
2309
+ ].join("\n");
2310
+ }
2311
+ /**
2312
+ * 渲染副本文件内容(确定性:同输入同字节,mtime 承载时间语义)。
2313
+ * 零 route 时渲染空补丁列表 `[]`(应用无效果)——绝不产出「只禁用不供给」的误导性副本。
2314
+ */
2315
+ function renderOfficialCopy(copy, meta) {
2316
+ const header = renderHeader(copy, meta);
2317
+ if (Object.keys(copy.providers).length === 0) return `${header}\n# 当前没有可迁移的 route(llm-pi 未配置或全部不可服务),空补丁应用无效果。\n[]\n`;
2318
+ const rows = [{
2319
+ id: "dsh-plus-llm-pi",
2320
+ disabled: true
2321
+ }, {
2322
+ id: "llm-pi-ai",
2323
+ config: { providers: copy.providers }
2324
+ }];
2325
+ return `${header}\n${stringify(rows, { lineWidth: 0 })}`;
2326
+ }
2327
+ //#endregion
2328
+ //#region src/official-copy-writer.ts
2329
+ /**
2330
+ * 官方应急副本的生成编排:装配(原始配置 → 物化 → 翻译)→ 逐 route 官方校验
2331
+ * → 备忘跳过 / 原子落盘 → 状态供配置卡与 lifeboat 面板消费。
2332
+ *
2333
+ * 触发时机:插件启动、配置热更新(loader/volatile-update)、adapter 注册变化
2334
+ * (llm/adapters-updated,官方 llm-pi-ai 晚注册时手写条目在此补入)。无定时器、
2335
+ * 无 watcher——平时零开销;写失败只降级为状态与日志,绝不影响路由注册。
2336
+ *
2337
+ * 逐 route 官方校验是「副本必然官方可识别」的执行点:官方 Config schema 解析
2338
+ * (非法协议/越界取值在此抛错)+ 官方 resolveProfiles 可服务性链;任一不过即
2339
+ * 剔除该 route 并记 warning——写不进官方的东西绝不落盘。
2340
+ * @module llm-pi/official-copy-writer
2341
+ */
2342
+ /** 官方 llm-pi-ai 的 settings 命名空间(与插件 NS 同字面量,官方 0.1.x 起稳定)。 */
2343
+ const NS_OFFICIAL_PI_AI = "llm-pi-ai";
2344
+ /**
2345
+ * 手写官方条目来源:settings describe 的 llm-pi-ai 生效视图(composition base
2346
+ * = profile patch 行 + settings 用户层,schema 已解析、已过官方 validate)——
2347
+ * 官方 seam,无需自行解析 patch 文件。ns 未注册/读取失败时降级为「仅本插件
2348
+ * 改写条目」并附 warning,不阻断生成。
2349
+ */
2350
+ function readManualProviders(settings) {
2351
+ try {
2352
+ const row = settings.describe().find((entry) => entry.ns === NS_OFFICIAL_PI_AI);
2353
+ if (row === void 0) return { warning: "官方 llm-pi-ai 未注册(插件缺席),副本仅含本插件改写条目" };
2354
+ const raw = row.value?.providers;
2355
+ const providers = isVolatileRef(raw) ? raw.get() : raw;
2356
+ if (providers === null || typeof providers !== "object") return {};
2357
+ return { providers };
2358
+ } catch (error) {
2359
+ return { warning: `读取官方 llm-pi-ai 生效配置失败(${error instanceof Error ? error.message : String(error)}),副本仅含本插件改写条目` };
2360
+ }
2361
+ }
2362
+ /** 文件是否已存在(内容备忘跳过后自愈外部删除)。 */
2363
+ async function fileExists(path) {
2364
+ try {
2365
+ return (await stat(path)).isFile();
2366
+ } catch {
2367
+ return false;
2368
+ }
2369
+ }
2370
+ /** 组装 + 翻译:返回含全部 warning 的副本内容(尚未官方校验)。 */
2371
+ function assemble(deps, warnings) {
2372
+ const routeFields = deriveOfficialRouteFields(deps.kit.officialConfig);
2373
+ if (routeFields === void 0) warnings.push("官方 Config schema 布局不可识别,route 字段回退内置键集");
2374
+ const providers = deps.current().providers ?? {};
2375
+ const normalizedPi = normalizeOfficialRoutes(providers, {
2376
+ kit: deps.kit,
2377
+ lenient: true,
2378
+ warn: (message) => warnings.push(message)
2379
+ });
2380
+ const deepseekResolved = buildDeepseekRoutes(providers, {
2381
+ kit: deps.kit,
2382
+ lenient: true,
2383
+ warn: (message) => warnings.push(message)
2384
+ });
2385
+ const manual = deps.readManual();
2386
+ if (manual.warning !== void 0) warnings.push(manual.warning);
2387
+ const copy = buildOfficialCopy({
2388
+ providers,
2389
+ normalizedPi,
2390
+ deepseekResolved,
2391
+ manualProviders: manual.providers,
2392
+ routeFields: routeFields ?? [...FALLBACK_ROUTE_FIELDS],
2393
+ modelFields: deps.kit.officialModelFields
2394
+ });
2395
+ warnings.push(...copy.warnings);
2396
+ return {
2397
+ providers: copy.providers,
2398
+ warnings
2399
+ };
2400
+ }
2401
+ /** 逐 route 官方校验:schema 解析 + 可服务性链;不过即剔除并记 warning。 */
2402
+ function acceptOfficial(deps, copy, warnings) {
2403
+ const accepted = {};
2404
+ for (const [route, profile] of Object.entries(copy.providers)) {
2405
+ try {
2406
+ deps.kit.officialConfig({ providers: { [route]: profile } });
2407
+ deps.kit.resolveProfiles({ [route]: profile });
2408
+ } catch (error) {
2409
+ const message = error instanceof Error ? error.message : String(error);
2410
+ warnings.push(`route "${route}" 未通过官方校验,已从副本剔除:${message}`);
2411
+ continue;
2412
+ }
2413
+ accepted[route] = profile;
2414
+ }
2415
+ return accepted;
2416
+ }
2417
+ /** 落盘:内容未变且文件仍在则跳过(并自愈外部删除);返回最近生成时间。 */
2418
+ async function persist(path, content, state) {
2419
+ if (content === state.lastContent && await fileExists(path)) return state.updatedAt ?? Date.now();
2420
+ await atomicWriteFile(path, content);
2421
+ state.lastContent = content;
2422
+ state.updatedAt = Date.now();
2423
+ return state.updatedAt;
2424
+ }
2425
+ /** 生成一次副本(错误上抛,由调用方降级为 status.error)。 */
2426
+ async function runOnce(deps, path, state) {
2427
+ const warnings = [];
2428
+ const providers = acceptOfficial(deps, assemble(deps, warnings), warnings);
2429
+ return {
2430
+ path,
2431
+ updatedAt: await persist(path, renderOfficialCopy({
2432
+ providers,
2433
+ warnings
2434
+ }, { profile: deps.profile }), state),
2435
+ routes: Object.keys(providers).length,
2436
+ warnings
2437
+ };
2438
+ }
2439
+ /** 装配写入器:启动/热更新/adapter 事件驱动,串行执行(promise 链)。 */
2440
+ function createOfficialCopyWriter(deps) {
2441
+ const path = deps.path ?? OFFICIAL_COPY_PATH;
2442
+ const state = {};
2443
+ let status = {
2444
+ path,
2445
+ routes: 0,
2446
+ warnings: []
2447
+ };
2448
+ let chain = Promise.resolve();
2449
+ const ensureOfficialCopy = () => {
2450
+ chain = chain.then(async () => {
2451
+ try {
2452
+ status = await runOnce(deps, path, state);
2453
+ } catch (error) {
2454
+ const message = error instanceof Error ? error.message : String(error);
2455
+ deps.logger.warn(`llm-pi: 应急副本生成失败:${message}`);
2456
+ status = {
2457
+ ...status,
2458
+ error: message
2459
+ };
2460
+ }
2461
+ });
2462
+ return chain;
2463
+ };
2464
+ return {
2465
+ ensureOfficialCopy,
2466
+ status: () => status
2467
+ };
2468
+ }
2052
2469
  /** 官方 jsonImage:pi-ai 凭据里显式 undefined 成员 JSON 序列化为缺省/数组 null。 */
2053
2470
  function jsonImage(value) {
2054
2471
  if (Array.isArray(value)) return value.map((entry) => entry === void 0 ? null : jsonImage(entry));
@@ -2173,7 +2590,7 @@ function authContextFrom(ctx) {
2173
2590
  * 模型条目字段集同样现场推导(official-surface.ts)。生效版本只用于状态行显示,
2174
2591
  * 超出已验证区间也只是提示(kit-meta.ts),永不阻断。
2175
2592
  *
2176
- * 0.1.2-alpha 线的新面(0.1.2-alpha.2 复核包根仍未导出、仅 src 子路径有):
2593
+ * 仅 src 子路径导出、包根不导出的两块面:
2177
2594
  * - resolveProfiles(config.ts):官方解析链,dev 布局树可经 src 子路径复用;
2178
2595
  * - credentialStoreFrom/authContextFrom(auth.ts):PiAiAdapter 必需 auth 注入。
2179
2596
  * npm 发布形态(lib/index.js 单 bundle + 无 src/)两者都拿不到:dsh 树优先
@@ -2196,6 +2613,7 @@ function assertKitShape(kit, origin) {
2196
2613
  "providerRetryPolicy"
2197
2614
  ]) if (typeof kit.PiAiAdapter.prototype[method] !== "function") problems.push(`PiAiAdapter.prototype.${method} 缺失`);
2198
2615
  if (typeof kit.createProvider !== "function") problems.push("pi-ai createProvider 缺失");
2616
+ if (typeof kit.officialConfig !== "function") problems.push("dsh-llm-pi-ai Config schema 缺失");
2199
2617
  if (typeof kit.getBuiltinModels !== "function") problems.push("pi-ai getBuiltinModels 缺失");
2200
2618
  if (typeof kit.builtinProviders !== "function") problems.push("pi-ai builtinProviders 缺失");
2201
2619
  if (kit.protocols.length === 0) problems.push("没有可服务的线协议(协议工厂全部加载失败)");
@@ -2431,6 +2849,7 @@ function kitFromTree(options) {
2431
2849
  officialModelFields: surface.officialModelFields,
2432
2850
  ...surface.catalogGeneratedAt === void 0 ? {} : { catalogGeneratedAt: surface.catalogGeneratedAt },
2433
2851
  PiAiAdapter: mods.piAiAdapter["PiAiAdapter"],
2852
+ officialConfig: mods.piAiAdapter["Config"],
2434
2853
  LlmError: mods.llm["LlmError"],
2435
2854
  resolveRetryPolicy: mods.llm["resolveRetryPolicy"],
2436
2855
  attributionHeaders: mods.llm["attributionHeaders"],
@@ -2509,6 +2928,7 @@ function loadVendoredKit(diagnostics = []) {
2509
2928
  officialModelFields: surface.officialModelFields,
2510
2929
  ...surface.catalogGeneratedAt === void 0 ? {} : { catalogGeneratedAt: surface.catalogGeneratedAt },
2511
2930
  PiAiAdapter: vendoredPiAiAdapter.PiAiAdapter,
2931
+ officialConfig: vendoredPiAiAdapter.Config,
2512
2932
  LlmError: vendoredLlm.LlmError,
2513
2933
  resolveRetryPolicy: vendoredLlm.resolveRetryPolicy,
2514
2934
  attributionHeaders: vendoredLlm.attributionHeaders,
@@ -2848,16 +3268,26 @@ async function startRuntime(ctx, rawConfig) {
2848
3268
  routes: deepseekRoutes
2849
3269
  });
2850
3270
  const ensureDeepseek = () => deepseekRegistrar.sync(deepseekRoutes());
3271
+ const copyWriter = createOfficialCopyWriter({
3272
+ kit,
3273
+ current,
3274
+ readManual: () => readManualProviders(ctx.settings),
3275
+ profile: ctx.get("profileContext")?.name,
3276
+ logger
3277
+ });
2851
3278
  ensureRegistration();
2852
3279
  ensureDeepseek();
2853
3280
  ensureDirectory();
3281
+ copyWriter.ensureOfficialCopy();
2854
3282
  const reconfigure = () => {
2855
3283
  snapshot = unwrapVolatile(fields);
2856
3284
  attempt(logger, "llm-pi: 更新被拒,保留此前注册的 route", ensureRegistration);
2857
3285
  attempt(logger, "llm-pi: deepseek route 更新失败,保留此前注册", ensureDeepseek);
2858
3286
  attempt(logger, "llm-pi: 更新被拒,保留此前的 configurable-provider 目录", ensureDirectory);
3287
+ copyWriter.ensureOfficialCopy();
2859
3288
  };
2860
3289
  ctx.events.on("loader/volatile-update", reconfigure);
3290
+ ctx.root.on("llm/adapters-updated", () => void copyWriter.ensureOfficialCopy());
2861
3291
  ctx.on("internal/config", function(_raw, next) {
2862
3292
  const candidate = next();
2863
3293
  if (this !== ctx.fiber) return candidate;
@@ -2869,6 +3299,7 @@ async function startRuntime(ctx, rawConfig) {
2869
3299
  const providers = listProviders(kit);
2870
3300
  const generatedAt = kit.catalogGeneratedAt;
2871
3301
  const notice = piAiVersionNotice(kit.versions.piAi);
3302
+ const copy = copyWriter.status();
2872
3303
  return {
2873
3304
  source: kit.source,
2874
3305
  ...kit.root === void 0 ? {} : { root: kit.root },
@@ -2883,7 +3314,12 @@ async function startRuntime(ctx, rawConfig) {
2883
3314
  ...generatedAt === void 0 ? {} : { generatedAt }
2884
3315
  },
2885
3316
  compatSource: compatTableInfo().source,
2886
- diagnostics
3317
+ officialCopy: copy,
3318
+ diagnostics: [
3319
+ ...diagnostics,
3320
+ ...copy.warnings.map((warning) => `应急副本:${warning}`),
3321
+ ...copy.error === void 0 ? [] : [`应急副本:生成失败(${copy.error})`]
3322
+ ]
2887
3323
  };
2888
3324
  };
2889
3325
  return {
@@ -2897,7 +3333,11 @@ async function startRuntime(ctx, rawConfig) {
2897
3333
  //#endregion
2898
3334
  //#region src/index.ts
2899
3335
  const name = "dsh-plus-llm-pi";
2900
- const inject = ["credentials", "llm"];
3336
+ const inject = [
3337
+ "credentials",
3338
+ "llm",
3339
+ "settings"
3340
+ ];
2901
3341
  async function apply(ctx, config) {
2902
3342
  const runtime = await startRuntime(ctx, config);
2903
3343
  ctx.inject(["webServer"], (webCtx) => {