@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 +61 -31
- package/lib/index.d.ts +1 -1
- package/lib/index.js +412 -4
- package/package.json +3 -2
- package/src/client/api.ts +8 -0
- package/src/client/i18n.ts +8 -0
- package/src/client/root-fields.tsx +11 -0
- package/src/index.ts +5 -2
- package/src/official-copy-writer.ts +207 -0
- package/src/official-copy.ts +293 -0
- package/src/profiles.ts +32 -0
- package/src/resolve-dsh.ts +15 -0
- package/src/service.ts +29 -1
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)(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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 = [
|
|
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.
|
|
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
|
-
"
|
|
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",
|