@aalis/runtime 0.12.5 → 0.14.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.
Files changed (47) hide show
  1. package/README.md +25 -0
  2. package/dist/bootstrap-buffer.d.ts +5 -4
  3. package/dist/bootstrap-buffer.d.ts.map +1 -1
  4. package/dist/bootstrap-buffer.js +1 -1
  5. package/dist/bootstrap-buffer.js.map +1 -1
  6. package/dist/config-store.d.ts +38 -0
  7. package/dist/config-store.d.ts.map +1 -0
  8. package/dist/config-store.js +171 -0
  9. package/dist/config-store.js.map +1 -0
  10. package/dist/config-sync.d.ts +28 -9
  11. package/dist/config-sync.d.ts.map +1 -1
  12. package/dist/config-sync.js +125 -82
  13. package/dist/config-sync.js.map +1 -1
  14. package/dist/console-sink.d.ts +8 -3
  15. package/dist/console-sink.d.ts.map +1 -1
  16. package/dist/console-sink.js +15 -3
  17. package/dist/console-sink.js.map +1 -1
  18. package/dist/file-logger.d.ts.map +1 -1
  19. package/dist/file-logger.js +2 -1
  20. package/dist/file-logger.js.map +1 -1
  21. package/dist/index.d.ts +3 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/node-modules-loader.d.ts +20 -8
  26. package/dist/node-modules-loader.d.ts.map +1 -1
  27. package/dist/node-modules-loader.js +84 -44
  28. package/dist/node-modules-loader.js.map +1 -1
  29. package/dist/plugin-discovery.d.ts +42 -0
  30. package/dist/plugin-discovery.d.ts.map +1 -0
  31. package/dist/plugin-discovery.js +100 -0
  32. package/dist/plugin-discovery.js.map +1 -0
  33. package/dist/providers.d.ts +10 -9
  34. package/dist/providers.d.ts.map +1 -1
  35. package/dist/providers.js +77 -44
  36. package/dist/providers.js.map +1 -1
  37. package/dist/start.d.ts +14 -2
  38. package/dist/start.d.ts.map +1 -1
  39. package/dist/start.js +75 -39
  40. package/dist/start.js.map +1 -1
  41. package/dist/subcommand.d.ts +3 -3
  42. package/dist/subcommand.d.ts.map +1 -1
  43. package/dist/subcommand.js +3 -3
  44. package/dist/subcommand.js.map +1 -1
  45. package/dist/terminal.d.ts +1 -1
  46. package/dist/terminal.js +1 -1
  47. package/package.json +8 -8
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # @aalis/runtime
2
+
3
+ Aalis 独立部署运行时:node_modules 插件加载 + YAML 配置 + 一行启动 startAalis
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add @aalis/runtime
9
+ ```
10
+
11
+ ## 提供 / 依赖
12
+
13
+ 宿主组装件,不是插件,无 `provides` / `uses`;`startAalis` 在根上独占登记 `host-config`(`@aalis/api-host-config`)与 `plugin-source`(`@aalis/api-plugin-source`)两个服务。主要导出:`startAalis`、`createNodeModulesPluginLoader`、`createFsPluginLoader`、`createPluginDiscovery`、`createConfigStore`、`installHostConfig`、`withPluginConfigSync`、`createFsYamlConfigProvider`、`createProcessRespawnStrategy`。peer:`@aalis/core`(`>=0.17.0 <1.0.0`)。
14
+
15
+ ```ts
16
+ import { startAalis } from '@aalis/runtime';
17
+ ```
18
+
19
+ ## 文档
20
+
21
+ 详见 [docs/architecture/runtime.md](../../docs/architecture/runtime.md)。
22
+
23
+ ## 许可
24
+
25
+ 见仓库根目录 LICENSE。
@@ -1,12 +1,13 @@
1
- import { type LogEntry, LogHub } from '@aalis/core';
1
+ import { LogHub } from '@aalis/core';
2
+ import type { LogEntry } from '@aalis/schema-log';
2
3
  /**
3
4
  * 启动期 bootstrap buffer:在任何 sink(file/console)装好之前订阅 LogHub,
4
5
  * 把启动期产生的所有 LogEntry 暂存。等所有 sink 装好后调用 `dispose()`
5
6
  * 解除订阅并清空——之后所有日志直接通过 LogHub.onEntry 走 sink 实时落盘 / 渲染。
6
7
  *
7
8
  * 设计要点:
8
- * - **进程级单例**:通过 module-level state 实现,必须由 `src/index.ts` 在最早
9
- * 时机调用 `installBootstrapBuffer()` 一次(在 Logger 第一次被调用之前)。
9
+ * - **进程级单例**:通过 module-level state 实现,由 `startAalis` 在最早时机
10
+ * 调用 `installBootstrapBuffer()` 一次(在 Logger 第一次被调用之前)。
10
11
  * - **多 sink 共享**:每个 sink 装载时调用 `snapshot()` 拿到当前已捕获条目副本
11
12
  * 并自行消费(写文件 / 渲染 stdout);snapshot 不改变状态,可任意次。
12
13
  * - **显式 dispose**:所有 sink 装好后由 host 显式 `dispose()` —— 之后 buffer
@@ -21,7 +22,7 @@ interface BootstrapBuffer {
21
22
  dispose(): void;
22
23
  }
23
24
  /**
24
- * 安装 bootstrap buffer。必须在所有日志产生之前调用(即 `src/index.ts` 顶部)。
25
+ * 安装 bootstrap buffer。必须在所有日志产生之前调用(由 `startAalis` 最先调用)。
25
26
  * 重复调用返回同一实例。
26
27
  */
27
28
  export declare function installBootstrapBuffer(hub?: LogHub): BootstrapBuffer;
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap-buffer.d.ts","sourceRoot":"","sources":["../src/bootstrap-buffer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;;;;;;;;;;GAcG;AACH,UAAU,eAAe;IACvB,+BAA+B;IAC/B,QAAQ,IAAI,QAAQ,EAAE,CAAC;IACvB,wBAAwB;IACxB,OAAO,IAAI,IAAI,CAAC;CACjB;AAID;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,GAAE,MAAuB,GAAG,eAAe,CAuBpF;AAED,0DAA0D;AAC1D,wBAAgB,kBAAkB,IAAI,eAAe,CAGpD;AAED,qCAAqC;AACrC,wBAAgB,8BAA8B,IAAI,IAAI,CAGrD"}
1
+ {"version":3,"file":"bootstrap-buffer.d.ts","sourceRoot":"","sources":["../src/bootstrap-buffer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAElD;;;;;;;;;;;;;;GAcG;AACH,UAAU,eAAe;IACvB,+BAA+B;IAC/B,QAAQ,IAAI,QAAQ,EAAE,CAAC;IACvB,wBAAwB;IACxB,OAAO,IAAI,IAAI,CAAC;CACjB;AAID;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,GAAE,MAAuB,GAAG,eAAe,CAuBpF;AAED,0DAA0D;AAC1D,wBAAgB,kBAAkB,IAAI,eAAe,CAGpD;AAED,qCAAqC;AACrC,wBAAgB,8BAA8B,IAAI,IAAI,CAGrD"}
@@ -1,7 +1,7 @@
1
1
  import { LogHub } from '@aalis/core';
2
2
  let installed = null;
3
3
  /**
4
- * 安装 bootstrap buffer。必须在所有日志产生之前调用(即 `src/index.ts` 顶部)。
4
+ * 安装 bootstrap buffer。必须在所有日志产生之前调用(由 `startAalis` 最先调用)。
5
5
  * 重复调用返回同一实例。
6
6
  */
7
7
  export function installBootstrapBuffer(hub = LogHub.default) {
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap-buffer.js","sourceRoot":"","sources":["../src/bootstrap-buffer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,MAAM,EAAE,MAAM,aAAa,CAAC;AAwBpD,IAAI,SAAS,GAA2B,IAAI,CAAC;AAE7C;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAc,MAAM,CAAC,OAAO;IACjE,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAEhC,IAAI,OAAO,GAAe,EAAE,CAAC;IAC7B,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;QAC9B,IAAI,CAAC,QAAQ;YAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAoB;QAC9B,QAAQ;YACN,OAAO,OAAO,CAAC,KAAK,EAAE,CAAC;QACzB,CAAC;QACD,OAAO;YACL,IAAI,QAAQ;gBAAE,OAAO;YACrB,QAAQ,GAAG,IAAI,CAAC;YAChB,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,EAAE,CAAC;QACR,CAAC;KACF,CAAC;IAEF,SAAS,GAAG,MAAM,CAAC;IACnB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,kBAAkB;IAChC,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAChC,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC;AACnD,CAAC;AAED,qCAAqC;AACrC,MAAM,UAAU,8BAA8B;IAC5C,SAAS,EAAE,OAAO,EAAE,CAAC;IACrB,SAAS,GAAG,IAAI,CAAC;AACnB,CAAC"}
1
+ {"version":3,"file":"bootstrap-buffer.js","sourceRoot":"","sources":["../src/bootstrap-buffer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAyBrC,IAAI,SAAS,GAA2B,IAAI,CAAC;AAE7C;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAc,MAAM,CAAC,OAAO;IACjE,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAEhC,IAAI,OAAO,GAAe,EAAE,CAAC;IAC7B,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;QAC9B,IAAI,CAAC,QAAQ;YAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAoB;QAC9B,QAAQ;YACN,OAAO,OAAO,CAAC,KAAK,EAAE,CAAC;QACzB,CAAC;QACD,OAAO;YACL,IAAI,QAAQ;gBAAE,OAAO;YACrB,QAAQ,GAAG,IAAI,CAAC;YAChB,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,EAAE,CAAC;QACR,CAAC;KACF,CAAC;IAEF,SAAS,GAAG,MAAM,CAAC;IACnB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,kBAAkB;IAChC,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAChC,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC;AACnD,CAAC;AAED,qCAAqC;AACrC,MAAM,UAAU,8BAA8B;IAC5C,SAAS,EAAE,OAAO,EAAE,CAAC;IACrB,SAAS,GAAG,IAAI,CAAC;AACnB,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { type AalisConfig, type HostConfig } from '@aalis/api-host-config';
2
+ import { type App } from '@aalis/core';
3
+ import type { ConfigSyncOptions } from './config-sync.js';
4
+ /**
5
+ * 配置提供者:负责持久化层。文档由宿主在构造前自己加载好传入,provider 不必提供 load()。
6
+ */
7
+ export interface ConfigProvider {
8
+ /**
9
+ * 持久化当前完整文档。不提供表示宿主不持久化(内存配置 / 只读部署),save() 立即兑现、不写盘。
10
+ * 不排队也不去重:上一次未结束时可能再次被调用,实现须可重入;传入的是活对象,
11
+ * 跨 await 使用须自行复制;写的原子性与外部编辑的合并由实现自负。
12
+ */
13
+ save?(config: AalisConfig): void | Promise<void>;
14
+ /** 订阅外部对配置源的变更;返回 dispose */
15
+ watch?(onChange: (next: AalisConfig) => void): () => void;
16
+ }
17
+ /**
18
+ * 宿主持有的配置文档。读写方法与插件经 host-config 看到的相同;落盘与外部变更监听只给宿主:
19
+ * `persist()` 是裸落盘,不记日志,兑现值表示是否交给了 provider 落盘(provider 不提供 save 时为 false),
20
+ * 失败原样以拒绝传出。插件拿到的 `save()` 由 {@link installHostConfig}
21
+ * 包上日志与「拒绝已处理」,两者名字不同,本对象因此不能被误当成 HostConfig 直接交出去。
22
+ * 裁剪政策 `trimUnknownFields` 是宿主的同步政策、不属文档,同样由 {@link installHostConfig} 填入。
23
+ */
24
+ export interface ConfigStore extends Omit<HostConfig, 'save' | 'trimUnknownFields'> {
25
+ persist(): Promise<boolean>;
26
+ /** 订阅外部变更(新快照已过同一道闸替换进文档后回调);单订阅者,返回退订 */
27
+ watch(onChange: () => void): () => void;
28
+ unwatch(): void;
29
+ }
30
+ export declare function createConfigStore(initial: Partial<AalisConfig>, provider?: ConfigProvider): ConfigStore;
31
+ /**
32
+ * 把文档接到 App:以 host-config 服务独占登记在根激活上,应用文档里的服务偏好,
33
+ * 并在服务上线时记录它有无用户偏好。须在注册任何插件之前调用,偏好才先于全部提供者生效。
34
+ * `opts` 传交给配置同步(withPluginConfigSync / installConfigHotReload)的同一份:host-config 的
35
+ * `trimUnknownFields` 据此声明宿主的裁剪政策(缺省与配置同步一样按裁剪)。
36
+ */
37
+ export declare function installHostConfig(app: App, store: ConfigStore, opts?: ConfigSyncOptions): void;
38
+ //# sourceMappingURL=config-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-store.d.ts","sourceRoot":"","sources":["../src/config-store.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAmC,MAAM,wBAAwB,CAAC;AAC5G,OAAO,EAAE,KAAK,GAAG,EAA6B,MAAM,aAAa,CAAC;AAElE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,IAAI,CAAC,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,6BAA6B;IAC7B,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC3D;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACjF,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5B,0CAA0C;IAC1C,KAAK,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IACxC,OAAO,IAAI,IAAI,CAAC;CACjB;AAUD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,EAAE,cAAc,GAAG,WAAW,CAmEvG;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAiC9F"}
@@ -0,0 +1,171 @@
1
+ // ============================================================
2
+ // config-store —— 宿主配置文档(内存态 + 危险键闸 + 落盘委托)与 host-config 服务
3
+ //
4
+ // 运行态(实例配置、禁用态、服务偏好)在 core;本模块持「下次启动用什么」的文档:
5
+ // - 启动:宿主从文档取每个实例的配置与禁用标记交给 app.pluginAll,偏好经根绑定 services.prefer 应用;
6
+ // - 运行:管理动作只改运行态,要跨重启保留的调用方经 host-config 自己写文档并 save()。
7
+ // 本模块不碰文件(无 node: import):持久化与外部变更监听由注入的 ConfigProvider 完成。
8
+ // ============================================================
9
+ import { hostConfig, isConfigSaveRefused } from '@aalis/api-host-config';
10
+ import { events, provide, services } from '@aalis/core';
11
+ import { cloneConfigObject, isUnsafeConfigKey } from '@aalis/schema-config';
12
+ /**
13
+ * 插件 id 不得当 `plugins` / 禁用名单的对象键——那三条会落到原型链。
14
+ * 按 id 取放的入口共用这一抛错,文案给 WebUI 映射 400。
15
+ */
16
+ function assertSafePluginId(id) {
17
+ if (isUnsafeConfigKey(id))
18
+ throw new Error(`插件 id 不合法: ${id}`);
19
+ }
20
+ export function createConfigStore(initial, provider) {
21
+ let config = normalize(initial);
22
+ let unwatchFn = null;
23
+ let onChangeCallback = null;
24
+ const store = {
25
+ get: key => config[key],
26
+ getAll: () => config,
27
+ set: (key, value) => {
28
+ config[key] = value;
29
+ },
30
+ getPluginConfig(instanceId) {
31
+ assertSafePluginId(instanceId);
32
+ return (Object.hasOwn(config.plugins, instanceId) ? config.plugins[instanceId] : {});
33
+ },
34
+ setPluginConfig(instanceId, next) {
35
+ assertSafePluginId(instanceId);
36
+ // 值也过闸:外来对象(WebUI 请求体、工具参数)里 JSON 解出的 __proto__ 等键不进文档,调用方之后改它也写不穿
37
+ config.plugins[instanceId] = cloneConfigObject(next);
38
+ },
39
+ removePluginConfig(instanceId) {
40
+ assertSafePluginId(instanceId);
41
+ delete config.plugins[instanceId];
42
+ },
43
+ isPluginDisabled(instanceId) {
44
+ assertSafePluginId(instanceId);
45
+ return (config.disabledPlugins ?? []).includes(instanceId);
46
+ },
47
+ setPluginEnabled(instanceId, enabled) {
48
+ assertSafePluginId(instanceId);
49
+ config.disabledPlugins ??= [];
50
+ const idx = config.disabledPlugins.indexOf(instanceId);
51
+ if (enabled && idx >= 0)
52
+ config.disabledPlugins.splice(idx, 1);
53
+ else if (!enabled && idx < 0)
54
+ config.disabledPlugins.push(instanceId);
55
+ },
56
+ getServicePreferences: () => config.servicePreferences ?? {},
57
+ setServicePreference(name, contextId) {
58
+ if (isUnsafeConfigKey(name))
59
+ return;
60
+ config.servicePreferences ??= {};
61
+ config.servicePreferences[name] = contextId;
62
+ },
63
+ removeServicePreference(name) {
64
+ if (!config.servicePreferences || isUnsafeConfigKey(name))
65
+ return;
66
+ delete config.servicePreferences[name];
67
+ },
68
+ // async:同步 provider 的抛错也以拒绝传出,调用方统一经 Promise 接住
69
+ persist: async () => {
70
+ if (!provider?.save)
71
+ return false;
72
+ await provider.save(config);
73
+ return true;
74
+ },
75
+ watch(onChange) {
76
+ if (onChangeCallback)
77
+ throw new Error('配置变更只支持一个订阅者,先 unwatch 再订阅');
78
+ onChangeCallback = onChange;
79
+ unwatchFn =
80
+ provider?.watch?.(next => {
81
+ config = normalize(next);
82
+ onChangeCallback?.();
83
+ }) ?? null;
84
+ return () => store.unwatch();
85
+ },
86
+ unwatch() {
87
+ unwatchFn?.();
88
+ unwatchFn = null;
89
+ onChangeCallback = null;
90
+ },
91
+ };
92
+ return store;
93
+ }
94
+ /**
95
+ * 把文档接到 App:以 host-config 服务独占登记在根激活上,应用文档里的服务偏好,
96
+ * 并在服务上线时记录它有无用户偏好。须在注册任何插件之前调用,偏好才先于全部提供者生效。
97
+ * `opts` 传交给配置同步(withPluginConfigSync / installConfigHotReload)的同一份:host-config 的
98
+ * `trimUnknownFields` 据此声明宿主的裁剪政策(缺省与配置同步一样按裁剪)。
99
+ */
100
+ export function installHostConfig(app, store, opts) {
101
+ const host = app.bind({ events, provide, services });
102
+ const { persist, watch: _watch, unwatch: _unwatch, ...doc } = store;
103
+ host.provide(hostConfig, {
104
+ ...doc,
105
+ trimUnknownFields: opts?.trimUnknownFields ?? true,
106
+ // 失败在这里记一笔并标记已处理:不 await 的调用方不会因一次落盘失败变成未处理拒绝、被宿主当致命错误退出
107
+ save: () => {
108
+ const done = (async () => {
109
+ // 宿主不持久化时没有写盘,不记「已保存」
110
+ if (await persist())
111
+ app.logger.info('配置已保存');
112
+ })();
113
+ done.catch(err => {
114
+ try {
115
+ // 拒写是保护盘上外部修改的预期结果,调用方已收到拒绝:一行告警即可,不带栈
116
+ if (isConfigSaveRefused(err))
117
+ app.logger.warn(`配置未保存:${err.message}`);
118
+ else
119
+ app.logger.error('配置保存失败:', err);
120
+ }
121
+ catch {
122
+ /* 上报器自身失败不再外抛 */
123
+ }
124
+ });
125
+ return done;
126
+ },
127
+ }, { exclusive: true });
128
+ for (const [name, contextId] of Object.entries(store.getServicePreferences()))
129
+ host.services.prefer(name, contextId);
130
+ host.events.on('service:registered', name => {
131
+ const pref = store.getServicePreferences()[name];
132
+ if (pref)
133
+ app.logger.debug(`服务 "${name}" 注册时存在用户偏好: ${pref}`);
134
+ });
135
+ }
136
+ // ----- helpers -----
137
+ function copyOwnSafeStringDict(input) {
138
+ const out = {};
139
+ if (!input || typeof input !== 'object' || Array.isArray(input))
140
+ return out;
141
+ for (const key of Object.keys(input)) {
142
+ if (isUnsafeConfigKey(key))
143
+ continue;
144
+ const val = input[key];
145
+ if (typeof val === 'string')
146
+ out[key] = val;
147
+ }
148
+ return out;
149
+ }
150
+ function normalize(input) {
151
+ const merged = {
152
+ name: input.name ?? 'Aalis',
153
+ logLevel: input.logLevel ?? 'info',
154
+ // 键与值都过闸:JSON 解出的 plugins.__proto__ / 插件配置里的 __proto__ 都不能当自有键留下
155
+ plugins: cloneConfigObject((input.plugins ?? {})),
156
+ disabledPlugins: input.disabledPlugins ?? [],
157
+ };
158
+ for (const [key, value] of Object.entries(input)) {
159
+ if (key === 'name' || key === 'logLevel' || key === 'plugins' || key === 'disabledPlugins')
160
+ continue;
161
+ if (isUnsafeConfigKey(key))
162
+ continue;
163
+ if (key === 'servicePreferences') {
164
+ merged.servicePreferences = copyOwnSafeStringDict(value);
165
+ continue;
166
+ }
167
+ merged[key] = value;
168
+ }
169
+ return merged;
170
+ }
171
+ //# sourceMappingURL=config-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-store.js","sourceRoot":"","sources":["../src/config-store.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,6 @@
1
- import type { App } from '@aalis/core';
1
+ import { type App, type PluginDefinition } from '@aalis/core';
2
+ import type { ConfigStore } from './config-store.js';
3
+ import type { PluginDiscovery, PluginLoader } from './plugin-discovery.js';
2
4
  export interface ConfigSyncOptions {
3
5
  /**
4
6
  * 是否按 configSchema 裁剪未知字段(默认 `true`)。
@@ -11,19 +13,36 @@ export interface ConfigSyncOptions {
11
13
  * 将各插件 schema 派生默认值中缺失的字段合并到配置;同时按 configSchema
12
14
  * 移除多余字段。返回发生变更的插件 instanceId 列表。
13
15
  *
14
- * 副作用:对每个变化条目 setPluginConfig;若有变化最终调用 save()。
15
- * 插件的 configSchema 经 `getPlugin(instanceId).module` 读取
16
+ * 覆盖已登记的实例,以及文档里新出现、尚未登记的 `name:suffix` 实例(模块已登记且 reusable 时):
17
+ * 后者随后由热重载按文档登记,先在这里规范化,首次 apply 拿到的就是带默认值的这一份。
18
+ *
19
+ * 副作用:对每个变化条目 setPluginConfig;若有变化最终落盘。
20
+ * 插件的 configSchema 经 `getPlugin(instanceId).definition` 读取
16
21
  * (core 的状态摘要不携带配置详情)。
17
22
  */
18
- export declare function syncPluginDefaults(app: App, opts?: ConfigSyncOptions): string[];
23
+ export declare function syncPluginDefaults(app: App, store: ConfigStore, opts?: ConfigSyncOptions): string[];
24
+ /**
25
+ * 宿主加载政策:导入定义后、交给 Core 注册前规范化主实例与已配置的复用实例。
26
+ * 首次加载批次只落盘一次;随后市场 rescan 的 load/reload 也走同一政策。
27
+ * 发现驱动随后按文档取每个实例的配置交给 core,首次 apply 拿到的就是规范化后的这一份。
28
+ */
29
+ export declare function withPluginConfigSync(loader: PluginLoader, app: App, store: ConfigStore, opts?: ConfigSyncOptions): {
30
+ loader: {
31
+ reload?: ((descriptor: import("./plugin-discovery.js").PluginDescriptor) => Promise<PluginDefinition<{}> | null>) | undefined;
32
+ discover: () => Promise<import("./plugin-discovery.js").PluginDescriptor[]>;
33
+ load: (descriptor: import("./plugin-discovery.js").PluginDescriptor) => Promise<PluginDefinition<{}> | null>;
34
+ };
35
+ finishInitialLoad(): void;
36
+ };
19
37
  /**
20
- * 配置外部变更时的处理:先按启动路径同一政策同步,再重新计算各插件配置
21
- * 并热重载差异(updateConfig → bounce)。
38
+ * 配置外部变更时的处理:先卸载文件里已没有配置段的 `name:suffix` 实例,再按启动路径同一政策同步,
39
+ * 重新计算各插件配置并热重载差异(updateConfig → bounce),最后经发现驱动登记文件里新出现的后缀实例
40
+ * (判据同冷启动与热扫描)。
22
41
  */
23
- export declare function handleConfigChanged(app: App, opts?: ConfigSyncOptions): Promise<void>;
42
+ export declare function handleConfigChanged(app: App, store: ConfigStore, discovery: Pick<PluginDiscovery, 'registerConfiguredInstances'>, opts?: ConfigSyncOptions): Promise<void>;
24
43
  /**
25
- * 接管配置外部变更监听(provider 不支持 watch 时为 no-op)。
44
+ * 接管配置外部变更监听(provider 不支持 watch 时为 no-op),在 app:stopping(在飞动作排干后)停止监听。
26
45
  * startAalis 默认调用;嵌入式宿主可自行选择是否接。
27
46
  */
28
- export declare function installConfigHotReload(app: App, opts?: ConfigSyncOptions): void;
47
+ export declare function installConfigHotReload(app: App, store: ConfigStore, discovery: Pick<PluginDiscovery, 'registerConfiguredInstances'>, opts?: ConfigSyncOptions): void;
29
48
  //# sourceMappingURL=config-sync.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config-sync.d.ts","sourceRoot":"","sources":["../src/config-sync.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAGvC,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,MAAM,EAAE,CAyC/E;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B3F;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAE/E"}
1
+ {"version":3,"file":"config-sync.d.ts","sourceRoot":"","sources":["../src/config-sync.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,GAAG,EAAU,KAAK,gBAAgB,EAAmB,MAAM,aAAa,CAAC;AAEvF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE3E,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,MAAM,EAAE,CAenG;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,iBAAiB;;;;;;;EAmChH;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE,6BAA6B,CAAC,EAC/D,IAAI,CAAC,EAAE,iBAAiB,GACvB,OAAO,CAAC,IAAI,CAAC,CAkCf;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE,6BAA6B,CAAC,EAC/D,IAAI,CAAC,EAAE,iBAAiB,GACvB,IAAI,CAGN"}
@@ -2,89 +2,130 @@
2
2
  // config-sync —— 插件配置同步政策 + 配置热重载编排(宿主政策层)
3
3
  //
4
4
  // - syncPluginDefaults:schema 派生默认值回填 + 按 configSchema 裁剪未知字段
5
- // - handleConfigChanged / installConfigHotReload:配置外部变更的 diff + bounce 编排
5
+ // - handleConfigChanged / installConfigHotReload:配置外部变更的 diff + bounce 编排(后缀实例随文件增删登记或卸载)
6
6
  //
7
- // 这些是**政策**(要不要裁剪、怎么合并、何时 bounce),core 只持有机制
8
- // (配置快照 get/set、watch 透传、updateConfig)。不接本模块的嵌入式
9
- // 宿主将没有自动配置同步与热重载——需要时用公开 API 自行编排。
7
+ // 这些是**政策**(要不要裁剪、怎么合并、何时 bounce)。配置文档在 config-store,core 只持有
8
+ // 运行态与 updateConfig 机制。不接本模块的嵌入式宿主将没有自动配置同步与热重载——需要时用
9
+ // 公开 API 自行编排。
10
10
  // ============================================================
11
- import { defaultsFrom, validateConfig } from '@aalis/schema-config';
11
+ import { isConfigSaveRefused } from '@aalis/api-host-config';
12
+ import { events, parseInstanceId } from '@aalis/core';
13
+ import { defaultsFrom, removeExtraFields, validateConfig } from '@aalis/schema-config';
12
14
  /**
13
15
  * 将各插件 schema 派生默认值中缺失的字段合并到配置;同时按 configSchema
14
16
  * 移除多余字段。返回发生变更的插件 instanceId 列表。
15
17
  *
16
- * 副作用:对每个变化条目 setPluginConfig;若有变化最终调用 save()。
17
- * 插件的 configSchema 经 `getPlugin(instanceId).module` 读取
18
+ * 覆盖已登记的实例,以及文档里新出现、尚未登记的 `name:suffix` 实例(模块已登记且 reusable 时):
19
+ * 后者随后由热重载按文档登记,先在这里规范化,首次 apply 拿到的就是带默认值的这一份。
20
+ *
21
+ * 副作用:对每个变化条目 setPluginConfig;若有变化最终落盘。
22
+ * 插件的 configSchema 经 `getPlugin(instanceId).definition` 读取
18
23
  * (core 的状态摘要不携带配置详情)。
19
24
  */
20
- export function syncPluginDefaults(app, opts) {
21
- const trim = opts?.trimUnknownFields ?? true;
22
- const config = app.ctx.config;
25
+ export function syncPluginDefaults(app, store, opts) {
23
26
  const changed = [];
24
27
  for (const status of app.plugins.getStatus()) {
25
28
  const entry = app.plugins.getPlugin(status.instanceId);
26
29
  if (!entry)
27
30
  continue;
28
- const schema = entry.module.configSchema;
29
- const defaults = defaultsFrom(schema);
30
- const fileConfig = config.getPluginConfig(status.instanceId);
31
- let merged = deepMergeDefaults(defaults, fileConfig);
32
- if (trim && schema && Object.keys(schema).length > 0) {
33
- const removed = [];
34
- merged = removeExtraFields(merged, schema, removed);
35
- if (removed.length > 0) {
36
- app.logger.warn(`配置同步:${status.instanceId} 裁掉 schema 外字段 [${removed.join(', ')}]`);
37
- }
38
- }
39
- // 结构校验只告警不拒载:存量脏值不该打死在跑的实例,但必须出声——
40
- // 静默的坏配置曾让压缩机制无声死亡一天半(summaryLLM 旧格式 ref)。
41
- // 禁用插件跳过:其配置是休眠数据,"必填缺失"等问题在启用后自然浮现。
42
- if (status.state !== 'disabled') {
43
- const problems = validateConfig(schema, merged);
44
- if (problems.length > 0) {
45
- app.logger.warn(`配置校验:${status.instanceId} 有 ${problems.length} 处问题(仅告警,不影响加载):` +
46
- problems.map(p => `${p.path}: ${p.message}`).join(';'));
47
- }
48
- }
49
- if (JSON.stringify(merged) !== JSON.stringify(fileConfig)) {
50
- config.setPluginConfig(status.instanceId, merged);
31
+ if (syncPluginConfig(app, store, entry.definition, status.instanceId, opts))
51
32
  changed.push(status.instanceId);
52
- }
53
33
  }
54
- // 尽力而为:同步失败不该拦住启动/热重载;同步 provider 的抛错仍同步冒出,这里只接异步拒绝。
34
+ for (const id of Object.keys(store.get('plugins'))) {
35
+ const { moduleName, suffix } = parseInstanceId(id);
36
+ if (!suffix || app.plugins.getPlugin(id))
37
+ continue;
38
+ const definition = app.plugins.getPlugin(moduleName)?.definition;
39
+ if (definition?.reusable && syncPluginConfig(app, store, definition, id, opts))
40
+ changed.push(id);
41
+ }
55
42
  if (changed.length > 0)
56
- config.save().catch(err => app.logger.warn('配置同步落盘失败:', err));
43
+ saveSyncedConfig(app, store);
57
44
  return changed;
58
45
  }
59
46
  /**
60
- * 配置外部变更时的处理:先按启动路径同一政策同步,再重新计算各插件配置
61
- * 并热重载差异(updateConfig → bounce)。
47
+ * 宿主加载政策:导入定义后、交给 Core 注册前规范化主实例与已配置的复用实例。
48
+ * 首次加载批次只落盘一次;随后市场 rescan 的 load/reload 也走同一政策。
49
+ * 发现驱动随后按文档取每个实例的配置交给 core,首次 apply 拿到的就是规范化后的这一份。
50
+ */
51
+ export function withPluginConfigSync(loader, app, store, opts) {
52
+ let initialLoad = true;
53
+ let initialChanged = false;
54
+ const prepare = async (loaded) => {
55
+ const definition = await loaded;
56
+ if (!definition)
57
+ return null;
58
+ const ids = [definition.name];
59
+ if (definition.reusable) {
60
+ for (const id of Object.keys(store.get('plugins'))) {
61
+ const { moduleName, suffix } = parseInstanceId(id);
62
+ if (suffix && moduleName === definition.name)
63
+ ids.push(id);
64
+ }
65
+ }
66
+ let changed = false;
67
+ for (const id of ids) {
68
+ if (!syncPluginConfig(app, store, definition, id, opts))
69
+ continue;
70
+ app.logger.debug(`同步插件配置: ${id}`);
71
+ changed = true;
72
+ }
73
+ if (initialLoad)
74
+ initialChanged ||= changed;
75
+ else if (changed)
76
+ saveSyncedConfig(app, store);
77
+ return definition;
78
+ };
79
+ return {
80
+ loader: {
81
+ discover: () => loader.discover(),
82
+ load: descriptor => prepare(loader.load(descriptor)),
83
+ ...(loader.reload ? { reload: descriptor => prepare(loader.reload(descriptor)) } : {}),
84
+ },
85
+ finishInitialLoad() {
86
+ initialLoad = false;
87
+ if (initialChanged)
88
+ saveSyncedConfig(app, store);
89
+ initialChanged = false;
90
+ },
91
+ };
92
+ }
93
+ /**
94
+ * 配置外部变更时的处理:先卸载文件里已没有配置段的 `name:suffix` 实例,再按启动路径同一政策同步,
95
+ * 重新计算各插件配置并热重载差异(updateConfig → bounce),最后经发现驱动登记文件里新出现的后缀实例
96
+ * (判据同冷启动与热扫描)。
62
97
  */
63
- export async function handleConfigChanged(app, opts) {
98
+ export async function handleConfigChanged(app, store, discovery, opts) {
64
99
  app.logger.info('检测到配置变更,正在热重载...');
65
100
  try {
101
+ // 后缀实例由配置段定义,冷启动只登记文件里有的;重载同样以文件为准,没有配置段的就卸载,新出现的在最后登记。
102
+ // 留着不管的话,下面的同步会把它当成缺配置的实例:运行态配置被换成默认值(无默认值时是 {}),
103
+ // 默认值还会作为一个新配置段写回文件。
104
+ const plugins = store.get('plugins');
105
+ for (const status of app.plugins.getStatus()) {
106
+ if (!parseInstanceId(status.instanceId).suffix || Object.hasOwn(plugins, status.instanceId))
107
+ continue;
108
+ app.logger.info(`实例 ${status.instanceId} 已不在配置文件中,正在卸载...`);
109
+ await app.plugins.unload(status.instanceId);
110
+ }
66
111
  // 与启动路径同一政策先同步一遍(补 schema 派生默认值缺失字段 + 裁剪 schema 外字段)
67
112
  // ——否则热重载读入的原始快照会绕过政策,内存态与启动态在字段清理上不一致。
68
- const synced = syncPluginDefaults(app, opts);
113
+ const synced = syncPluginDefaults(app, store, opts);
69
114
  for (const id of synced)
70
115
  app.logger.debug(`热重载配置同步: ${id}`);
71
- let changed = false;
116
+ // 每次 updateConfig 收尾的重算都会发 plugins:changed,这里不必再补发
72
117
  for (const status of app.plugins.getStatus()) {
73
118
  const entry = app.plugins.getPlugin(status.instanceId);
74
119
  if (!entry)
75
120
  continue;
76
- const defaults = defaultsFrom(entry.module.configSchema);
77
- const fileConfig = app.ctx.config.getPluginConfig(status.instanceId);
78
- const newConfig = { ...defaults, ...fileConfig };
121
+ const newConfig = store.getPluginConfig(status.instanceId);
79
122
  if (JSON.stringify(newConfig) !== JSON.stringify(entry.config)) {
80
123
  app.logger.info(`插件 ${status.instanceId} 配置已变更,正在重新加载...`);
81
124
  await app.plugins.updateConfig(status.instanceId, newConfig);
82
- changed = true;
83
125
  }
84
126
  }
85
- if (changed) {
86
- await app.ctx.emit('plugins:changed');
87
- }
127
+ // 按上面已规范化的文档登记,首次 apply 即带默认值
128
+ await discovery.registerConfiguredInstances();
88
129
  app.logger.info('配置热重载完成');
89
130
  }
90
131
  catch (e) {
@@ -92,13 +133,46 @@ export async function handleConfigChanged(app, opts) {
92
133
  }
93
134
  }
94
135
  /**
95
- * 接管配置外部变更监听(provider 不支持 watch 时为 no-op)。
136
+ * 接管配置外部变更监听(provider 不支持 watch 时为 no-op),在 app:stopping(在飞动作排干后)停止监听。
96
137
  * startAalis 默认调用;嵌入式宿主可自行选择是否接。
97
138
  */
98
- export function installConfigHotReload(app, opts) {
99
- app.ctx.config.watch(() => void handleConfigChanged(app, opts));
139
+ export function installConfigHotReload(app, store, discovery, opts) {
140
+ store.watch(() => void handleConfigChanged(app, store, discovery, opts));
141
+ app.bind({ events }).events.on('app:stopping', () => store.unwatch());
100
142
  }
101
143
  // ---- helpers ----
144
+ function syncPluginConfig(app, store, definition, id, opts) {
145
+ const schema = definition.configSchema;
146
+ const fileConfig = store.getPluginConfig(id);
147
+ let merged = deepMergeDefaults(defaultsFrom(schema), fileConfig);
148
+ if ((opts?.trimUnknownFields ?? true) && schema && Object.keys(schema).length > 0) {
149
+ const removed = [];
150
+ merged = removeExtraFields(merged, schema, removed);
151
+ if (removed.length > 0)
152
+ app.logger.warn(`配置同步:${id} 裁掉 schema 外字段 [${removed.join(', ')}]`);
153
+ }
154
+ // 脏值只告警不拒载;禁用实例的休眠配置不产生必填缺失噪音。
155
+ if (!store.isPluginDisabled(id)) {
156
+ const problems = validateConfig(schema, merged);
157
+ if (problems.length > 0) {
158
+ app.logger.warn(`配置校验:${id} 有 ${problems.length} 处问题(仅告警,不影响加载):` +
159
+ problems.map(p => `${p.path}: ${p.message}`).join(';'));
160
+ }
161
+ }
162
+ if (JSON.stringify(merged) === JSON.stringify(fileConfig))
163
+ return false;
164
+ store.setPluginConfig(id, merged);
165
+ return true;
166
+ }
167
+ function saveSyncedConfig(app, store) {
168
+ store.persist().catch(err => {
169
+ // 拒写是保护盘上外部修改的预期结果,与 host-config 的 save 同一口径:一行告警,不带栈
170
+ if (isConfigSaveRefused(err))
171
+ app.logger.warn(`配置同步未落盘:${err.message}`);
172
+ else
173
+ app.logger.warn('配置同步落盘失败:', err);
174
+ });
175
+ }
102
176
  /**
103
177
  * 深度合并默认值:只填充缺失的键,不覆盖已有值。
104
178
  * 嵌套对象会递归合并;数组与基础类型按"已存在则保留"处理。
@@ -120,35 +194,4 @@ function deepMergeDefaults(defaults, current) {
120
194
  }
121
195
  return result;
122
196
  }
123
- /**
124
- * 移除多余字段。configSchema 是插件配置的**唯一声明来源**(默认值也从它派生),
125
- * 所以它的键集就是完整的白名单:不在 schema 里的字段,要么是用户手写的错别字,
126
- * 要么是已废弃的旧字段,裁掉即归位。无 schema 的插件不裁(见调用方守卫)。
127
- */
128
- function removeExtraFields(config, schema, removed, prefix = '') {
129
- const result = {};
130
- for (const [key, value] of Object.entries(config)) {
131
- if (!(key in schema)) {
132
- // 被裁的键必须留痕——静默裁剪会让「字段被吃掉」与「用户没配」不可分辨,
133
- // tool-browser 的 allowedHosts 正是这样潜伏多年(用户照注释配了、启动即被抹、无人知晓)。
134
- removed?.push(prefix + key);
135
- continue;
136
- }
137
- const schemaDef = schema[key];
138
- if (schemaDef.type === 'array') {
139
- result[key] = value;
140
- }
141
- else if (schemaDef.fields &&
142
- typeof schemaDef.fields === 'object' &&
143
- value !== null &&
144
- typeof value === 'object' &&
145
- !Array.isArray(value)) {
146
- result[key] = removeExtraFields(value, schemaDef.fields, removed, `${prefix + key}.`);
147
- }
148
- else {
149
- result[key] = value;
150
- }
151
- }
152
- return result;
153
- }
154
197
  //# sourceMappingURL=config-sync.js.map