@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/client.js +22 -3
- package/lib/index.d.ts +3 -3
- package/lib/index.js +464 -24
- package/package.json +3 -2
- package/src/client/api.ts +8 -0
- package/src/client/client.ts +2 -2
- package/src/client/i18n.ts +8 -0
- package/src/client/root-fields.tsx +11 -0
- package/src/compat.ts +2 -2
- package/src/config-api.ts +1 -1
- package/src/config.ts +6 -17
- package/src/deepseek-routes.ts +2 -2
- package/src/index.ts +5 -2
- package/src/inherit.ts +1 -1
- package/src/ns.ts +1 -1
- package/src/official-copy-writer.ts +207 -0
- package/src/official-copy.ts +293 -0
- package/src/official-surface.ts +45 -4
- package/src/profiles-deepseek.ts +1 -11
- package/src/profiles.ts +34 -10
- package/src/resolve-dsh.ts +19 -4
- package/src/service.ts +35 -7
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
|
-
|
|
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
|
|
38
|
+
/** settings 命名空间(字面量即合法命名空间,编译期校验;webui 配置卡片与插件运行期读取同一份)。 */
|
|
37
39
|
const SETTINGS_NS = "dsh-plus-llm-pi";
|
|
38
40
|
/**
|
|
39
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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 = [
|
|
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) => {
|