foliko 1.1.67 → 1.1.69

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 (157) hide show
  1. package/.claude/settings.local.json +19 -10
  2. package/.dockerignore +45 -45
  3. package/.env.example +56 -56
  4. package/CLAUDE.md +2 -2
  5. package/README.md +13 -13
  6. package/SPEC.md +3 -3
  7. package/cli/src/commands/chat.js +2 -20
  8. package/cli/src/commands/list.js +7 -6
  9. package/cli/src/commands/plugin.js +3 -2
  10. package/cli/src/daemon.js +2 -2
  11. package/cli/src/ui/chat-ui-old.js +15 -4
  12. package/cli/src/ui/chat-ui.js +236 -203
  13. package/cli/src/ui/footer-bar.js +20 -46
  14. package/cli/src/ui/message-bubble.js +24 -2
  15. package/cli/src/ui/status-bar.js +177 -0
  16. package/cli/src/utils/config.js +29 -0
  17. package/cli/src/utils/plugin-config.js +1 -1
  18. package/docker-compose.yml +33 -33
  19. package/docs/features.md +120 -120
  20. package/docs/quick-reference.md +160 -160
  21. package/docs/user-manual.md +1391 -1391
  22. package/examples/ambient-example.js +2 -2
  23. package/examples/bootstrap.js +3 -3
  24. package/examples/test-chat.js +1 -1
  25. package/examples/test-reload.js +1 -1
  26. package/examples/test-telegram.js +1 -1
  27. package/examples/test-tg-bot.js +1 -1
  28. package/examples/test-tg-simple.js +2 -2
  29. package/examples/test-tg.js +1 -1
  30. package/examples/test-think.js +1 -1
  31. package/examples/test-weixin-feishu.js +3 -3
  32. package/package.json +3 -1
  33. package/plugins/ambient-agent/index.js +1 -1
  34. package/plugins/audit-plugin.js +84 -29
  35. package/plugins/coordinator-plugin.js +14 -12
  36. package/plugins/data-splitter-plugin.js +323 -0
  37. package/plugins/default-plugins.js +23 -12
  38. package/plugins/email/index.js +1 -1
  39. package/plugins/extension-executor-plugin.js +87 -9
  40. package/plugins/feishu-plugin.js +118 -16
  41. package/plugins/file-system-plugin.js +68 -50
  42. package/plugins/gate-trading.js +10 -10
  43. package/plugins/install-plugin.js +7 -7
  44. package/plugins/memory-plugin.js +9 -12
  45. package/plugins/plugin-manager-plugin.js +12 -14
  46. package/plugins/python-executor-plugin.js +1 -1
  47. package/plugins/python-plugin-loader.js +1 -1
  48. package/plugins/qq-plugin.js +151 -24
  49. package/plugins/rules-plugin.js +8 -8
  50. package/plugins/scheduler-plugin.js +24 -20
  51. package/plugins/session-plugin.js +313 -397
  52. package/plugins/storage-plugin.js +235 -175
  53. package/plugins/subagent-plugin.js +17 -13
  54. package/plugins/telegram-plugin.js +116 -17
  55. package/plugins/think-plugin.js +64 -60
  56. package/plugins/tools-plugin.js +8 -8
  57. package/plugins/web-plugin.js +2 -2
  58. package/plugins/weixin-plugin.js +107 -24
  59. package/skills/find-skills/AGENTS.md +2 -2
  60. package/skills/find-skills/SKILL.md +133 -133
  61. package/skills/foliko-dev/AGENTS.md +236 -236
  62. package/skills/foliko-dev/SKILL.md +19 -19
  63. package/skills/mcp-usage/SKILL.md +200 -200
  64. package/skills/plugin-guide/SKILL.md +4 -4
  65. package/skills/python-plugin-dev/SKILL.md +5 -5
  66. package/skills/skill-guide/SKILL.md +104 -6
  67. package/skills/subagent-guide/SKILL.md +237 -237
  68. package/skills/workflow-guide/SKILL.md +646 -646
  69. package/src/capabilities/skill-manager.js +124 -17
  70. package/src/capabilities/workflow-engine.js +3 -3
  71. package/src/core/agent-chat.js +72 -26
  72. package/src/core/agent.js +17 -27
  73. package/src/core/branch-summary-auto.js +206 -0
  74. package/src/core/chat-session.js +45 -169
  75. package/src/core/command-registry.js +200 -0
  76. package/src/core/constants.js +198 -0
  77. package/src/core/context-compressor.js +702 -326
  78. package/src/core/context-manager.js +0 -1
  79. package/src/core/enhanced-context-compressor.js +210 -0
  80. package/src/core/framework.js +260 -84
  81. package/src/core/jsonl-storage.js +253 -0
  82. package/src/core/plugin-base.js +7 -5
  83. package/src/core/plugin-manager.js +15 -10
  84. package/src/core/provider-registry.js +159 -0
  85. package/src/core/provider.js +2 -0
  86. package/src/core/session-entry.js +225 -0
  87. package/src/core/session-manager.js +701 -0
  88. package/src/core/storage-manager.js +494 -0
  89. package/src/core/sub-agent-config.js +1 -1
  90. package/src/core/subagent.js +16 -135
  91. package/src/core/token-counter.js +177 -58
  92. package/src/core/tool-executor.js +2 -70
  93. package/src/core/ui-extension-context.js +174 -0
  94. package/src/executors/mcp-executor.js +27 -16
  95. package/src/utils/chat-queue.js +11 -22
  96. package/src/utils/data-splitter.js +345 -0
  97. package/src/utils/logger.js +152 -180
  98. package/src/utils/message-validator.js +283 -0
  99. package/src/utils/plugin-helpers.js +2 -2
  100. package/src/utils/retry.js +168 -22
  101. package/website_v2/docs/api.html +1 -1
  102. package/website_v2/docs/configuration.html +2 -2
  103. package/website_v2/docs/plugin-development.html +4 -4
  104. package/website_v2/docs/project-structure.html +2 -2
  105. package/website_v2/docs/skill-development.html +2 -2
  106. package/website_v2/index.html +1 -1
  107. package/website_v2/styles/animations.css +7 -7
  108. package/.agent/agents/backend-dev.md +0 -102
  109. package/.agent/agents/data-analyst.md +0 -117
  110. package/.agent/agents/devops.md +0 -115
  111. package/.agent/agents/frontend-dev.md +0 -94
  112. package/.agent/agents/network-requester.md +0 -44
  113. package/.agent/agents/poster-designer.md +0 -52
  114. package/.agent/agents/product-manager.md +0 -85
  115. package/.agent/agents/qa-engineer.md +0 -100
  116. package/.agent/agents/security-engineer.md +0 -99
  117. package/.agent/agents/team-lead.md +0 -137
  118. package/.agent/agents/ui-designer.md +0 -116
  119. package/.agent/data/default.json +0 -58
  120. package/.agent/data/email/processed-emails.json +0 -1
  121. package/.agent/data/plugins-state.json +0 -199
  122. package/.agent/data/scheduler/tasks.json +0 -1
  123. package/.agent/data/web/web-config.json +0 -5
  124. package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
  125. package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
  126. package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
  127. package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
  128. package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
  129. package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
  130. package/.agent/data/weixin.json +0 -6
  131. package/.agent/mcp_config.json +0 -14
  132. package/.agent/memory/user/mof6gk94-kneeuh.md +0 -9
  133. package/.agent/package.json +0 -8
  134. package/.agent/plugins/marknative/README.md +0 -134
  135. package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
  136. package/.agent/plugins/marknative/fonts.zip +0 -0
  137. package/.agent/plugins/marknative/index.js +0 -256
  138. package/.agent/plugins/marknative/package.json +0 -12
  139. package/.agent/plugins/test-plugin.py +0 -99
  140. package/.agent/plugins.json +0 -14
  141. package/.agent/python-scripts/test_sample.py +0 -24
  142. package/.agent/sessions/cli_default.json +0 -247
  143. package/.agent/skills/agent-browser/SKILL.md +0 -311
  144. package/.agent/skills/agent-browser/TEST_PLAN.md +0 -200
  145. package/.agent/skills/sysinfo/SKILL.md +0 -38
  146. package/.agent/skills/sysinfo/system-info.sh +0 -130
  147. package/.agent/skills/workflow/SKILL.md +0 -324
  148. package/.agent/test-agent.js +0 -35
  149. package/.agent/weixin.json +0 -6
  150. package/.agent/workflows/email-digest.json +0 -50
  151. package/.agent/workflows/file-backup.json +0 -21
  152. package/.agent/workflows/get-ip-notify.json +0 -32
  153. package/.agent/workflows/news-aggregator.json +0 -93
  154. package/.agent/workflows/news-dashboard-v2.json +0 -94
  155. package/.agent/workflows/notification-batch.json +0 -32
  156. package/src/core/session-context.js +0 -346
  157. package/src/core/session-storage.js +0 -295
@@ -0,0 +1,253 @@
1
+ /**
2
+ * JSONL Session Storage
3
+ * Append-only JSONL storage with tree structure and leaf tracking
4
+ * Ported from pi's session/jsonl-storage.ts
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { generateEntryId, EntryTypes } = require('./session-entry');
10
+
11
+ const CURRENT_SESSION_VERSION = 3;
12
+
13
+ const SessionErrors = {
14
+ NOT_FOUND: 'not_found',
15
+ INVALID_SESSION: 'invalid_session',
16
+ INVALID_ENTRY: 'invalid_entry'
17
+ };
18
+
19
+ class SessionError extends Error {
20
+ constructor(code, message, cause) {
21
+ super(message, cause === undefined ? undefined : { cause });
22
+ this.name = 'SessionError';
23
+ this.code = code;
24
+ }
25
+ }
26
+
27
+ function isRecord(value) {
28
+ return typeof value === 'object' && value !== null;
29
+ }
30
+
31
+ function invalidSession(filePath, message, cause) {
32
+ return new SessionError(SessionErrors.INVALID_SESSION, `Invalid JSONL session file ${filePath}: ${message}`, cause);
33
+ }
34
+
35
+ function invalidEntry(filePath, lineNumber, message, cause) {
36
+ return new SessionError(
37
+ SessionErrors.INVALID_ENTRY,
38
+ `Invalid JSONL session file ${filePath}: line ${lineNumber} ${message}`,
39
+ cause
40
+ );
41
+ }
42
+
43
+ function parseHeaderLine(line, filePath) {
44
+ let parsed;
45
+ try {
46
+ parsed = JSON.parse(line);
47
+ } catch (error) {
48
+ throw invalidSession(filePath, 'first line is not a valid session header', error);
49
+ }
50
+ if (!isRecord(parsed)) throw invalidSession(filePath, 'first line is not a valid session header');
51
+ if (parsed.type !== 'session') throw invalidSession(filePath, 'first line is not a valid session header');
52
+ if (parsed.version !== 3) throw invalidSession(filePath, 'unsupported session version');
53
+ if (typeof parsed.id !== 'string' || !parsed.id) throw invalidSession(filePath, 'session header is missing id');
54
+ if (typeof parsed.timestamp !== 'string' || !parsed.timestamp) {
55
+ throw invalidSession(filePath, 'session header is missing timestamp');
56
+ }
57
+ if (typeof parsed.cwd !== 'string' || !parsed.cwd) throw invalidSession(filePath, 'session header is missing cwd');
58
+ if (parsed.parentSession !== undefined && typeof parsed.parentSession !== 'string') {
59
+ throw invalidSession(filePath, 'session header parentSession must be a string');
60
+ }
61
+ return {
62
+ type: 'session',
63
+ version: 3,
64
+ id: parsed.id,
65
+ timestamp: parsed.timestamp,
66
+ cwd: parsed.cwd,
67
+ parentSession: parsed.parentSession
68
+ };
69
+ }
70
+
71
+ function parseEntryLine(line, filePath, lineNumber) {
72
+ let parsed;
73
+ try {
74
+ parsed = JSON.parse(line);
75
+ } catch (error) {
76
+ throw invalidEntry(filePath, lineNumber, 'is not valid JSON', error);
77
+ }
78
+ if (!isRecord(parsed)) throw invalidEntry(filePath, lineNumber, 'is not valid session entry');
79
+ if (typeof parsed.type !== 'string') throw invalidEntry(filePath, lineNumber, 'is missing entry type');
80
+ if (typeof parsed.id !== 'string' || !parsed.id) throw invalidEntry(filePath, lineNumber, 'is missing entry id');
81
+ if (parsed.parentId !== null && typeof parsed.parentId !== 'string') {
82
+ throw invalidEntry(filePath, lineNumber, 'has invalid parentId');
83
+ }
84
+ if (typeof parsed.timestamp !== 'string' || !parsed.timestamp) {
85
+ throw invalidEntry(filePath, lineNumber, 'is missing timestamp');
86
+ }
87
+ if (parsed.type === EntryTypes.LEAF && parsed.targetId !== null && typeof parsed.targetId !== 'string') {
88
+ throw invalidEntry(filePath, lineNumber, 'has invalid targetId');
89
+ }
90
+ return parsed;
91
+ }
92
+
93
+ function leafIdAfterEntry(entry) {
94
+ return entry.type === EntryTypes.LEAF ? entry.targetId : entry.id;
95
+ }
96
+
97
+ function updateLabelCache(labelsById, entry) {
98
+ if (entry.type !== EntryTypes.LABEL) return;
99
+ const label = entry.label?.trim();
100
+ if (label) {
101
+ labelsById.set(entry.targetId, label);
102
+ } else {
103
+ labelsById.delete(entry.targetId);
104
+ }
105
+ }
106
+
107
+ function buildLabelsById(entries) {
108
+ const labelsById = new Map();
109
+ for (const entry of entries) {
110
+ updateLabelCache(labelsById, entry);
111
+ }
112
+ return labelsById;
113
+ }
114
+
115
+ class JsonlSessionStorage {
116
+ constructor(fsModule, filePath, header, entries, leafId) {
117
+ this.fs = fsModule;
118
+ this.filePath = filePath;
119
+ this.metadata = {
120
+ id: header.id,
121
+ createdAt: header.timestamp,
122
+ cwd: header.cwd,
123
+ path: filePath,
124
+ parentSessionPath: header.parentSession
125
+ };
126
+ this.entries = entries;
127
+ this.byId = new Map(entries.map(entry => [entry.id, entry]));
128
+ this.labelsById = buildLabelsById(entries);
129
+ this.currentLeafId = leafId;
130
+ }
131
+
132
+ static async open(filePath) {
133
+ const fsModule = fs;
134
+ const loaded = await this._loadJsonlStorage(fsModule, filePath);
135
+ return new JsonlSessionStorage(fsModule, filePath, loaded.header, loaded.entries, loaded.leafId);
136
+ }
137
+
138
+ static async _loadJsonlStorage(fsModule, filePath) {
139
+ const content = fsModule.readFileSync(filePath, 'utf-8');
140
+ const lines = content.split('\n').filter(line => line.trim());
141
+ if (lines.length === 0) {
142
+ throw invalidSession(filePath, 'missing session header');
143
+ }
144
+
145
+ const header = parseHeaderLine(lines[0], filePath);
146
+ const entries = [];
147
+ let leafId = null;
148
+ for (let i = 1; i < lines.length; i++) {
149
+ const entry = parseEntryLine(lines[i], filePath, i + 1);
150
+ entries.push(entry);
151
+ leafId = leafIdAfterEntry(entry);
152
+ }
153
+ return { header, entries, leafId };
154
+ }
155
+
156
+ static async create(filePath, options) {
157
+ const fsModule = fs;
158
+ const header = {
159
+ type: 'session',
160
+ version: CURRENT_SESSION_VERSION,
161
+ id: options.sessionId,
162
+ timestamp: new Date().toISOString(),
163
+ cwd: options.cwd,
164
+ parentSession: options.parentSessionPath
165
+ };
166
+
167
+ const dir = path.dirname(filePath);
168
+ if (!fsModule.existsSync(dir)) {
169
+ fsModule.mkdirSync(dir, { recursive: true });
170
+ }
171
+
172
+ fsModule.writeFileSync(filePath, JSON.stringify(header) + '\n');
173
+ return new JsonlSessionStorage(fsModule, filePath, header, [], null);
174
+ }
175
+
176
+ async getMetadata() {
177
+ return { ...this.metadata };
178
+ }
179
+
180
+ async getLeafId() {
181
+ if (this.currentLeafId !== null && !this.byId.has(this.currentLeafId)) {
182
+ throw new SessionError(SessionErrors.INVALID_SESSION, `Entry ${this.currentLeafId} not found`);
183
+ }
184
+ return this.currentLeafId;
185
+ }
186
+
187
+ async setLeafId(leafId) {
188
+ if (leafId !== null && !this.byId.has(leafId)) {
189
+ throw new SessionError(SessionErrors.NOT_FOUND, `Entry ${leafId} not found`);
190
+ }
191
+ const entry = {
192
+ type: EntryTypes.LEAF,
193
+ id: generateEntryId(this.byId),
194
+ parentId: this.currentLeafId,
195
+ timestamp: new Date().toISOString(),
196
+ targetId: leafId
197
+ };
198
+ fs.appendFileSync(this.filePath, JSON.stringify(entry) + '\n');
199
+ this.entries.push(entry);
200
+ this.byId.set(entry.id, entry);
201
+ this.currentLeafId = leafId;
202
+ }
203
+
204
+ async createEntryId() {
205
+ return generateEntryId(this.byId);
206
+ }
207
+
208
+ async appendEntry(entry) {
209
+ fs.appendFileSync(this.filePath, JSON.stringify(entry) + '\n');
210
+ this.entries.push(entry);
211
+ this.byId.set(entry.id, entry);
212
+ updateLabelCache(this.labelsById, entry);
213
+ this.currentLeafId = leafIdAfterEntry(entry);
214
+ }
215
+
216
+ async getEntry(id) {
217
+ return this.byId.get(id);
218
+ }
219
+
220
+ async findEntries(type) {
221
+ return this.entries.filter(entry => entry.type === type);
222
+ }
223
+
224
+ async getLabel(id) {
225
+ return this.labelsById.get(id);
226
+ }
227
+
228
+ async getPathToRoot(leafId) {
229
+ if (leafId === null) return [];
230
+ const path = [];
231
+ let current = this.byId.get(leafId);
232
+ if (!current) throw new SessionError(SessionErrors.NOT_FOUND, `Entry ${leafId} not found`);
233
+ while (current) {
234
+ path.unshift(current);
235
+ if (!current.parentId) break;
236
+ const parent = this.byId.get(current.parentId);
237
+ if (!parent) throw new SessionError(SessionErrors.INVALID_SESSION, `Entry ${current.parentId} not found`);
238
+ current = parent;
239
+ }
240
+ return path;
241
+ }
242
+
243
+ async getEntries() {
244
+ return [...this.entries];
245
+ }
246
+ }
247
+
248
+ module.exports = {
249
+ JsonlSessionStorage,
250
+ SessionError,
251
+ SessionErrors,
252
+ CURRENT_SESSION_VERSION
253
+ };
@@ -286,10 +286,11 @@ class Plugin {
286
286
  this._promptParts.push({ name, priority, provider });
287
287
 
288
288
  // 如果主 agent 已存在,直接注册
289
- if (this._framework._mainAgent) {
290
- this._framework._mainAgent.registerPromptPart(name, priority, provider);
289
+ const mainAgent = this._framework.getMainAgent();
290
+ if (mainAgent) {
291
+ mainAgent.registerPromptPart(name, priority, provider);
291
292
  // 同步到所有子Agent
292
- this._framework._syncPromptPartsToSubagents();
293
+ this._framework.syncPromptPartsToSubagents();
293
294
  return;
294
295
  }
295
296
 
@@ -307,14 +308,15 @@ class Plugin {
307
308
  if (this._deferredPromptParts) {
308
309
  for (const part of this._deferredPromptParts) {
309
310
  try {
310
- this._framework._mainAgent?.registerPromptPart(part.name, part.priority, part.provider);
311
+ const mainAgent = this._framework.getMainAgent();
312
+ mainAgent?.registerPromptPart(part.name, part.priority, part.provider);
311
313
  } catch (err) {
312
314
  log.error(` Failed to register prompt part ${part.name}:`, err.message);
313
315
  }
314
316
  }
315
317
  this._deferredPromptParts = null;
316
318
  // 同步到所有子Agent
317
- this._framework._syncPromptPartsToSubagents();
319
+ this._framework.syncPromptPartsToSubagents();
318
320
  }
319
321
  }, 100);
320
322
  });
@@ -22,7 +22,7 @@ class PluginManager {
22
22
  this.framework = framework;
23
23
  this._plugins = new Map();
24
24
  this._loading = false;
25
- this._stateFile = path.join(process.cwd(), '.agent', 'data', 'plugins-state.json');
25
+ this._stateFile = path.join(process.cwd(), '.foliko', 'data', 'plugins-state.json');
26
26
  this._knownPlugins = new Set(); // 已知插件列表(包括未加载的)
27
27
 
28
28
  // 创建子日志器
@@ -406,7 +406,7 @@ class PluginManager {
406
406
  entry.instance._started = false;
407
407
  }
408
408
 
409
- // 2. 扫描 .agent/plugins 目录,加载新插件
409
+ // 2. 扫描 .foliko/plugins 目录,加载新插件
410
410
  await this._discoverCustomPlugins();
411
411
 
412
412
  // 3. 启动所有未启动的插件
@@ -416,8 +416,8 @@ class PluginManager {
416
416
  /**
417
417
  * 解析插件路径
418
418
  * 支持两种结构:
419
- * 1. 文件夹结构: .agent/plugins/my-plugin/index.js
420
- * 2. 单文件结构: .agent/plugins/my-plugin.js
419
+ * 1. 文件夹结构: .foliko/plugins/my-plugin/index.js
420
+ * 2. 单文件结构: .foliko/plugins/my-plugin.js
421
421
  * @param {string} pluginsDir - 插件目录
422
422
  * @param {string} name - 插件名称
423
423
  * @returns {{path: string, type: 'folder'|'file'}|null} 插件路径和类型
@@ -442,7 +442,7 @@ class PluginManager {
442
442
  * @private
443
443
  */
444
444
  async _discoverCustomPlugins() {
445
- const pluginsDir = path.resolve(process.cwd(), '.agent', 'plugins');
445
+ const pluginsDir = path.resolve(process.cwd(), '.foliko', 'plugins');
446
446
  if (!fs.existsSync(pluginsDir)) {
447
447
  return;
448
448
  }
@@ -466,7 +466,7 @@ class PluginManager {
466
466
 
467
467
  // 添加模块路径到搜索路径(优先级从高到低)
468
468
  const modulePathsToAdd = [
469
- agentNodeModules, // .agent/node_modules(项目本地安装的包)
469
+ agentNodeModules, // .foliko/node_modules(项目本地安装的包)
470
470
  path.join(__dirname, '..', '..', 'node_modules'), // 全局安装的包
471
471
  ];
472
472
  for (const mp of modulePathsToAdd) {
@@ -521,7 +521,7 @@ class PluginManager {
521
521
  }
522
522
 
523
523
  this._log.info(`Loading new plugin: ${pluginName} (${type})`);
524
- // .agent/plugins 目录下的插件默认强制启用,不受 state 文件影响
524
+ // .foliko/plugins 目录下的插件默认强制启用,不受 state 文件影响
525
525
  await this.load(plugin, { forceEnabled: true });
526
526
  } catch (err) {
527
527
  this._log.error(`Failed to load plugin ${pluginName}:`, err.message);
@@ -861,14 +861,19 @@ class PluginManager {
861
861
  return result;
862
862
  }
863
863
 
864
- // 支持对象形式: { name, version, install, start, ... }
864
+ // 支持 exports 对象形式: exports.id, exports.name, exports.version, ...
865
+ // 这种格式没有 install/start 等方法,只有数据属性
865
866
  class AnonymousPlugin extends Plugin {
866
867
  constructor() {
867
868
  super();
868
- this.name = obj.name;
869
+ this.id = obj.id;
870
+ this.name = obj.name || obj.id;
869
871
  this.version = obj.version || '1.0.0';
870
872
  this.description = obj.description || '';
871
- this.priority = obj.priority || 100;
873
+
874
+ // exports 形式的插件可以没有 priority/install 等
875
+ this.priority = obj.priority !== undefined ? obj.priority : 100;
876
+ this.system = obj.system || false;
872
877
 
873
878
  // 如果提供了 install/start/reload/uninstall 方法,绑定它们
874
879
  if (typeof obj.install === 'function') {
@@ -0,0 +1,159 @@
1
+ /**
2
+ * ProviderRegistry - Dynamic LLM Provider Registration
3
+ * 允许插件动态注册 LLM Provider
4
+ */
5
+
6
+ const { createOpenAICompatible } = require('ai');
7
+ const { logger } = require('../utils/logger');
8
+
9
+ class ProviderRegistry {
10
+ constructor() {
11
+ this._providers = new Map();
12
+ this._models = new Map();
13
+ }
14
+
15
+ /**
16
+ * 注册 Provider
17
+ * @param {string} name - Provider 名称 (如 'openai', 'deepseek')
18
+ * @param {Object} config - Provider 配置
19
+ * @param {string} config.baseURL - API base URL
20
+ * @param {string} [config.apiKey] - API key
21
+ * @param {string} [config.name] - 显示名称
22
+ * @param {string} [config.defaultModel] - 默认模型
23
+ * @param {Object} [config.headers] - 自定义 headers
24
+ */
25
+ registerProvider(name, config) {
26
+ this._providers.set(name, {
27
+ name: config.name || name,
28
+ baseURL: config.baseURL,
29
+ apiKey: config.apiKey || '',
30
+ defaultModel: config.defaultModel || '',
31
+ headers: config.headers || {},
32
+ });
33
+
34
+ if (config.defaultModel) {
35
+ this.registerModel(config.defaultModel, name, {
36
+ label: config.defaultModel,
37
+ description: `Default model for ${name}`,
38
+ });
39
+ }
40
+
41
+ logger.debug(`[ProviderRegistry] Registered provider: ${name}`);
42
+ return this;
43
+ }
44
+
45
+ /**
46
+ * 注册模型
47
+ * @param {string} modelId - 模型 ID
48
+ * @param {string} provider - Provider 名称
49
+ * @param {Object} [options] - 模型选项
50
+ */
51
+ registerModel(modelId, provider, options = {}) {
52
+ this._models.set(modelId, { provider, ...options });
53
+ return this;
54
+ }
55
+
56
+ /**
57
+ * 获取 Provider 配置
58
+ */
59
+ getProvider(name) {
60
+ return this._providers.get(name);
61
+ }
62
+
63
+ /**
64
+ * 获取模型配置
65
+ */
66
+ getModel(modelId) {
67
+ return this._models.get(modelId);
68
+ }
69
+
70
+ /**
71
+ * 获取所有已注册的 Provider 名称
72
+ */
73
+ getProviderNames() {
74
+ return Array.from(this._providers.keys());
75
+ }
76
+
77
+ /**
78
+ * 检查 Provider 是否已注册
79
+ */
80
+ hasProvider(name) {
81
+ return this._providers.has(name);
82
+ }
83
+
84
+ /**
85
+ * 创建 AI 客户端
86
+ * @param {string} providerName - Provider 名称
87
+ * @param {string} model - 模型 ID
88
+ * @param {string} [apiKey] - API key (可选,会使用注册时提供的)
89
+ * @param {string} [baseURL] - Base URL (可选,会使用注册时提供的)
90
+ */
91
+ createClient(providerName, model, apiKey, baseURL) {
92
+ const provider = this._providers.get(providerName);
93
+ if (!provider) {
94
+ throw new Error(`Provider '${providerName}' not registered. Use framework.registerProvider() first.`);
95
+ }
96
+
97
+ const finalApiKey = apiKey || provider.apiKey;
98
+ const finalBaseURL = baseURL || provider.baseURL;
99
+ const finalModel = model || provider.defaultModel;
100
+
101
+ if (!finalApiKey) {
102
+ throw new Error(`No API key provided for provider '${providerName}'`);
103
+ }
104
+
105
+ const client = createOpenAICompatible({
106
+ baseURL: finalBaseURL,
107
+ apiKey: finalApiKey,
108
+ });
109
+
110
+ return client(finalModel);
111
+ }
112
+
113
+ /**
114
+ * 移除 Provider
115
+ */
116
+ unregisterProvider(name) {
117
+ this._providers.delete(name);
118
+ // 同时移除该 provider 的所有模型
119
+ for (const [modelId, modelConfig] of this._models) {
120
+ if (modelConfig.provider === name) {
121
+ this._models.delete(modelId);
122
+ }
123
+ }
124
+ return this;
125
+ }
126
+ }
127
+
128
+ // 单例
129
+ const _registry = new ProviderRegistry();
130
+
131
+ /**
132
+ * 获取 Provider Registry 实例
133
+ */
134
+ function getProviderRegistry() {
135
+ return _registry;
136
+ }
137
+
138
+ /**
139
+ * 注册默认的 Providers
140
+ */
141
+ function registerDefaultProviders() {
142
+ const { DEFAULT_PROVIDERS } = require('./provider');
143
+
144
+ for (const [key, config] of Object.entries(DEFAULT_PROVIDERS)) {
145
+ _registry.registerProvider(key, {
146
+ name: config.name,
147
+ baseURL: config.baseURL,
148
+ defaultModel: config.defaultModel,
149
+ });
150
+ }
151
+ }
152
+
153
+ // 初始化时注册默认 providers
154
+ registerDefaultProviders();
155
+
156
+ module.exports = {
157
+ ProviderRegistry,
158
+ getProviderRegistry,
159
+ };
@@ -151,4 +151,6 @@ module.exports = {
151
151
  getAvailableProviders,
152
152
  DEFAULT_PROVIDERS,
153
153
  isThinkingModel,
154
+ getProviderRegistry: () => require('./provider-registry').getProviderRegistry(),
155
+ registerProvider: (name, config) => require('./provider-registry').getProviderRegistry().registerProvider(name, config),
154
156
  };