@zhin.js/core 1.0.24 → 1.0.26

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 (211) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +84 -342
  3. package/lib/adapter.d.ts +45 -1
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +182 -1
  6. package/lib/adapter.js.map +1 -1
  7. package/lib/ai/agent.d.ts +126 -0
  8. package/lib/ai/agent.d.ts.map +1 -0
  9. package/lib/ai/agent.js +645 -0
  10. package/lib/ai/agent.js.map +1 -0
  11. package/lib/ai/context-manager.d.ts +213 -0
  12. package/lib/ai/context-manager.d.ts.map +1 -0
  13. package/lib/ai/context-manager.js +313 -0
  14. package/lib/ai/context-manager.js.map +1 -0
  15. package/lib/ai/conversation-memory.d.ts +181 -0
  16. package/lib/ai/conversation-memory.d.ts.map +1 -0
  17. package/lib/ai/conversation-memory.js +581 -0
  18. package/lib/ai/conversation-memory.js.map +1 -0
  19. package/lib/ai/follow-up.d.ts +131 -0
  20. package/lib/ai/follow-up.d.ts.map +1 -0
  21. package/lib/ai/follow-up.js +265 -0
  22. package/lib/ai/follow-up.js.map +1 -0
  23. package/lib/ai/index.d.ts +29 -0
  24. package/lib/ai/index.d.ts.map +1 -0
  25. package/lib/ai/index.js +34 -0
  26. package/lib/ai/index.js.map +1 -0
  27. package/lib/ai/init.d.ts +30 -0
  28. package/lib/ai/init.d.ts.map +1 -0
  29. package/lib/ai/init.js +424 -0
  30. package/lib/ai/init.js.map +1 -0
  31. package/lib/ai/output.d.ts +93 -0
  32. package/lib/ai/output.d.ts.map +1 -0
  33. package/lib/ai/output.js +176 -0
  34. package/lib/ai/output.js.map +1 -0
  35. package/lib/ai/providers/anthropic.d.ts +23 -0
  36. package/lib/ai/providers/anthropic.d.ts.map +1 -0
  37. package/lib/ai/providers/anthropic.js +322 -0
  38. package/lib/ai/providers/anthropic.js.map +1 -0
  39. package/lib/ai/providers/base.d.ts +43 -0
  40. package/lib/ai/providers/base.d.ts.map +1 -0
  41. package/lib/ai/providers/base.js +135 -0
  42. package/lib/ai/providers/base.js.map +1 -0
  43. package/lib/ai/providers/index.d.ts +12 -0
  44. package/lib/ai/providers/index.d.ts.map +1 -0
  45. package/lib/ai/providers/index.js +9 -0
  46. package/lib/ai/providers/index.js.map +1 -0
  47. package/lib/ai/providers/ollama.d.ts +25 -0
  48. package/lib/ai/providers/ollama.d.ts.map +1 -0
  49. package/lib/ai/providers/ollama.js +243 -0
  50. package/lib/ai/providers/ollama.js.map +1 -0
  51. package/lib/ai/providers/openai.d.ts +46 -0
  52. package/lib/ai/providers/openai.d.ts.map +1 -0
  53. package/lib/ai/providers/openai.js +132 -0
  54. package/lib/ai/providers/openai.js.map +1 -0
  55. package/lib/ai/rate-limiter.d.ts +38 -0
  56. package/lib/ai/rate-limiter.d.ts.map +1 -0
  57. package/lib/ai/rate-limiter.js +86 -0
  58. package/lib/ai/rate-limiter.js.map +1 -0
  59. package/lib/ai/service.d.ts +81 -0
  60. package/lib/ai/service.d.ts.map +1 -0
  61. package/lib/ai/service.js +274 -0
  62. package/lib/ai/service.js.map +1 -0
  63. package/lib/ai/session.d.ts +186 -0
  64. package/lib/ai/session.d.ts.map +1 -0
  65. package/lib/ai/session.js +443 -0
  66. package/lib/ai/session.js.map +1 -0
  67. package/lib/ai/tone-detector.d.ts +19 -0
  68. package/lib/ai/tone-detector.d.ts.map +1 -0
  69. package/lib/ai/tone-detector.js +72 -0
  70. package/lib/ai/tone-detector.js.map +1 -0
  71. package/lib/ai/tools.d.ts +45 -0
  72. package/lib/ai/tools.d.ts.map +1 -0
  73. package/lib/ai/tools.js +206 -0
  74. package/lib/ai/tools.js.map +1 -0
  75. package/lib/ai/types.d.ts +264 -0
  76. package/lib/ai/types.d.ts.map +1 -0
  77. package/lib/ai/types.js +6 -0
  78. package/lib/ai/types.js.map +1 -0
  79. package/lib/ai/user-profile.d.ts +56 -0
  80. package/lib/ai/user-profile.d.ts.map +1 -0
  81. package/lib/ai/user-profile.js +130 -0
  82. package/lib/ai/user-profile.js.map +1 -0
  83. package/lib/ai/zhin-agent.d.ts +165 -0
  84. package/lib/ai/zhin-agent.d.ts.map +1 -0
  85. package/lib/ai/zhin-agent.js +707 -0
  86. package/lib/ai/zhin-agent.js.map +1 -0
  87. package/lib/built/adapter-process.d.ts +4 -0
  88. package/lib/built/adapter-process.d.ts.map +1 -1
  89. package/lib/built/adapter-process.js +94 -0
  90. package/lib/built/adapter-process.js.map +1 -1
  91. package/lib/built/ai-trigger.d.ts +89 -0
  92. package/lib/built/ai-trigger.d.ts.map +1 -0
  93. package/lib/built/ai-trigger.js +166 -0
  94. package/lib/built/ai-trigger.js.map +1 -0
  95. package/lib/built/command.d.ts +33 -17
  96. package/lib/built/command.d.ts.map +1 -1
  97. package/lib/built/command.js +71 -44
  98. package/lib/built/command.js.map +1 -1
  99. package/lib/built/component.d.ts +42 -15
  100. package/lib/built/component.d.ts.map +1 -1
  101. package/lib/built/component.js +84 -52
  102. package/lib/built/component.js.map +1 -1
  103. package/lib/built/config.d.ts +54 -5
  104. package/lib/built/config.d.ts.map +1 -1
  105. package/lib/built/config.js +76 -10
  106. package/lib/built/config.js.map +1 -1
  107. package/lib/built/cron.d.ts +41 -18
  108. package/lib/built/cron.d.ts.map +1 -1
  109. package/lib/built/cron.js +106 -63
  110. package/lib/built/cron.js.map +1 -1
  111. package/lib/built/database.d.ts +55 -6
  112. package/lib/built/database.d.ts.map +1 -1
  113. package/lib/built/database.js +93 -22
  114. package/lib/built/database.js.map +1 -1
  115. package/lib/built/dispatcher.d.ts +118 -0
  116. package/lib/built/dispatcher.d.ts.map +1 -0
  117. package/lib/built/dispatcher.js +196 -0
  118. package/lib/built/dispatcher.js.map +1 -0
  119. package/lib/built/permission.d.ts +45 -5
  120. package/lib/built/permission.d.ts.map +1 -1
  121. package/lib/built/permission.js +56 -11
  122. package/lib/built/permission.js.map +1 -1
  123. package/lib/built/skill.d.ts +117 -0
  124. package/lib/built/skill.d.ts.map +1 -0
  125. package/lib/built/skill.js +191 -0
  126. package/lib/built/skill.js.map +1 -0
  127. package/lib/built/tool.d.ts +188 -0
  128. package/lib/built/tool.d.ts.map +1 -0
  129. package/lib/built/tool.js +749 -0
  130. package/lib/built/tool.js.map +1 -0
  131. package/lib/feature.d.ts +75 -0
  132. package/lib/feature.d.ts.map +1 -0
  133. package/lib/feature.js +69 -0
  134. package/lib/feature.js.map +1 -0
  135. package/lib/index.d.ts +6 -0
  136. package/lib/index.d.ts.map +1 -1
  137. package/lib/index.js +11 -0
  138. package/lib/index.js.map +1 -1
  139. package/lib/plugin.d.ts +53 -18
  140. package/lib/plugin.d.ts.map +1 -1
  141. package/lib/plugin.js +301 -31
  142. package/lib/plugin.js.map +1 -1
  143. package/lib/types.d.ts +248 -9
  144. package/lib/types.d.ts.map +1 -1
  145. package/lib/utils.d.ts.map +1 -1
  146. package/lib/utils.js +38 -12
  147. package/lib/utils.js.map +1 -1
  148. package/package.json +4 -4
  149. package/src/adapter.ts +206 -2
  150. package/src/ai/agent.ts +772 -0
  151. package/src/ai/context-manager.ts +440 -0
  152. package/src/ai/conversation-memory.ts +774 -0
  153. package/src/ai/follow-up.ts +357 -0
  154. package/src/ai/index.ts +128 -0
  155. package/src/ai/init.ts +502 -0
  156. package/src/ai/output.ts +261 -0
  157. package/src/ai/providers/anthropic.ts +375 -0
  158. package/src/ai/providers/base.ts +173 -0
  159. package/src/ai/providers/index.ts +13 -0
  160. package/src/ai/providers/ollama.ts +292 -0
  161. package/src/ai/providers/openai.ts +167 -0
  162. package/src/ai/rate-limiter.ts +129 -0
  163. package/src/ai/service.ts +319 -0
  164. package/src/ai/session.ts +544 -0
  165. package/src/ai/tone-detector.ts +89 -0
  166. package/src/ai/tools.ts +218 -0
  167. package/src/ai/types.ts +296 -0
  168. package/src/ai/user-profile.ts +181 -0
  169. package/src/ai/zhin-agent.ts +845 -0
  170. package/src/built/adapter-process.ts +99 -0
  171. package/src/built/ai-trigger.ts +259 -0
  172. package/src/built/command.ts +75 -69
  173. package/src/built/component.ts +94 -76
  174. package/src/built/config.ts +238 -128
  175. package/src/built/cron.ts +117 -101
  176. package/src/built/database.ts +128 -33
  177. package/src/built/dispatcher.ts +332 -0
  178. package/src/built/permission.ts +146 -54
  179. package/src/built/skill.ts +280 -0
  180. package/src/built/tool.ts +928 -0
  181. package/src/feature.ts +113 -0
  182. package/src/index.ts +11 -0
  183. package/src/plugin.ts +359 -69
  184. package/src/types.ts +306 -11
  185. package/src/utils.ts +37 -13
  186. package/tests/adapter.test.ts +153 -1
  187. package/tests/ai/agent.test.ts +614 -0
  188. package/tests/ai/ai-trigger.test.ts +368 -0
  189. package/tests/ai/context-manager.test.ts +413 -0
  190. package/tests/ai/conversation-memory.test.ts +128 -0
  191. package/tests/ai/follow-up.test.ts +175 -0
  192. package/tests/ai/integration.test.ts +584 -0
  193. package/tests/ai/output.test.ts +128 -0
  194. package/tests/ai/providers.integration.test.ts +227 -0
  195. package/tests/ai/rate-limiter.test.ts +108 -0
  196. package/tests/ai/session.test.ts +375 -0
  197. package/tests/ai/setup.ts +308 -0
  198. package/tests/ai/tone-detector.test.ts +80 -0
  199. package/tests/ai/tool.test.ts +800 -0
  200. package/tests/ai/tools-builtin.test.ts +346 -0
  201. package/tests/ai/user-profile.test.ts +73 -0
  202. package/tests/ai/zhin-agent.test.ts +177 -0
  203. package/tests/component-new.test.ts +17 -6
  204. package/tests/config.test.ts +46 -0
  205. package/tests/cron.test.ts +94 -5
  206. package/tests/dispatcher.test.ts +146 -0
  207. package/tests/feature.test.ts +145 -0
  208. package/tests/features-builtin.test.ts +191 -0
  209. package/tests/plugin.test.ts +88 -14
  210. package/tests/skill-feature.test.ts +179 -0
  211. package/tests/tool-feature.test.ts +254 -0
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Component Context
3
- * 管理所有插件注册的组件
2
+ * ComponentFeature
3
+ * 管理所有插件注册的组件,继承自 Feature 抽象类
4
4
  */
5
+ import { Feature, FeatureJSON } from "../feature.js";
5
6
  import { Component, renderComponents } from "../component.js";
6
7
  import { SendOptions, MaybePromise } from "../types.js";
7
- import { Context, Plugin, getPlugin } from "../plugin.js";
8
+ import { Plugin, getPlugin } from "../plugin.js";
8
9
 
9
10
  type Listener = (options: SendOptions) => MaybePromise<SendOptions>;
10
11
 
@@ -21,90 +22,107 @@ declare module "../plugin.js" {
21
22
  namespace Plugin {
22
23
  interface Extensions extends ComponentContextExtensions {}
23
24
  interface Contexts {
24
- component: ComponentService;
25
+ component: ComponentFeature;
25
26
  }
26
27
  }
27
28
  }
28
29
 
29
30
  /**
30
- * 组件服务数据
31
+ * 组件服务 Feature
31
32
  */
32
- export interface ComponentService {
33
+ export class ComponentFeature extends Feature<Component<any>> {
34
+ readonly name = 'component' as const;
35
+ readonly icon = 'Box';
36
+ readonly desc = '组件';
37
+
33
38
  /** 按名称索引 */
34
- readonly byName: Map<string, Component<any>>;
35
- /** 添加组件 */
36
- add(component: Component<any>, pluginName: string): () => void;
37
- /** 获取所有组件名称 */
38
- getAllNames(): string[];
39
- /** 移除组件 */
40
- remove(component: Component<any>): boolean;
41
- /** 按名称获取 */
42
- get(name: string): Component<any> | undefined;
43
- }
39
+ readonly byName = new Map<string, Component<any>>();
44
40
 
45
- /**
46
- * 创建组件 Context
47
- */
48
- export function createComponentService(): Context<'component', ComponentContextExtensions> {
49
- const byName = new Map<string, Component<any>>();
50
- const pluginMap = new Map<Component<any>, string>();
51
- let listener: Listener | undefined;
52
- let rootPlugin: Plugin | undefined;
53
-
54
- const value: ComponentService = {
55
- byName,
56
-
57
- add(component, pluginName) {
58
- byName.set(component.name, component);
59
- pluginMap.set(component, pluginName);
60
- return () => value.remove(component);
61
- },
62
- getAllNames() {
63
- return Array.from(byName.keys());
64
- },
65
- remove(component) {
66
- if (byName.has(component.name)) {
67
- byName.delete(component.name);
68
- pluginMap.delete(component);
69
- return true;
70
- }
71
- return false;
72
- },
73
-
74
- get(name) {
75
- return byName.get(name);
41
+ /** 内部状态:消息渲染监听器 & 宿主插件 */
42
+ #listener?: Listener;
43
+ #rootPlugin?: Plugin;
44
+
45
+ /**
46
+ * 添加组件
47
+ */
48
+ add(component: Component<any>, pluginName: string): () => void {
49
+ this.byName.set(component.name, component);
50
+ return super.add(component, pluginName);
51
+ }
52
+
53
+ /**
54
+ * 移除组件
55
+ */
56
+ remove(component: Component<any>): boolean {
57
+ this.byName.delete(component.name);
58
+ return super.remove(component);
59
+ }
60
+
61
+ /**
62
+ * 获取所有组件名称
63
+ */
64
+ getAllNames(): string[] {
65
+ return Array.from(this.byName.keys());
66
+ }
67
+
68
+ /**
69
+ * 按名称获取组件
70
+ */
71
+ get(name: string): Component<any> | undefined {
72
+ return this.byName.get(name);
73
+ }
74
+
75
+ /**
76
+ * 生命周期: 挂载时注册消息渲染监听器
77
+ */
78
+ mounted(plugin: Plugin): void {
79
+ this.#rootPlugin = plugin;
80
+ this.#listener = (options: SendOptions) => {
81
+ return renderComponents(this.byName, options);
82
+ };
83
+ plugin.root.on('before.sendMessage', this.#listener);
84
+ }
85
+
86
+ /**
87
+ * 生命周期: 销毁时移除监听器
88
+ */
89
+ dispose(): void {
90
+ if (this.#listener && this.#rootPlugin) {
91
+ this.#rootPlugin.root.off('before.sendMessage', this.#listener);
92
+ this.#listener = undefined;
76
93
  }
77
- };
78
-
79
- return {
80
- name: 'component',
81
- description: '组件服务',
82
-
83
- mounted(plugin: Plugin) {
84
- rootPlugin = plugin;
85
- // 创建消息渲染监听器
86
- listener = (options: SendOptions) => {
87
- return renderComponents(byName, options);
88
- };
89
- plugin.root.on('before.sendMessage', listener);
90
- return value;
91
- },
92
-
93
- dispose() {
94
- if (listener && rootPlugin) {
95
- rootPlugin.root.off('before.sendMessage', listener);
96
- listener = undefined;
97
- }
98
- },
99
-
100
- extensions: {
94
+ }
95
+
96
+ /**
97
+ * 序列化为 JSON
98
+ */
99
+ toJSON(pluginName?: string): FeatureJSON {
100
+ const list = pluginName ? this.getByPlugin(pluginName) : this.items;
101
+ return {
102
+ name: this.name,
103
+ icon: this.icon,
104
+ desc: this.desc,
105
+ count: list.length,
106
+ items: list.map(c => ({
107
+ name: c.name,
108
+ type: 'component',
109
+ })),
110
+ };
111
+ }
112
+
113
+ /**
114
+ * 提供给 Plugin.prototype 的扩展方法
115
+ */
116
+ get extensions() {
117
+ const feature = this;
118
+ return {
101
119
  addComponent<T extends Component<any>>(component: T) {
102
120
  const plugin = getPlugin();
103
- const dispose = value.add(component, plugin.name);
121
+ const dispose = feature.add(component, plugin.name);
122
+ plugin.recordFeatureContribution(feature.name, component.name);
104
123
  plugin.onDispose(dispose);
105
124
  return dispose;
106
- }
107
- }
108
- };
125
+ },
126
+ };
127
+ }
109
128
  }
110
-
@@ -1,142 +1,252 @@
1
+ /**
2
+ * ConfigFeature
3
+ * 配置管理服务,继承自 Feature 抽象类
4
+ * 保留原有 ConfigLoader / ConfigService 逻辑,增加 addConfig 扩展
5
+ */
1
6
  import path from "node:path";
2
7
  import fs from "node:fs";
3
- import { stringify as stringifyYaml,parse as parseYaml } from "yaml";
8
+ import { stringify as stringifyYaml, parse as parseYaml } from "yaml";
4
9
  import { Schema } from "@zhin.js/schema";
5
- export class ConfigLoader<T extends object>{
6
- #data: T;
7
- get data(): T {
8
- return this.#proxy(this.#data,this);
9
- }
10
- get raw(): T {
11
- return this.#data;
12
- }
13
- get extension() {
14
- return path.extname(this.filename).toLowerCase();
15
- }
16
- constructor(public filename: string, public initial:T, public schema?: Schema<T>) {
17
- this.#data = this.initial;
18
- }
19
- #proxy<R extends object>(data: R, loader: ConfigLoader<T>) {
20
- return new Proxy(data, {
21
- get(target, prop, receiver) {
22
- // 对于特殊属性(如 Symbol、prototype 等),直接返回原始值
23
- if (typeof prop === 'symbol' || prop === 'constructor' || prop === 'prototype') {
24
- return Reflect.get(target, prop, receiver);
25
- }
26
-
27
- const result= Reflect.get(target, prop, receiver);
28
-
29
- // 对于函数属性(如数组的 map、filter 等),直接返回,不进行代理
30
- if (typeof result === 'function') {
31
- return result;
32
- }
33
-
34
- // 对于对象和数组,递归代理(但排除函数)
35
- if(result instanceof Object && result!==null && typeof result !== 'function') {
36
- return loader.#proxy(result,loader);
37
- }
38
-
39
- // 处理环境变量占位符
40
- if(typeof result==='string') {
41
- if(result.startsWith('\\${') && result.endsWith('}')) return result.slice(1);
42
- if(/^\$\{(.*)\}$/.test(result)){
43
- const content = result.slice(2, -1);
44
- const [key, ...rest] = content.split(':');
45
- const defaultValue = rest.length > 0 ? rest.join(':') : undefined;
46
- return process.env[key] ?? defaultValue ?? (loader.initial as any)[key] ?? result;
47
- }
48
- }
49
- return result;
50
- },
51
- set(target, prop, value, receiver) {
52
- const result= Reflect.set(target, prop, value, receiver);
53
- loader.save(loader.filename);
54
- return result;
55
- },
56
- deleteProperty(target, prop) {
57
- const result= Reflect.deleteProperty(target, prop);
58
- loader.save(loader.filename);
59
- return result;
60
- }
61
- });
62
- }
63
- load() {
64
- const fullPath=path.resolve(process.cwd(), this.filename);
65
- if (!fs.existsSync(fullPath)) {
66
- this.save(fullPath);
10
+ import { Feature, FeatureJSON } from "../feature.js";
11
+ import { getPlugin } from "../plugin.js";
12
+
13
+ // ============================================================================
14
+ // ConfigLoader(保持不变)
15
+ // ============================================================================
16
+
17
+ export class ConfigLoader<T extends object> {
18
+ #data: T;
19
+ get data(): T {
20
+ return this.#proxy(this.#data, this);
21
+ }
22
+ get raw(): T {
23
+ return this.#data;
24
+ }
25
+ get extension() {
26
+ return path.extname(this.filename).toLowerCase();
27
+ }
28
+ constructor(public filename: string, public initial: T, public schema?: Schema<T>) {
29
+ this.#data = this.initial;
30
+ }
31
+ #proxy<R extends object>(data: R, loader: ConfigLoader<T>) {
32
+ return new Proxy(data, {
33
+ get(target, prop, receiver) {
34
+ if (typeof prop === 'symbol' || prop === 'constructor' || prop === 'prototype') {
35
+ return Reflect.get(target, prop, receiver);
67
36
  }
68
- const content = fs.readFileSync(fullPath, "utf-8");
69
- let rawConfig: any;
70
- switch (this.extension) {
71
- case ".json":
72
- rawConfig = JSON.parse(content);
73
- break;
74
- case ".yaml":
75
- case ".yml":
76
- rawConfig = parseYaml(content);
77
- break;
37
+
38
+ const result = Reflect.get(target, prop, receiver);
39
+
40
+ if (typeof result === 'function') {
41
+ return result;
78
42
  }
79
- if(this.schema){
80
- this.#data = this.schema(rawConfig || this.initial) as T;
81
- }else{
82
- this.#data = rawConfig as T;
43
+
44
+ if (result instanceof Object && result !== null && typeof result !== 'function') {
45
+ return loader.#proxy(result, loader);
83
46
  }
84
- }
85
- save(fullPath: string) {
86
- switch (this.extension) {
87
- case ".json":
88
- fs.writeFileSync(fullPath, JSON.stringify(this.#data, null, 2));
89
- break;
90
- case ".yaml":
91
- case ".yml":
92
- fs.writeFileSync(fullPath, stringifyYaml(this.#data));
93
- break;
47
+
48
+ if (typeof result === 'string') {
49
+ if (result.startsWith('\\${') && result.endsWith('}')) return result.slice(1);
50
+ if (/^\$\{(.*)\}$/.test(result)) {
51
+ const content = result.slice(2, -1);
52
+ const [key, ...rest] = content.split(':');
53
+ const defaultValue = rest.length > 0 ? rest.join(':') : undefined;
54
+ return process.env[key] ?? defaultValue ?? (loader.initial as any)[key] ?? result;
55
+ }
94
56
  }
95
- }
96
- }
97
- export namespace ConfigLoader{
98
- export const supportedExtensions = [".json", ".yaml", ".yml"];
99
- export function load<T extends object>(filename: string, initial?:T, schema?: Schema<T>) {
100
- const result = new ConfigLoader<T>(filename, initial??{} as T, schema);
101
- result.load();
102
57
  return result;
58
+ },
59
+ set(target, prop, value, receiver) {
60
+ const result = Reflect.set(target, prop, value, receiver);
61
+ loader.save(loader.filename);
62
+ return result;
63
+ },
64
+ deleteProperty(target, prop) {
65
+ const result = Reflect.deleteProperty(target, prop);
66
+ loader.save(loader.filename);
67
+ return result;
68
+ }
69
+ });
70
+ }
71
+ load() {
72
+ const fullPath = path.resolve(process.cwd(), this.filename);
73
+ if (!fs.existsSync(fullPath)) {
74
+ this.save(fullPath);
103
75
  }
104
- }
105
- export class ConfigService{
106
- configs: Map<string, ConfigLoader<any>> = new Map();
107
- constructor() {
76
+ const content = fs.readFileSync(fullPath, "utf-8");
77
+ let rawConfig: any;
78
+ switch (this.extension) {
79
+ case ".json":
80
+ rawConfig = JSON.parse(content);
81
+ break;
82
+ case ".yaml":
83
+ case ".yml":
84
+ rawConfig = parseYaml(content);
85
+ break;
108
86
  }
109
- load<T extends object>(filename: string, initial?:Partial<T>, schema?: Schema<T>) {
110
- const ext = path.extname(filename).toLowerCase();
111
- if (!ConfigLoader.supportedExtensions.includes(ext)) {
112
- throw new Error(`不支持的配置文件格式: ${ext}`);
113
- }
114
- const config = ConfigLoader.load(filename, initial as T, schema);
115
- this.configs.set(filename, config);
116
- return config;
87
+ if (this.schema) {
88
+ this.#data = this.schema(rawConfig || this.initial) as T;
89
+ } else {
90
+ this.#data = rawConfig as T;
117
91
  }
118
- get<T extends object>(filename: string, initial?:Partial<T>, schema?: Schema<T>): T {
119
- if(!this.configs.has(filename)) this.load(filename, initial, schema);
120
- const config = this.configs.get(filename);
121
- if(!config) throw new Error(`配置文件 ${filename} 未加载`);
122
- return config.data as T;
92
+ }
93
+ save(fullPath: string) {
94
+ switch (this.extension) {
95
+ case ".json":
96
+ fs.writeFileSync(fullPath, JSON.stringify(this.#data, null, 2));
97
+ break;
98
+ case ".yaml":
99
+ case ".yml":
100
+ fs.writeFileSync(fullPath, stringifyYaml(this.#data));
101
+ break;
123
102
  }
124
- getRaw<T extends object>(filename: string, initial?:Partial<T>, schema?: Schema<T>): T {
125
- if(!this.configs.has(filename)) this.load(filename, initial, schema);
126
- const config = this.configs.get(filename);
127
- if(!config) throw new Error(`配置文件 ${filename} 未加载`);
128
- return config.raw as T;
103
+ }
104
+ }
105
+
106
+ export namespace ConfigLoader {
107
+ export const supportedExtensions = [".json", ".yaml", ".yml"];
108
+ export function load<T extends object>(filename: string, initial?: T, schema?: Schema<T>) {
109
+ const result = new ConfigLoader<T>(filename, initial ?? {} as T, schema);
110
+ result.load();
111
+ return result;
112
+ }
113
+ }
114
+
115
+ // ============================================================================
116
+ // ConfigFeature
117
+ // ============================================================================
118
+
119
+ /**
120
+ * 配置项声明记录
121
+ */
122
+ export interface ConfigRecord {
123
+ key: string;
124
+ defaultValue: any;
125
+ }
126
+
127
+ /**
128
+ * ConfigFeature 扩展方法类型
129
+ */
130
+ export interface ConfigContextExtensions {
131
+ /** 声明插件配置项(key + 默认值),如果配置文件中不存在则写入默认值 */
132
+ addConfig(key: string, defaultValue: any): () => void;
133
+ }
134
+
135
+ declare module "../plugin.js" {
136
+ namespace Plugin {
137
+ interface Extensions extends ConfigContextExtensions {}
138
+ interface Contexts {
139
+ config: ConfigFeature;
129
140
  }
130
- /**
131
- * 更新配置文件内容
132
- * @param filename 配置文件名
133
- * @param data 新的配置数据
134
- */
135
- set<T extends object>(filename: string, data: T): void {
136
- const config = this.configs.get(filename);
137
- if(!config) throw new Error(`配置文件 ${filename} 未加载`);
138
- // 直接更新内部数据(会触发 Proxy 的 set 拦截器并自动保存)
139
- Object.assign(config.raw, data);
140
- config.save(path.resolve(process.cwd(), filename));
141
+ }
142
+ }
143
+
144
+ export class ConfigFeature extends Feature<ConfigRecord> {
145
+ readonly name = 'config' as const;
146
+ readonly icon = 'Settings';
147
+ readonly desc = '配置';
148
+
149
+ /** 内部配置文件管理 */
150
+ readonly configs: Map<string, ConfigLoader<any>> = new Map();
151
+
152
+ /** 主配置文件名(第一个加载的配置文件) */
153
+ #primaryConfigFile: string = '';
154
+
155
+ /**
156
+ * 加载配置文件
157
+ */
158
+ load<T extends object>(filename: string, initial?: Partial<T>, schema?: Schema<T>): ConfigLoader<T> {
159
+ const ext = path.extname(filename).toLowerCase();
160
+ if (!ConfigLoader.supportedExtensions.includes(ext)) {
161
+ throw new Error(`不支持的配置文件格式: ${ext}`);
162
+ }
163
+ const config = ConfigLoader.load(filename, initial as T, schema);
164
+ this.configs.set(filename, config);
165
+ if (!this.#primaryConfigFile) {
166
+ this.#primaryConfigFile = filename;
141
167
  }
142
- }
168
+ return config;
169
+ }
170
+
171
+ /**
172
+ * 获取配置数据(代理模式,自动保存)
173
+ */
174
+ get<T extends object>(filename: string, initial?: Partial<T>, schema?: Schema<T>): T {
175
+ if (!this.configs.has(filename)) this.load(filename, initial, schema);
176
+ const config = this.configs.get(filename);
177
+ if (!config) throw new Error(`配置文件 ${filename} 未加载`);
178
+ return config.data as T;
179
+ }
180
+
181
+ /**
182
+ * 获取原始配置数据
183
+ */
184
+ getRaw<T extends object>(filename: string, initial?: Partial<T>, schema?: Schema<T>): T {
185
+ if (!this.configs.has(filename)) this.load(filename, initial, schema);
186
+ const config = this.configs.get(filename);
187
+ if (!config) throw new Error(`配置文件 ${filename} 未加载`);
188
+ return config.raw as T;
189
+ }
190
+
191
+ /**
192
+ * 更新配置文件内容
193
+ */
194
+ set<T extends object>(filename: string, data: T): void {
195
+ const config = this.configs.get(filename);
196
+ if (!config) throw new Error(`配置文件 ${filename} 未加载`);
197
+ Object.assign(config.raw, data);
198
+ config.save(path.resolve(process.cwd(), filename));
199
+ }
200
+
201
+ /**
202
+ * 序列化为 JSON
203
+ */
204
+ toJSON(pluginName?: string): FeatureJSON {
205
+ const list = pluginName ? this.getByPlugin(pluginName) : this.items;
206
+ return {
207
+ name: this.name,
208
+ icon: this.icon,
209
+ desc: this.desc,
210
+ count: list.length,
211
+ items: list.map(r => ({
212
+ name: r.key,
213
+ // 不暴露 defaultValue 以防止泄露密钥/令牌
214
+ })),
215
+ };
216
+ }
217
+
218
+ /**
219
+ * 提供给 Plugin.prototype 的扩展方法
220
+ */
221
+ get extensions() {
222
+ const feature = this;
223
+ return {
224
+ addConfig(key: string, defaultValue: any) {
225
+ const plugin = getPlugin();
226
+
227
+ // 尝试写入主配置文件(如果 key 不存在)
228
+ if (feature.#primaryConfigFile) {
229
+ const config = feature.configs.get(feature.#primaryConfigFile);
230
+ if (config) {
231
+ const raw = config.raw as Record<string, any>;
232
+ if (!(key in raw)) {
233
+ raw[key] = defaultValue;
234
+ config.save(path.resolve(process.cwd(), feature.#primaryConfigFile));
235
+ }
236
+ }
237
+ }
238
+
239
+ const record: ConfigRecord = { key, defaultValue };
240
+ const dispose = feature.add(record, plugin.name);
241
+ plugin.recordFeatureContribution(feature.name, key);
242
+ plugin.onDispose(dispose);
243
+ return dispose;
244
+ },
245
+ };
246
+ }
247
+ }
248
+
249
+ /**
250
+ * @deprecated Use ConfigFeature instead
251
+ */
252
+ export const ConfigService = ConfigFeature;