@dsh-plus/llm-pi 0.1.44 → 0.2.0

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
@@ -7,6 +7,42 @@ window.__ModuleLoader__.load({
7
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
8
  let react = require("react");
9
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
+ //#region ../shared/src/client/fields.tsx
11
+ /** 卡片按钮(与 CardChrome 页脚同一套类名与视觉)。 */
12
+ function Button(props) {
13
+ const p = props.prefix ?? "dshp";
14
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
15
+ type: "button",
16
+ className: `${p}-btn ${props.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
17
+ disabled: props.disabled === true,
18
+ onClick: props.onClick,
19
+ children: props.label
20
+ });
21
+ }
22
+ /**
23
+ * 勾选行:自绘圆角扁平开关(switch),对齐官方 WebUI 无原生 checkbox 的
24
+ * 设计语言(官方选择类控件全走胶囊/菜单,见 cardCss 的 -switch 规则)。
25
+ * 语义保持 checkbox(无障碍树与表单语义不变),视觉自绘。
26
+ */
27
+ function CheckRow(props) {
28
+ const p = props.prefix ?? "dshp";
29
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
30
+ className: `${p}-checkRow`,
31
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
32
+ id: props.id,
33
+ type: "checkbox",
34
+ role: "switch",
35
+ "aria-checked": props.checked,
36
+ checked: props.checked,
37
+ disabled: props.disabled === true,
38
+ onChange: (event) => props.onEdit(event.target.checked)
39
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
40
+ htmlFor: props.id,
41
+ children: props.label
42
+ })]
43
+ });
44
+ }
45
+ //#endregion
10
46
  //#region ../shared/src/client/card.tsx
11
47
  const IDLE_STATUS = {
12
48
  kind: "idle",
@@ -55,12 +91,12 @@ window.__ModuleLoader__.load({
55
91
  className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
56
92
  role: "status",
57
93
  children: props.status.text
58
- }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
59
- type: "button",
60
- className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
61
- disabled: action.disabled === true,
62
- onClick: action.onClick,
63
- children: action.label
94
+ }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
95
+ prefix: p,
96
+ label: action.label,
97
+ variant: action.variant,
98
+ disabled: action.disabled,
99
+ onClick: action.onClick
64
100
  }, action.key))]
65
101
  })
66
102
  ]
@@ -139,31 +175,6 @@ window.__ModuleLoader__.load({
139
175
  * @module @dsh-plus/shared/client/draft
140
176
  */
141
177
  //#endregion
142
- //#region ../shared/src/client/fields.tsx
143
- /**
144
- * 勾选行:自绘圆角扁平开关(switch),对齐官方 WebUI 无原生 checkbox 的
145
- * 设计语言(官方选择类控件全走胶囊/菜单,见 cardCss 的 -switch 规则)。
146
- * 语义保持 checkbox(无障碍树与表单语义不变),视觉自绘。
147
- */
148
- function CheckRow(props) {
149
- const p = props.prefix ?? "dshp";
150
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
151
- className: `${p}-checkRow`,
152
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
153
- id: props.id,
154
- type: "checkbox",
155
- role: "switch",
156
- "aria-checked": props.checked,
157
- checked: props.checked,
158
- disabled: props.disabled === true,
159
- onChange: (event) => props.onEdit(event.target.checked)
160
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
161
- htmlFor: props.id,
162
- children: props.label
163
- })]
164
- });
165
- }
166
- //#endregion
167
178
  //#region ../shared/src/client/i18n.ts
168
179
  /** 卡片通用文案(save/discard/unsaved/loading 等四插件共有键)。 */
169
180
  const commonZh = {
@@ -1136,6 +1147,13 @@ ${extra}
1136
1147
  function sourceLabel(t, source) {
1137
1148
  return source === "official" ? t("runtimeDerived") : t("runtimeFallback");
1138
1149
  }
1150
+ /** 应急副本状态行:路径 · route 数 · 生成时间;未生成/失败显式提示。 */
1151
+ function officialCopyText(kit, t) {
1152
+ const copy = kit.officialCopy;
1153
+ if (copy.error !== void 0) return `${t("officialCopyFailed")}${copy.error}`;
1154
+ if (copy.updatedAt === void 0) return t("officialCopyPending");
1155
+ return `${copy.path} · ${copy.routes} ${t("officialCopyRoutes")} · ${formatTime(copy.updatedAt, t("runtimeVersionUnknown"))}`;
1156
+ }
1139
1157
  function RootFields(props) {
1140
1158
  const { t, draft, disabled, kit, onEdit } = props;
1141
1159
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
@@ -1179,6 +1197,10 @@ ${extra}
1179
1197
  className: "lpc-statusRow",
1180
1198
  children: `${t("runtimeCompat")}:${sourceLabel(t, kit.compatSource)}`
1181
1199
  }),
1200
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1201
+ className: "lpc-statusRow",
1202
+ children: `${t("runtimeOfficialCopy")}:${officialCopyText(kit, t)}`
1203
+ }),
1182
1204
  kit.versionNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1183
1205
  className: "lpc-invalid",
1184
1206
  children: `${t("runtimeNotice")}:${kit.versionNotice}`
@@ -2840,6 +2862,10 @@ ${extra}
2840
2862
  runtimeCatalog: "内置模型目录",
2841
2863
  runtimeProtocols: "可服务协议",
2842
2864
  runtimeCompat: "compat 门控表",
2865
+ runtimeOfficialCopy: "应急副本",
2866
+ officialCopyPending: "尚未生成",
2867
+ officialCopyFailed: "生成失败:",
2868
+ officialCopyRoutes: "个 route",
2843
2869
  runtimeDiagnostics: "诊断",
2844
2870
  runtimeVersionUnknown: "未知",
2845
2871
  runtimeCatalogUnit: "个 provider /",
@@ -2970,6 +2996,10 @@ ${extra}
2970
2996
  runtimeCatalog: "Built-in catalog",
2971
2997
  runtimeProtocols: "Servable protocols",
2972
2998
  runtimeCompat: "compat gates",
2999
+ runtimeOfficialCopy: "Emergency copy",
3000
+ officialCopyPending: "not generated yet",
3001
+ officialCopyFailed: "generation failed: ",
3002
+ officialCopyRoutes: "routes",
2973
3003
  runtimeDiagnostics: "Diagnostics",
2974
3004
  runtimeVersionUnknown: "unknown",
2975
3005
  runtimeCatalogUnit: "providers /",
package/lib/index.d.ts CHANGED
@@ -95,7 +95,7 @@ declare const Config: z<LlmPiConfigInput, LlmPiConfigFields>;
95
95
  //#endregion
96
96
  //#region src/index.d.ts
97
97
  declare const name = "dsh-plus-llm-pi";
98
- declare const inject: readonly ['credentials', 'llm'];
98
+ declare const inject: readonly ['credentials', 'llm', 'settings'];
99
99
  declare function apply(ctx: Context, config: LlmPiConfig): Promise<void>;
100
100
  //#endregion
101
101
  export { Config, type LlmPiConfig, type ModelEntryConfig, type ProviderProfileConfig, SETTINGS_NS, apply, inject, name };
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,7 +19,6 @@ 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
@@ -771,6 +773,10 @@ function compatTableInfo() {
771
773
  problem: active.problem
772
774
  };
773
775
  }
776
+ /** 某协议某字段的可配性('offer'/'withhold';未列出 = 无此字段)。 */
777
+ function compatDispositionOf(api, field) {
778
+ return active.gates[api]?.[field];
779
+ }
774
780
  /** 某协议全部可配置(offer)的 compat 键(UI 渲染字段组与校验共用)。 */
775
781
  function compatFieldsOf(api) {
776
782
  const gate = active.gates[api];
@@ -2044,11 +2050,390 @@ function buildProfiles(providers, deps) {
2044
2050
  }
2045
2051
  return resolved;
2046
2052
  }
2053
+ /**
2054
+ * 应急副本用:逐 route 归一化为官方 schema 形状的 profile(与运行期同一条
2055
+ * normalizeRoute 链:extends 物化、单协议单端点约束、模型条目全显式)。
2056
+ *
2057
+ * 与运行期 buildProfiles 的两点差异:
2058
+ * - 不受 `enabled` 开关影响——副本是配置层事实,插件关闭时其 route 仍应可复制;
2059
+ * - 逐 route 隔离失败(不依赖 lenient 开关)——一个坏条目只丢它自己,
2060
+ * 不让整份应急副本变空。
2061
+ *
2062
+ * adapter: deepseek 的 route 与草稿路由不在此列(前者由 profiles-deepseek
2063
+ * 物化、后者本就不可服务),调用方按缺席另行告警。
2064
+ */
2065
+ function normalizeOfficialRoutes(providers, deps) {
2066
+ const out = /* @__PURE__ */ new Map();
2067
+ for (const [route, profile] of Object.entries(providers ?? {})) try {
2068
+ const normalized = normalizeRoute(route, profile, deps);
2069
+ if (normalized !== void 0) out.set(route, normalized.profile);
2070
+ } catch (error) {
2071
+ deps.warn?.(`llm-pi: provider "${route}" 无法物化为官方形状(${error instanceof Error ? error.message : String(error)}),已跳过应急副本的该 route`);
2072
+ }
2073
+ return out;
2074
+ }
2047
2075
  /** settings 写入校验钩子:完整试跑解析,非法配置在写入处拒绝。 */
2048
2076
  function assertServiceable(config, deps) {
2049
2077
  buildProfiles(config.providers, deps);
2050
2078
  buildDeepseekRoutes(config.providers, deps);
2051
2079
  }
2080
+ //#endregion
2081
+ //#region src/official-copy.ts
2082
+ /**
2083
+ * 官方应急副本的翻译与渲染(纯函数,零 IO)。
2084
+ *
2085
+ * 产出一份「官方可识别」的 profile patch 覆盖层:把 dsh-plus-llm-pi 的 route
2086
+ * 配置改写为官方 `llm-pi-ai` 的 Config 形状,另附一行禁用 dsh-plus-llm-pi 的
2087
+ * 覆盖(应用即切换,同名 route 不撞 DUPLICATE_ADAPTER)。写盘、逐 route 官方
2088
+ * 校验与状态管理见 official-copy-writer.ts。
2089
+ *
2090
+ * 三条翻译底线(对旧「出错自动改写」失效教训的根治):
2091
+ * 1. **只产出官方 schema 声明的字段**:route 键集从官方 Config schema 现场推导
2092
+ * (失败回退内置键集),模型条目按 kit.officialModelFields 过滤——官方拒收
2093
+ * 的字段根本不出现在副本里;
2094
+ * 2. **compat 只留 offer**:withhold 与未知键按生效门控表丢弃(目录继承值可能
2095
+ * 携带 withhold 键,写时会被官方拒绝);
2096
+ * 3. **继承全物化**:官方无 extends——模型条目来自 normalizeRoute 的全显式物化
2097
+ * 产物(单协议、单端点约束由该链保证,无需拆分 route)。
2098
+ *
2099
+ * deepseek 路由改写为 openai-completions(中继网关本就是 OpenAI 兼容面):
2100
+ * 文件通道与思考配置不迁移(官方 llm-pi-ai 无对应字段/无逐模型 reasoningEfforts),
2101
+ * 以 warning 明示,绝不静默丢弃。
2102
+ * @module llm-pi/official-copy
2103
+ */
2104
+ /** 副本文件在 Harness home(.dsh)根目录的落点;lifeboat 面板按同一路径消费(文件契约)。 */
2105
+ const OFFICIAL_COPY_PATH = dshHomePath("llm-pi.official-patch.yaml");
2106
+ /** deepseek 路由改写所用线协议:中继网关对外提供 OpenAI 兼容 chat completions 面。 */
2107
+ const DEEPSEEK_COPY_API = "openai-completions";
2108
+ /**
2109
+ * 官方 Config schema 布局不可识别时的 route 字段兜底(官方 PiAiProviderProfile
2110
+ * 最后已知键集)。与官方新增字段的失配只表现为「新字段不进副本」,不弄挂生成。
2111
+ */
2112
+ const FALLBACK_ROUTE_FIELDS = [
2113
+ "apiKeyEnv",
2114
+ "displayName",
2115
+ "api",
2116
+ "baseURL",
2117
+ "models",
2118
+ "modelOverrides",
2119
+ "compat",
2120
+ "defaultContextWindow",
2121
+ "defaultMaxTokens",
2122
+ "defaultInput",
2123
+ "headers",
2124
+ "reasoning",
2125
+ "thinkingBudgets",
2126
+ "cacheRetention",
2127
+ "transport",
2128
+ "timeoutMs",
2129
+ "websocketConnectTimeoutMs",
2130
+ "streamIdleTimeoutMs",
2131
+ "maxRequestImageBytes",
2132
+ "requestImagePixelBudget",
2133
+ "requestImageMaxBytes",
2134
+ "retryPolicy"
2135
+ ];
2136
+ /** deepseek 专有且官方 llm-pi-ai 无对应字段的 raw 键(出现即记 warning)。 */
2137
+ const DEEPSEEK_UNMIGRATED_FIELDS = [
2138
+ "thinking",
2139
+ "reasoningEffort",
2140
+ "maxRequestFilesBytes",
2141
+ "filesApiTimeoutMs",
2142
+ "fileExpiresAfterSeconds",
2143
+ "fileRefreshMarginSeconds",
2144
+ "maxImagesPerRequest",
2145
+ "imageOffloadByteQuantum",
2146
+ "inlineImageOffloadByteQuantum",
2147
+ "imageOffloadCountQuantum"
2148
+ ];
2149
+ /**
2150
+ * 从官方 Config schema 推导 route 字段键集(`providers.*` 的 dict 键);
2151
+ * 布局不可识别/键集为空返回 undefined,调用方回退内置键集并告警。
2152
+ */
2153
+ function deriveOfficialRouteFields(config) {
2154
+ const fields = config.dict?.["providers"]?.inner?.dict;
2155
+ if (fields === void 0) return void 0;
2156
+ const keys = Object.keys(fields);
2157
+ return keys.length > 0 ? keys : void 0;
2158
+ }
2159
+ /** compat 过滤:只保留该协议 offer 的字段(withhold/未知键丢弃);无保留项返回 undefined。 */
2160
+ function offerCompat(api, compat) {
2161
+ if (compat === void 0 || typeof compat !== "object") return void 0;
2162
+ const out = {};
2163
+ for (const [field, value] of Object.entries(compat)) {
2164
+ if (value === void 0 || value === null) continue;
2165
+ if (compatDispositionOf(api, field) === "offer") out[field] = value;
2166
+ }
2167
+ return Object.keys(out).length > 0 ? out : void 0;
2168
+ }
2169
+ /** 单个模型条目 → 官方键集内的条目(compat 另过 offer 门控)。 */
2170
+ function filterModel(api, model, modelFields) {
2171
+ const source = model;
2172
+ const out = {};
2173
+ for (const key of modelFields) {
2174
+ const value = source[key];
2175
+ if (value === void 0) continue;
2176
+ if (key === "compat") {
2177
+ const compat = offerCompat(api, value);
2178
+ if (compat !== void 0) out.compat = compat;
2179
+ continue;
2180
+ }
2181
+ out[key] = value;
2182
+ }
2183
+ return out;
2184
+ }
2185
+ /** 归一化 profile → 官方 route 字段内的条目(models 逐条过滤)。 */
2186
+ function filterRoute(profile, routeFields, modelFields) {
2187
+ const source = profile;
2188
+ const api = typeof source["api"] === "string" ? source["api"] : "";
2189
+ const out = {};
2190
+ for (const key of routeFields) {
2191
+ const value = source[key];
2192
+ if (value === void 0) continue;
2193
+ if (key === "models") {
2194
+ out.models = (Array.isArray(value) ? value : []).map((model) => filterModel(api, model, modelFields));
2195
+ continue;
2196
+ }
2197
+ out[key] = value;
2198
+ }
2199
+ return out;
2200
+ }
2201
+ /** deepseek 单模型 → 官方条目(inputModalities 改名 input;图像预算不迁移)。 */
2202
+ function deepseekModelEntry(model) {
2203
+ const out = { id: model.id };
2204
+ if (model.name !== void 0) out.name = model.name;
2205
+ if (model.contextWindow !== void 0) out.contextWindow = model.contextWindow;
2206
+ if (model.maxTokens !== void 0) out.maxTokens = model.maxTokens;
2207
+ if (model.inputModalities !== void 0) out.input = [...model.inputModalities];
2208
+ return out;
2209
+ }
2210
+ /**
2211
+ * deepseek 路由 → 官方 llm-pi-ai profile:连接事实取 resolveAdapterOptions 的
2212
+ * 校验产物(端点/凭据引用/限额),模型取官方目录物化结果。思考配置与文件通道
2213
+ * 字段不迁移(llm-pi-ai 无对应语义),逐项列入 unmirrored。
2214
+ */
2215
+ function deepseekCopyProfile(raw, resolved) {
2216
+ const connection = resolved.connection;
2217
+ const unmirrored = DEEPSEEK_UNMIGRATED_FIELDS.filter((key) => raw[key] !== void 0);
2218
+ if (raw.models?.some((m) => m.imagePixelBudget !== void 0 || m.imageMaxBytes !== void 0)) unmirrored.push("模型级 imagePixelBudget/imageMaxBytes");
2219
+ const profile = {
2220
+ displayName: resolved.displayName,
2221
+ api: DEEPSEEK_COPY_API,
2222
+ baseURL: connection.baseURL,
2223
+ apiKeyEnv: raw.apiKeyEnv,
2224
+ defaultContextWindow: connection.defaultContextWindow,
2225
+ defaultMaxTokens: connection.maxTokens,
2226
+ streamIdleTimeoutMs: connection.streamIdleTimeoutMs,
2227
+ models: connection.models.map(deepseekModelEntry)
2228
+ };
2229
+ if (raw.retryPolicy !== void 0) profile.retryPolicy = raw.retryPolicy;
2230
+ if (raw.maxInlineRequestImageBytes !== void 0) profile.maxRequestImageBytes = connection.maxInlineRequestImageBytes;
2231
+ return {
2232
+ profile,
2233
+ unmirrored
2234
+ };
2235
+ }
2236
+ /** 翻译整份配置为官方副本内容(纯函数;坏 route 的跳过告警由上游 warn 通道汇集)。 */
2237
+ function buildOfficialCopy(input) {
2238
+ const warnings = [];
2239
+ const providers = {};
2240
+ for (const [route, raw] of Object.entries(input.providers)) {
2241
+ if ((raw.adapter ?? "pi") === "deepseek") {
2242
+ const resolved = input.deepseekResolved.get(route);
2243
+ if (resolved === void 0) continue;
2244
+ const { profile, unmirrored } = deepseekCopyProfile(raw, resolved);
2245
+ providers[route] = profile;
2246
+ if (unmirrored.length > 0) warnings.push(`deepseek route "${route}" 的字段未迁移到 llm-pi-ai:${unmirrored.join("、")}`);
2247
+ continue;
2248
+ }
2249
+ const normalized = input.normalizedPi.get(route);
2250
+ if (normalized === void 0) {
2251
+ if (isDraftRoute(raw)) warnings.push(`草稿 route "${route}" 未纳入副本(尚无模型)`);
2252
+ continue;
2253
+ }
2254
+ providers[route] = filterRoute(normalized, input.routeFields, input.modelFields);
2255
+ }
2256
+ for (const [route, profile] of Object.entries(input.manualProviders ?? {})) if (profile !== null && typeof profile === "object") providers[route] = profile;
2257
+ return {
2258
+ providers,
2259
+ warnings
2260
+ };
2261
+ }
2262
+ /** 头注释:用途、两种应用方式、切换语义、回退方法与警告清单(确定性输出,mtime 即生成时间)。 */
2263
+ function renderHeader(copy, meta) {
2264
+ const profile = meta.profile ?? "<profile>";
2265
+ return [
2266
+ "# 官方应急 LLM 配置副本 —— @dsh-plus/llm-pi 自动生成(勿手改:下次配置变更会被覆盖;文件 mtime 即最近生成时间)",
2267
+ `# 来源 profile:${profile}`,
2268
+ "# 用途:dsh-plus-llm-pi 缺席/故障时,用本文件把其 route 以官方 llm-pi-ai 配置恢复 LLM 能力。",
2269
+ "# 应用(二选一,重启/重载后生效):",
2270
+ `# 1) 单次启动:dsh ${profile} --patch ${OFFICIAL_COPY_PATH}`,
2271
+ `# 2) 长期生效:把本文件条目并入该 profile 的 cordis.patch.yml(~/.dsh/profiles/${profile}/)`,
2272
+ "# 语义:应用即切换 —— 副本禁用 dsh-plus-llm-pi,并整行替换 llm-pi-ai 的 config",
2273
+ "# (官方 llm-pi-ai 现有 providers 已合并保留;同名 key 以手写条目优先)。",
2274
+ "# 回退:移除 --patch 参数或删除并入的条目(含 dsh-plus-llm-pi 的 disabled 行),重启/重载。",
2275
+ `# 本次内容:${Object.keys(copy.providers).length} 个官方 provider route。`,
2276
+ ...copy.warnings.map((warning) => `# 警告:${warning}`)
2277
+ ].join("\n");
2278
+ }
2279
+ /**
2280
+ * 渲染副本文件内容(确定性:同输入同字节,mtime 承载时间语义)。
2281
+ * 零 route 时渲染空补丁列表 `[]`(应用无效果)——绝不产出「只禁用不供给」的误导性副本。
2282
+ */
2283
+ function renderOfficialCopy(copy, meta) {
2284
+ const header = renderHeader(copy, meta);
2285
+ if (Object.keys(copy.providers).length === 0) return `${header}\n# 当前没有可迁移的 route(llm-pi 未配置或全部不可服务),空补丁应用无效果。\n[]\n`;
2286
+ const rows = [{
2287
+ id: "dsh-plus-llm-pi",
2288
+ disabled: true
2289
+ }, {
2290
+ id: "llm-pi-ai",
2291
+ config: { providers: copy.providers }
2292
+ }];
2293
+ return `${header}\n${stringify(rows, { lineWidth: 0 })}`;
2294
+ }
2295
+ //#endregion
2296
+ //#region src/official-copy-writer.ts
2297
+ /**
2298
+ * 官方应急副本的生成编排:装配(原始配置 → 物化 → 翻译)→ 逐 route 官方校验
2299
+ * → 备忘跳过 / 原子落盘 → 状态供配置卡与 lifeboat 面板消费。
2300
+ *
2301
+ * 触发时机:插件启动、配置热更新(loader/volatile-update)、adapter 注册变化
2302
+ * (llm/adapters-updated,官方 llm-pi-ai 晚注册时手写条目在此补入)。无定时器、
2303
+ * 无 watcher——平时零开销;写失败只降级为状态与日志,绝不影响路由注册。
2304
+ *
2305
+ * 逐 route 官方校验是「副本必然官方可识别」的执行点:官方 Config schema 解析
2306
+ * (非法协议/越界取值在此抛错)+ 官方 resolveProfiles 可服务性链;任一不过即
2307
+ * 剔除该 route 并记 warning——写不进官方的东西绝不落盘。
2308
+ * @module llm-pi/official-copy-writer
2309
+ */
2310
+ /** 官方 llm-pi-ai 的 settings 命名空间(与插件 NS 同字面量,官方 0.1.x 起稳定)。 */
2311
+ const NS_OFFICIAL_PI_AI = "llm-pi-ai";
2312
+ /**
2313
+ * 手写官方条目来源:settings describe 的 llm-pi-ai 生效视图(composition base
2314
+ * = profile patch 行 + settings 用户层,schema 已解析、已过官方 validate)——
2315
+ * 官方 seam,无需自行解析 patch 文件。ns 未注册/读取失败时降级为「仅本插件
2316
+ * 改写条目」并附 warning,不阻断生成。
2317
+ */
2318
+ function readManualProviders(settings) {
2319
+ try {
2320
+ const row = settings.describe().find((entry) => entry.ns === NS_OFFICIAL_PI_AI);
2321
+ if (row === void 0) return { warning: "官方 llm-pi-ai 未注册(插件缺席),副本仅含本插件改写条目" };
2322
+ const raw = row.value?.providers;
2323
+ const providers = isVolatileRef(raw) ? raw.get() : raw;
2324
+ if (providers === null || typeof providers !== "object") return {};
2325
+ return { providers };
2326
+ } catch (error) {
2327
+ return { warning: `读取官方 llm-pi-ai 生效配置失败(${error instanceof Error ? error.message : String(error)}),副本仅含本插件改写条目` };
2328
+ }
2329
+ }
2330
+ /** 文件是否已存在(内容备忘跳过后自愈外部删除)。 */
2331
+ async function fileExists(path) {
2332
+ try {
2333
+ return (await stat(path)).isFile();
2334
+ } catch {
2335
+ return false;
2336
+ }
2337
+ }
2338
+ /** 组装 + 翻译:返回含全部 warning 的副本内容(尚未官方校验)。 */
2339
+ function assemble(deps, warnings) {
2340
+ const routeFields = deriveOfficialRouteFields(deps.kit.officialConfig);
2341
+ if (routeFields === void 0) warnings.push("官方 Config schema 布局不可识别,route 字段回退内置键集");
2342
+ const providers = deps.current().providers ?? {};
2343
+ const normalizedPi = normalizeOfficialRoutes(providers, {
2344
+ kit: deps.kit,
2345
+ lenient: true,
2346
+ warn: (message) => warnings.push(message)
2347
+ });
2348
+ const deepseekResolved = buildDeepseekRoutes(providers, {
2349
+ kit: deps.kit,
2350
+ lenient: true,
2351
+ warn: (message) => warnings.push(message)
2352
+ });
2353
+ const manual = deps.readManual();
2354
+ if (manual.warning !== void 0) warnings.push(manual.warning);
2355
+ const copy = buildOfficialCopy({
2356
+ providers,
2357
+ normalizedPi,
2358
+ deepseekResolved,
2359
+ manualProviders: manual.providers,
2360
+ routeFields: routeFields ?? [...FALLBACK_ROUTE_FIELDS],
2361
+ modelFields: deps.kit.officialModelFields
2362
+ });
2363
+ warnings.push(...copy.warnings);
2364
+ return {
2365
+ providers: copy.providers,
2366
+ warnings
2367
+ };
2368
+ }
2369
+ /** 逐 route 官方校验:schema 解析 + 可服务性链;不过即剔除并记 warning。 */
2370
+ function acceptOfficial(deps, copy, warnings) {
2371
+ const accepted = {};
2372
+ for (const [route, profile] of Object.entries(copy.providers)) {
2373
+ try {
2374
+ deps.kit.officialConfig({ providers: { [route]: profile } });
2375
+ deps.kit.resolveProfiles({ [route]: profile });
2376
+ } catch (error) {
2377
+ const message = error instanceof Error ? error.message : String(error);
2378
+ warnings.push(`route "${route}" 未通过官方校验,已从副本剔除:${message}`);
2379
+ continue;
2380
+ }
2381
+ accepted[route] = profile;
2382
+ }
2383
+ return accepted;
2384
+ }
2385
+ /** 落盘:内容未变且文件仍在则跳过(并自愈外部删除);返回最近生成时间。 */
2386
+ async function persist(path, content, state) {
2387
+ if (content === state.lastContent && await fileExists(path)) return state.updatedAt ?? Date.now();
2388
+ await atomicWriteFile(path, content);
2389
+ state.lastContent = content;
2390
+ state.updatedAt = Date.now();
2391
+ return state.updatedAt;
2392
+ }
2393
+ /** 生成一次副本(错误上抛,由调用方降级为 status.error)。 */
2394
+ async function runOnce(deps, path, state) {
2395
+ const warnings = [];
2396
+ const providers = acceptOfficial(deps, assemble(deps, warnings), warnings);
2397
+ return {
2398
+ path,
2399
+ updatedAt: await persist(path, renderOfficialCopy({
2400
+ providers,
2401
+ warnings
2402
+ }, { profile: deps.profile }), state),
2403
+ routes: Object.keys(providers).length,
2404
+ warnings
2405
+ };
2406
+ }
2407
+ /** 装配写入器:启动/热更新/adapter 事件驱动,串行执行(promise 链)。 */
2408
+ function createOfficialCopyWriter(deps) {
2409
+ const path = deps.path ?? OFFICIAL_COPY_PATH;
2410
+ const state = {};
2411
+ let status = {
2412
+ path,
2413
+ routes: 0,
2414
+ warnings: []
2415
+ };
2416
+ let chain = Promise.resolve();
2417
+ const ensureOfficialCopy = () => {
2418
+ chain = chain.then(async () => {
2419
+ try {
2420
+ status = await runOnce(deps, path, state);
2421
+ } catch (error) {
2422
+ const message = error instanceof Error ? error.message : String(error);
2423
+ deps.logger.warn(`llm-pi: 应急副本生成失败:${message}`);
2424
+ status = {
2425
+ ...status,
2426
+ error: message
2427
+ };
2428
+ }
2429
+ });
2430
+ return chain;
2431
+ };
2432
+ return {
2433
+ ensureOfficialCopy,
2434
+ status: () => status
2435
+ };
2436
+ }
2052
2437
  /** 官方 jsonImage:pi-ai 凭据里显式 undefined 成员 JSON 序列化为缺省/数组 null。 */
2053
2438
  function jsonImage(value) {
2054
2439
  if (Array.isArray(value)) return value.map((entry) => entry === void 0 ? null : jsonImage(entry));
@@ -2196,6 +2581,7 @@ function assertKitShape(kit, origin) {
2196
2581
  "providerRetryPolicy"
2197
2582
  ]) if (typeof kit.PiAiAdapter.prototype[method] !== "function") problems.push(`PiAiAdapter.prototype.${method} 缺失`);
2198
2583
  if (typeof kit.createProvider !== "function") problems.push("pi-ai createProvider 缺失");
2584
+ if (typeof kit.officialConfig !== "function") problems.push("dsh-llm-pi-ai Config schema 缺失");
2199
2585
  if (typeof kit.getBuiltinModels !== "function") problems.push("pi-ai getBuiltinModels 缺失");
2200
2586
  if (typeof kit.builtinProviders !== "function") problems.push("pi-ai builtinProviders 缺失");
2201
2587
  if (kit.protocols.length === 0) problems.push("没有可服务的线协议(协议工厂全部加载失败)");
@@ -2431,6 +2817,7 @@ function kitFromTree(options) {
2431
2817
  officialModelFields: surface.officialModelFields,
2432
2818
  ...surface.catalogGeneratedAt === void 0 ? {} : { catalogGeneratedAt: surface.catalogGeneratedAt },
2433
2819
  PiAiAdapter: mods.piAiAdapter["PiAiAdapter"],
2820
+ officialConfig: mods.piAiAdapter["Config"],
2434
2821
  LlmError: mods.llm["LlmError"],
2435
2822
  resolveRetryPolicy: mods.llm["resolveRetryPolicy"],
2436
2823
  attributionHeaders: mods.llm["attributionHeaders"],
@@ -2509,6 +2896,7 @@ function loadVendoredKit(diagnostics = []) {
2509
2896
  officialModelFields: surface.officialModelFields,
2510
2897
  ...surface.catalogGeneratedAt === void 0 ? {} : { catalogGeneratedAt: surface.catalogGeneratedAt },
2511
2898
  PiAiAdapter: vendoredPiAiAdapter.PiAiAdapter,
2899
+ officialConfig: vendoredPiAiAdapter.Config,
2512
2900
  LlmError: vendoredLlm.LlmError,
2513
2901
  resolveRetryPolicy: vendoredLlm.resolveRetryPolicy,
2514
2902
  attributionHeaders: vendoredLlm.attributionHeaders,
@@ -2848,16 +3236,26 @@ async function startRuntime(ctx, rawConfig) {
2848
3236
  routes: deepseekRoutes
2849
3237
  });
2850
3238
  const ensureDeepseek = () => deepseekRegistrar.sync(deepseekRoutes());
3239
+ const copyWriter = createOfficialCopyWriter({
3240
+ kit,
3241
+ current,
3242
+ readManual: () => readManualProviders(ctx.settings),
3243
+ profile: ctx.get("profileContext")?.name,
3244
+ logger
3245
+ });
2851
3246
  ensureRegistration();
2852
3247
  ensureDeepseek();
2853
3248
  ensureDirectory();
3249
+ copyWriter.ensureOfficialCopy();
2854
3250
  const reconfigure = () => {
2855
3251
  snapshot = unwrapVolatile(fields);
2856
3252
  attempt(logger, "llm-pi: 更新被拒,保留此前注册的 route", ensureRegistration);
2857
3253
  attempt(logger, "llm-pi: deepseek route 更新失败,保留此前注册", ensureDeepseek);
2858
3254
  attempt(logger, "llm-pi: 更新被拒,保留此前的 configurable-provider 目录", ensureDirectory);
3255
+ copyWriter.ensureOfficialCopy();
2859
3256
  };
2860
3257
  ctx.events.on("loader/volatile-update", reconfigure);
3258
+ ctx.root.on("llm/adapters-updated", () => void copyWriter.ensureOfficialCopy());
2861
3259
  ctx.on("internal/config", function(_raw, next) {
2862
3260
  const candidate = next();
2863
3261
  if (this !== ctx.fiber) return candidate;
@@ -2869,6 +3267,7 @@ async function startRuntime(ctx, rawConfig) {
2869
3267
  const providers = listProviders(kit);
2870
3268
  const generatedAt = kit.catalogGeneratedAt;
2871
3269
  const notice = piAiVersionNotice(kit.versions.piAi);
3270
+ const copy = copyWriter.status();
2872
3271
  return {
2873
3272
  source: kit.source,
2874
3273
  ...kit.root === void 0 ? {} : { root: kit.root },
@@ -2883,7 +3282,12 @@ async function startRuntime(ctx, rawConfig) {
2883
3282
  ...generatedAt === void 0 ? {} : { generatedAt }
2884
3283
  },
2885
3284
  compatSource: compatTableInfo().source,
2886
- diagnostics
3285
+ officialCopy: copy,
3286
+ diagnostics: [
3287
+ ...diagnostics,
3288
+ ...copy.warnings.map((warning) => `应急副本:${warning}`),
3289
+ ...copy.error === void 0 ? [] : [`应急副本:生成失败(${copy.error})`]
3290
+ ]
2887
3291
  };
2888
3292
  };
2889
3293
  return {
@@ -2897,7 +3301,11 @@ async function startRuntime(ctx, rawConfig) {
2897
3301
  //#endregion
2898
3302
  //#region src/index.ts
2899
3303
  const name = "dsh-plus-llm-pi";
2900
- const inject = ["credentials", "llm"];
3304
+ const inject = [
3305
+ "credentials",
3306
+ "llm",
3307
+ "settings"
3308
+ ];
2901
3309
  async function apply(ctx, config) {
2902
3310
  const runtime = await startRuntime(ctx, config);
2903
3311
  ctx.inject(["webServer"], (webCtx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/llm-pi",
3
- "version": "0.1.44",
3
+ "version": "0.2.0",
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",
@@ -31,7 +31,8 @@
31
31
  }
32
32
  },
33
33
  "dependencies": {
34
- "@dsh-plus/shared": "0.1.34"
34
+ "yaml": "^2.9.0",
35
+ "@dsh-plus/shared": "0.1.35"
35
36
  },
36
37
  "peerDependencies": {
37
38
  "@deepseek-ai/cordis": "^4.0.5-alpha.1",