@dsh-plus/llm-pi 0.1.31 → 0.1.33

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
@@ -42,7 +42,7 @@ window.__ModuleLoader__.load({
42
42
  className: `${p}-pending`,
43
43
  children: props.dirtyLabel
44
44
  }) : null,
45
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}` })
45
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}` })
46
46
  ]
47
47
  }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48
48
  className: `${p}-body`,
@@ -792,7 +792,7 @@ ${extra}
792
792
  id: props.id,
793
793
  "aria-expanded": open,
794
794
  onClick: () => setOpen(!open),
795
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: `lpc-chevron${open ? " lpc-chevronOpen" : ""}` }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
795
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `lpc-chevron${open ? " lpc-chevronOpen" : ""}` }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
796
796
  className: "lpc-collapseTitle",
797
797
  children: props.title
798
798
  })]
@@ -1674,7 +1674,7 @@ ${extra}
1674
1674
  "aria-expanded": open,
1675
1675
  onClick: () => setOpen(!open),
1676
1676
  children: [
1677
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: `lpc-chevron${open ? " lpc-chevronOpen" : ""}` }),
1677
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `lpc-chevron${open ? " lpc-chevronOpen" : ""}` }),
1678
1678
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1679
1679
  className: "lpc-routeKey",
1680
1680
  children: route
package/lib/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
+ import { VolatileFields } from "@dsh-plus/shared";
2
3
  import { Context } from "@deepseek-ai/cordis";
3
4
  //#region src/config.d.ts
4
5
  /** settings 命名空间(字面量即合法命名空间,0.1.2-alpha.2 起编译期校验;webui 配置卡片与插件运行期读取同一份)。 */
@@ -92,7 +93,9 @@ interface LlmPiConfig {
92
93
  * (5 个根字段均有 schema 默认值),解析输出仍是完整的 {@link LlmPiConfig}。
93
94
  */
94
95
  type LlmPiConfigInput = Partial<LlmPiConfig>;
95
- declare const Config: z<LlmPiConfigInput, LlmPiConfig>;
96
+ /** 活动字段形态(0.1.7 loader 解析产物:volatile 字段为活动引用)。 */
97
+ type LlmPiConfigFields = VolatileFields<LlmPiConfig>;
98
+ declare const Config: z<LlmPiConfigInput, LlmPiConfigFields>;
96
99
  //#endregion
97
100
  //#region src/index.d.ts
98
101
  declare const name = "dsh-plus-llm-pi";
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
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 { pluginDataPath } from "@dsh-plus/shared";
4
+ import { hasVolatileRefs, pluginDataPath, unwrapVolatile } from "@dsh-plus/shared";
5
5
  import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
6
6
  import { request } from "node:http";
7
7
  import { request as request$1 } from "node:https";
@@ -146,11 +146,11 @@ const providerProfile = z.object({
146
146
  models: z.array(modelEntry).description("本 route 的模型目录;缺省且 provider 有 extends 时继承该源全部模型")
147
147
  });
148
148
  const Config = z.object({
149
- enabled: z.boolean().description("总开关(关闭则不注册任何 route)").default(true),
150
- catalogUrl: z.string().description("models.dev 目录数据端点").default("https://models.dev/api.json"),
151
- catalogRefreshHours: z.number().description("models.dev 自动拉取间隔小时数;0 = 不自动拉取(可手动拉取或读已有缓存)").default(0),
152
- catalogProxy: z.string().description("拉取 models.dev 目录时的 HTTP 代理地址(如 http://127.0.0.1:7890);留空直连").default(""),
153
- providers: z.dict(providerProfile).description("provider 路由表,键即 route 名").default({})
149
+ enabled: z.boolean().description("总开关(关闭则不注册任何 route)").default(true).volatile(),
150
+ catalogUrl: z.string().description("models.dev 目录数据端点").default("https://models.dev/api.json").volatile(),
151
+ catalogRefreshHours: z.number().description("models.dev 自动拉取间隔小时数;0 = 不自动拉取(可手动拉取或读已有缓存)").default(0).volatile(),
152
+ catalogProxy: z.string().description("拉取 models.dev 目录时的 HTTP 代理地址(如 http://127.0.0.1:7890);留空直连").default("").volatile(),
153
+ providers: z.dict(providerProfile).description("provider 路由表,键即 route 名").default({}).volatile()
154
154
  });
155
155
  //#endregion
156
156
  //#region src/catalog/builtin.ts
@@ -2131,13 +2131,14 @@ function makeResolveApiKey(ctx, kit) {
2131
2131
  /** 启动插件运行时:解析套件、挂载注册/发现/settings 联动。 */
2132
2132
  async function startRuntime(ctx, rawConfig) {
2133
2133
  const logger = ctx.logger("llm-pi");
2134
- const config = Config(rawConfig ?? {});
2134
+ const fields = hasVolatileRefs(rawConfig) ? rawConfig : Config(rawConfig ?? {});
2135
2135
  const { kit, diagnostics } = await resolveDshKit();
2136
2136
  for (const line of diagnostics) logger.warn(line);
2137
2137
  logger.info(`运行时套件来源:${kit.source}`);
2138
- const modelsDev = new ModelsDevSource(pluginDataPath("llm-pi", "models-dev.json"), config.catalogUrl, config.catalogRefreshHours, (message) => logger.warn(message), config.catalogProxy ?? "");
2138
+ let snapshot = unwrapVolatile(fields);
2139
+ const modelsDev = new ModelsDevSource(pluginDataPath("llm-pi", "models-dev.json"), snapshot.catalogUrl, snapshot.catalogRefreshHours, (message) => logger.warn(message), snapshot.catalogProxy ?? "");
2139
2140
  modelsDev.ensureLoaded();
2140
- let current = () => config;
2141
+ const current = () => snapshot;
2141
2142
  let lastRaw;
2142
2143
  let memoized;
2143
2144
  let memoizedDeepseek;
@@ -2283,35 +2284,35 @@ async function startRuntime(ctx, rawConfig) {
2283
2284
  ensureRegistration();
2284
2285
  ensureDeepseek();
2285
2286
  ensureDirectory();
2286
- ctx.inject(["settings"], (settingsCtx) => {
2287
- settingsCtx.settings.installSection(ctx, SETTINGS_NS, Config, config, {
2288
- validate: (cfg) => assertServiceable(cfg, deps),
2289
- setSource: (source) => {
2290
- current = source;
2291
- },
2292
- onChange: () => {
2293
- try {
2294
- ensureRegistration();
2295
- } catch (error) {
2296
- logger.error("llm-pi: 更新被拒,保留此前注册的 route");
2297
- logger.error(error);
2298
- }
2299
- try {
2300
- ensureDeepseek();
2301
- } catch (error) {
2302
- logger.error("llm-pi: deepseek route 更新失败,保留此前注册");
2303
- logger.error(error);
2304
- }
2305
- try {
2306
- ensureDirectory();
2307
- } catch (error) {
2308
- logger.error("llm-pi: 更新被拒,保留此前的 configurable-provider 目录");
2309
- logger.error(error);
2310
- }
2311
- const cfg = current();
2312
- modelsDev.reconfigure(cfg.catalogUrl, cfg.catalogRefreshHours, cfg.catalogProxy ?? "");
2313
- }
2314
- });
2287
+ const reconfigure = () => {
2288
+ snapshot = unwrapVolatile(fields);
2289
+ try {
2290
+ ensureRegistration();
2291
+ } catch (error) {
2292
+ logger.error("llm-pi: 更新被拒,保留此前注册的 route");
2293
+ logger.error(error);
2294
+ }
2295
+ try {
2296
+ ensureDeepseek();
2297
+ } catch (error) {
2298
+ logger.error("llm-pi: deepseek route 更新失败,保留此前注册");
2299
+ logger.error(error);
2300
+ }
2301
+ try {
2302
+ ensureDirectory();
2303
+ } catch (error) {
2304
+ logger.error("llm-pi: 更新被拒,保留此前的 configurable-provider 目录");
2305
+ logger.error(error);
2306
+ }
2307
+ const cfg = current();
2308
+ modelsDev.reconfigure(cfg.catalogUrl, cfg.catalogRefreshHours, cfg.catalogProxy ?? "");
2309
+ };
2310
+ ctx.events.on("loader/volatile-update", reconfigure);
2311
+ ctx.on("internal/config", function(_raw, next) {
2312
+ const candidate = next();
2313
+ if (this !== ctx.fiber) return candidate;
2314
+ assertServiceable(unwrapVolatile(Config(candidate)), deps);
2315
+ return candidate;
2315
2316
  });
2316
2317
  return {
2317
2318
  currentConfig: () => current(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/llm-pi",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "dsh-plus service+ui plugin: 基于 PiAiAdapter 的自定义 LLM 路由(全量 compat、模型继承、models.dev 目录兜底)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -32,41 +32,41 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "https-proxy-agent": "^9.1.0",
35
- "@dsh-plus/shared": "0.1.20"
35
+ "@dsh-plus/shared": "0.1.22"
36
36
  },
37
37
  "peerDependencies": {
38
- "@deepseek-ai/cordis": "^4.0.2",
39
- "@deepseek-ai/dsh-anonymous-user-id": "^0.1.6-alpha.2",
40
- "@deepseek-ai/dsh-credentials": "^0.1.6-alpha.2",
41
- "@deepseek-ai/dsh-home-paths": "^0.1.6-alpha.2",
42
- "@deepseek-ai/dsh-host-webserver": "^0.1.6-alpha.2",
43
- "@deepseek-ai/dsh-launch-environment": "^0.1.6-alpha.2",
44
- "@deepseek-ai/dsh-llm": "^0.1.6-alpha.2",
45
- "@deepseek-ai/dsh-llm-deepseek": "^0.1.6-alpha.2",
46
- "@deepseek-ai/dsh-llm-pi-ai": "^0.1.6-alpha.2",
47
- "@deepseek-ai/dsh-settings": "^0.1.6-alpha.2",
48
- "@deepseek-ai/dsh-util-values": "^0.1.6-alpha.2",
49
- "@deepseek-ai/schemastery": "^3.18.2",
38
+ "@deepseek-ai/cordis": "^4.0.4",
39
+ "@deepseek-ai/dsh-anonymous-user-id": "^0.1.7-alpha.2",
40
+ "@deepseek-ai/dsh-credentials": "^0.1.7-alpha.2",
41
+ "@deepseek-ai/dsh-home-paths": "^0.1.7-alpha.2",
42
+ "@deepseek-ai/dsh-host-webserver": "^0.1.7-alpha.2",
43
+ "@deepseek-ai/dsh-launch-environment": "^0.1.7-alpha.2",
44
+ "@deepseek-ai/dsh-llm": "^0.1.7-alpha.2",
45
+ "@deepseek-ai/dsh-llm-deepseek": "^0.1.7-alpha.2",
46
+ "@deepseek-ai/dsh-llm-pi-ai": "^0.1.7-alpha.2",
47
+ "@deepseek-ai/dsh-settings": "^0.1.7-alpha.2",
48
+ "@deepseek-ai/dsh-util-values": "^0.1.7-alpha.2",
49
+ "@deepseek-ai/schemastery": "^3.18.4",
50
50
  "@earendil-works/pi-ai": "^0.85.1"
51
51
  },
52
52
  "devDependencies": {
53
- "@deepseek-ai/cordis": "4.0.2",
54
- "@deepseek-ai/dsh-anonymous-user-id": "0.1.6-alpha.2",
55
- "@deepseek-ai/dsh-atomic-write": "0.1.6-alpha.2",
56
- "@deepseek-ai/dsh-attachment": "0.1.6-alpha.2",
57
- "@deepseek-ai/dsh-credentials": "0.1.6-alpha.2",
58
- "@deepseek-ai/dsh-home-paths": "0.1.6-alpha.2",
59
- "@deepseek-ai/dsh-host-webserver": "0.1.6-alpha.2",
60
- "@deepseek-ai/dsh-launch-environment": "0.1.6-alpha.2",
61
- "@deepseek-ai/dsh-llm": "0.1.6-alpha.2",
62
- "@deepseek-ai/dsh-llm-deepseek": "0.1.6-alpha.2",
63
- "@deepseek-ai/dsh-llm-pi-ai": "0.1.6-alpha.2",
64
- "@deepseek-ai/dsh-settings": "0.1.6-alpha.2",
65
- "@deepseek-ai/dsh-timeout": "0.1.6-alpha.2",
66
- "@deepseek-ai/dsh-typert-protocol": "0.1.6-alpha.2",
67
- "@deepseek-ai/dsh-util-crypto": "0.1.6-alpha.2",
68
- "@deepseek-ai/dsh-util-values": "0.1.6-alpha.2",
69
- "@deepseek-ai/schemastery": "3.18.2",
53
+ "@deepseek-ai/cordis": "4.0.4",
54
+ "@deepseek-ai/dsh-anonymous-user-id": "0.1.7-alpha.2",
55
+ "@deepseek-ai/dsh-atomic-write": "0.1.7-alpha.2",
56
+ "@deepseek-ai/dsh-attachment": "0.1.7-alpha.2",
57
+ "@deepseek-ai/dsh-credentials": "0.1.7-alpha.2",
58
+ "@deepseek-ai/dsh-home-paths": "0.1.7-alpha.2",
59
+ "@deepseek-ai/dsh-host-webserver": "0.1.7-alpha.2",
60
+ "@deepseek-ai/dsh-launch-environment": "0.1.7-alpha.2",
61
+ "@deepseek-ai/dsh-llm": "0.1.7-alpha.2",
62
+ "@deepseek-ai/dsh-llm-deepseek": "0.1.7-alpha.2",
63
+ "@deepseek-ai/dsh-llm-pi-ai": "0.1.7-alpha.2",
64
+ "@deepseek-ai/dsh-settings": "0.1.7-alpha.2",
65
+ "@deepseek-ai/dsh-timeout": "0.1.7-alpha.2",
66
+ "@deepseek-ai/dsh-typert-protocol": "0.1.7-alpha.2",
67
+ "@deepseek-ai/dsh-util-crypto": "0.1.7-alpha.2",
68
+ "@deepseek-ai/dsh-util-values": "0.1.7-alpha.2",
69
+ "@deepseek-ai/schemastery": "3.18.4",
70
70
  "@earendil-works/pi-ai": "0.85.1",
71
71
  "@types/react": "~18.3.31",
72
72
  "react": "^18.2.0"
@@ -8,8 +8,8 @@
8
8
  * 类型说明:浏览器半只用到 slots/locale/remote 的很窄一面,
9
9
  * scope 与基础控件走 @dsh-plus/shared/client 套件(收编自本插件原实现)。
10
10
  * 配置读写经 ctx.remote.settings 直连(0.1.2-alpha.1 起 connection.api.settings
11
- * 已移除;不复用 settingsScope 服务——非 loopback 页面下它固定 memory 模式无数据,
12
- * 见 @dsh-plus/shared/client scope.ts);自定义端点仅剩「模型目录」
11
+ * 已移除;0.1.7 起 settingsScope 服务亦删除(configForms 取代)——插件卡片自始
12
+ * 直连 remote.settings,见 @dsh-plus/shared/client scope.ts);自定义端点仅剩「模型目录」
13
13
  * (api.ts,含 kitSource/models-dev 运行期诊断)。
14
14
  * 卡片 key/rowId 约定见 @dsh-plus/shared/client/config-slots。
15
15
  * @module @dsh-plus/llm-pi/client
package/src/config.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import z from '@deepseek-ai/schemastery'
13
+ import type { VolatileFields } from '@dsh-plus/shared'
13
14
 
14
15
  import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
15
16
 
@@ -325,21 +326,34 @@ const providerProfile = z.object({
325
326
  .description('本 route 的模型目录;缺省且 provider 有 extends 时继承该源全部模型'),
326
327
  })
327
328
 
328
- export const Config: z<LlmPiConfigInput, LlmPiConfig> = z.object({
329
- enabled: z.boolean().description('总开关(关闭则不注册任何 route)').default(true),
329
+ // 0.1.7:根字段全 `.volatile()`——条目进入 settings describe 视图(卡片可读写),
330
+ // loader 原位提交活动引用;输出面标注活动字段声明面(结构兼容 volatile 引用),
331
+ // 平面消费契约保持 LlmPiConfig 接口不变(服务侧经 unwrapVolatile 解包现取)。
332
+ /** 活动字段形态(0.1.7 loader 解析产物:volatile 字段为活动引用)。 */
333
+ export type LlmPiConfigFields = VolatileFields<LlmPiConfig>
334
+
335
+ export const Config: z<LlmPiConfigInput, LlmPiConfigFields> = z.object({
336
+ enabled: z.boolean().description('总开关(关闭则不注册任何 route)').default(true).volatile(),
330
337
  catalogUrl: z
331
338
  .string()
332
339
  .description('models.dev 目录数据端点')
333
- .default('https://models.dev/api.json'),
340
+ .default('https://models.dev/api.json')
341
+ .volatile(),
334
342
  catalogRefreshHours: z
335
343
  .number()
336
344
  .description('models.dev 自动拉取间隔小时数;0 = 不自动拉取(可手动拉取或读已有缓存)')
337
- .default(0),
345
+ .default(0)
346
+ .volatile(),
338
347
  catalogProxy: z
339
348
  .string()
340
349
  .description('拉取 models.dev 目录时的 HTTP 代理地址(如 http://127.0.0.1:7890);留空直连')
341
- .default(''),
342
- providers: z.dict(providerProfile).description('provider 路由表,键即 route 名').default({}),
350
+ .default('')
351
+ .volatile(),
352
+ providers: z
353
+ .dict(providerProfile)
354
+ .description('provider 路由表,键即 route 名')
355
+ .default({})
356
+ .volatile(),
343
357
  })
344
358
 
345
359
  /** 卡片提交的形状:完整配置对象(含 providers 全量),settings.replace 整段覆盖。 */
package/src/service.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * 运行时主逻辑:注册/热更新/发现(逐点对齐官方 dsh-llm-pi-ai apply 的模式)。
3
3
  *
4
- * - profiles 回调按原始 config 对象 identity 备忘;配置变更经 settings 的
5
- * setSource/onChange 传播,下一请求生效(adapter 快照按 profiles identity 失效);
4
+ * - profiles 回调按原始 config 对象 identity 备忘;配置变更经 0.1.7
5
+ * volatile 原位提交(loader/volatile-update 换代快照)传播,下一请求生效
6
+ * (adapter 快照按 profiles identity 失效);
6
7
  * - route 集或注册时捕获的事实(displayName/retryPolicy)变化 → 原子的
7
8
  * handle.replace 重注册;写入被校验拒绝时保留旧注册(官方同款护栏);
8
9
  * - registerConfigurableProviders + registerModelDiscovery 让插件 route
@@ -16,10 +17,16 @@ import type { DirectoryRegistrationHandle } from '@deepseek-ai/dsh-llm'
16
17
  import type { ResolvedPiAiProviderProfile } from '@deepseek-ai/dsh-llm-pi-ai'
17
18
  import type {} from '@deepseek-ai/dsh-settings'
18
19
  import { deepEqualJson } from '@deepseek-ai/dsh-util-values'
19
- import { pluginDataPath } from '@dsh-plus/shared'
20
+ import { hasVolatileRefs, pluginDataPath, unwrapVolatile } from '@dsh-plus/shared'
20
21
 
21
22
  import { ModelsDevSource } from './catalog/models-dev.ts'
22
- import { Config, type LlmPiConfig, SETTINGS_NS } from './config.ts'
23
+ import {
24
+ Config,
25
+ type LlmPiConfig,
26
+ type LlmPiConfigFields,
27
+ type LlmPiConfigInput,
28
+ SETTINGS_NS,
29
+ } from './config.ts'
23
30
  import { DeepseekRouteRegistrar } from './deepseek-routes.ts'
24
31
  import { buildDirectoryEntries, commitDirectory, type DirectoryEntry } from './directory.ts'
25
32
  import { discoverModels } from './discovery.ts'
@@ -72,25 +79,34 @@ function makeResolveApiKey(ctx: Context, kit: DshKit) {
72
79
  }
73
80
 
74
81
  /** 启动插件运行时:解析套件、挂载注册/发现/settings 联动。 */
75
- export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promise<LlmPiRuntime> {
82
+ export async function startRuntime(
83
+ ctx: Context,
84
+ rawConfig: LlmPiConfig | LlmPiConfigFields,
85
+ ): Promise<LlmPiRuntime> {
76
86
  const logger = ctx.logger('llm-pi')
77
87
  // cordis 行级 config 可能未经 schema 解析(insert 行无 config 键时为原始空对象),
78
- // 在此统一规范化,保证 enabled/默认值在纯组合层场景也成立。
79
- const config = Config(rawConfig ?? {})
88
+ // 在此统一规范化;0.1.7 loader 解析出的 volatile 活动字段形态直接透传
89
+ // (hasVolatileRefs 辨识——重复校验会把引用再包一层、破坏 loader 原位提交)。
90
+ const fields: LlmPiConfigFields = hasVolatileRefs(rawConfig)
91
+ ? (rawConfig as LlmPiConfigFields)
92
+ : Config((rawConfig ?? {}) as LlmPiConfigInput)
80
93
  const { kit, diagnostics } = await resolveDshKit()
81
94
  for (const line of diagnostics) logger.warn(line)
82
95
  logger.info(`运行时套件来源:${kit.source}`)
83
96
 
97
+ // 活动引用解包出平面快照:identity 仅在 volatile-update 重解包时换代
98
+ // (profiles 备忘依赖 raw === lastRaw;非 volatile 变更走 fiber reload 整树重建)。
99
+ let snapshot = unwrapVolatile(fields)
84
100
  const modelsDev = new ModelsDevSource(
85
101
  pluginDataPath('llm-pi', 'models-dev.json'),
86
- config.catalogUrl,
87
- config.catalogRefreshHours,
102
+ snapshot.catalogUrl,
103
+ snapshot.catalogRefreshHours,
88
104
  (message) => logger.warn(message),
89
- config.catalogProxy ?? '',
105
+ snapshot.catalogProxy ?? '',
90
106
  )
91
107
  void modelsDev.ensureLoaded()
92
108
 
93
- let current: () => LlmPiConfig = () => config
109
+ const current: () => LlmPiConfig = () => snapshot
94
110
  let lastRaw: LlmPiConfig | undefined
95
111
  let memoized: Map<string, ResolvedPiAiProviderProfile> | undefined
96
112
  let memoizedDeepseek: Map<string, ResolvedDeepseekRoute> | undefined
@@ -285,37 +301,39 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
285
301
  ensureDeepseek()
286
302
  ensureDirectory()
287
303
 
288
- // 官方 installSection 范式(0.1.2-alpha.2):settings 在时以行级 config 为
289
- // base 注册用户层(validate 拒绝坏写入),缺席/detach 时回落行级 config。
290
- ctx.inject(['settings'], (settingsCtx) => {
291
- settingsCtx.settings.installSection(ctx, SETTINGS_NS, Config, config, {
292
- validate: (cfg: LlmPiConfig) => assertServiceable(cfg, deps),
293
- setSource: (source: () => LlmPiConfig) => {
294
- current = source
295
- },
296
- onChange: () => {
297
- try {
298
- ensureRegistration()
299
- } catch (error) {
300
- logger.error('llm-pi: 更新被拒,保留此前注册的 route')
301
- logger.error(error)
302
- }
303
- try {
304
- ensureDeepseek()
305
- } catch (error) {
306
- logger.error('llm-pi: deepseek route 更新失败,保留此前注册')
307
- logger.error(error)
308
- }
309
- try {
310
- ensureDirectory()
311
- } catch (error) {
312
- logger.error('llm-pi: 更新被拒,保留此前的 configurable-provider 目录')
313
- logger.error(error)
314
- }
315
- const cfg = current()
316
- modelsDev.reconfigure(cfg.catalogUrl, cfg.catalogRefreshHours, cfg.catalogProxy ?? '')
317
- },
318
- })
304
+ // 0.1.7 替代 installSection(validate/setSource/onChange 三钩子):
305
+ // - 活动引用原位提交 → volatile-update 触发快照换代与重注册(原 onChange 体);
306
+ // - internal/config waterfall 承接写入校验(原 validate;官方 llm-pi-ai
307
+ // 同款):configEditor.edit 落盘前先跑 waterfall,handler throw 即拒绝整笔写入。
308
+ const reconfigure = (): void => {
309
+ snapshot = unwrapVolatile(fields)
310
+ try {
311
+ ensureRegistration()
312
+ } catch (error) {
313
+ logger.error('llm-pi: 更新被拒,保留此前注册的 route')
314
+ logger.error(error)
315
+ }
316
+ try {
317
+ ensureDeepseek()
318
+ } catch (error) {
319
+ logger.error('llm-pi: deepseek route 更新失败,保留此前注册')
320
+ logger.error(error)
321
+ }
322
+ try {
323
+ ensureDirectory()
324
+ } catch (error) {
325
+ logger.error('llm-pi: 更新被拒,保留此前的 configurable-provider 目录')
326
+ logger.error(error)
327
+ }
328
+ const cfg = current()
329
+ modelsDev.reconfigure(cfg.catalogUrl, cfg.catalogRefreshHours, cfg.catalogProxy ?? '')
330
+ }
331
+ ctx.events.on('loader/volatile-update', reconfigure)
332
+ ctx.on('internal/config', function (_raw: unknown, next: () => unknown) {
333
+ const candidate = next()
334
+ if (this !== ctx.fiber) return candidate
335
+ assertServiceable(unwrapVolatile(Config(candidate as LlmPiConfigInput)), deps)
336
+ return candidate
319
337
  })
320
338
 
321
339
  return {