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.
- package/.claude/settings.local.json +19 -10
- package/.dockerignore +45 -45
- package/.env.example +56 -56
- package/CLAUDE.md +2 -2
- package/README.md +13 -13
- package/SPEC.md +3 -3
- package/cli/src/commands/chat.js +2 -20
- package/cli/src/commands/list.js +7 -6
- package/cli/src/commands/plugin.js +3 -2
- package/cli/src/daemon.js +2 -2
- package/cli/src/ui/chat-ui-old.js +15 -4
- package/cli/src/ui/chat-ui.js +236 -203
- package/cli/src/ui/footer-bar.js +20 -46
- package/cli/src/ui/message-bubble.js +24 -2
- package/cli/src/ui/status-bar.js +177 -0
- package/cli/src/utils/config.js +29 -0
- package/cli/src/utils/plugin-config.js +1 -1
- package/docker-compose.yml +33 -33
- package/docs/features.md +120 -120
- package/docs/quick-reference.md +160 -160
- package/docs/user-manual.md +1391 -1391
- package/examples/ambient-example.js +2 -2
- package/examples/bootstrap.js +3 -3
- package/examples/test-chat.js +1 -1
- package/examples/test-reload.js +1 -1
- package/examples/test-telegram.js +1 -1
- package/examples/test-tg-bot.js +1 -1
- package/examples/test-tg-simple.js +2 -2
- package/examples/test-tg.js +1 -1
- package/examples/test-think.js +1 -1
- package/examples/test-weixin-feishu.js +3 -3
- package/package.json +3 -1
- package/plugins/ambient-agent/index.js +1 -1
- package/plugins/audit-plugin.js +84 -29
- package/plugins/coordinator-plugin.js +14 -12
- package/plugins/data-splitter-plugin.js +323 -0
- package/plugins/default-plugins.js +23 -12
- package/plugins/email/index.js +1 -1
- package/plugins/extension-executor-plugin.js +87 -9
- package/plugins/feishu-plugin.js +118 -16
- package/plugins/file-system-plugin.js +68 -50
- package/plugins/gate-trading.js +10 -10
- package/plugins/install-plugin.js +7 -7
- package/plugins/memory-plugin.js +9 -12
- package/plugins/plugin-manager-plugin.js +12 -14
- package/plugins/python-executor-plugin.js +1 -1
- package/plugins/python-plugin-loader.js +1 -1
- package/plugins/qq-plugin.js +151 -24
- package/plugins/rules-plugin.js +8 -8
- package/plugins/scheduler-plugin.js +24 -20
- package/plugins/session-plugin.js +313 -397
- package/plugins/storage-plugin.js +235 -175
- package/plugins/subagent-plugin.js +17 -13
- package/plugins/telegram-plugin.js +116 -17
- package/plugins/think-plugin.js +64 -60
- package/plugins/tools-plugin.js +8 -8
- package/plugins/web-plugin.js +2 -2
- package/plugins/weixin-plugin.js +107 -24
- package/skills/find-skills/AGENTS.md +2 -2
- package/skills/find-skills/SKILL.md +133 -133
- package/skills/foliko-dev/AGENTS.md +236 -236
- package/skills/foliko-dev/SKILL.md +19 -19
- package/skills/mcp-usage/SKILL.md +200 -200
- package/skills/plugin-guide/SKILL.md +4 -4
- package/skills/python-plugin-dev/SKILL.md +5 -5
- package/skills/skill-guide/SKILL.md +104 -6
- package/skills/subagent-guide/SKILL.md +237 -237
- package/skills/workflow-guide/SKILL.md +646 -646
- package/src/capabilities/skill-manager.js +124 -17
- package/src/capabilities/workflow-engine.js +3 -3
- package/src/core/agent-chat.js +72 -26
- package/src/core/agent.js +17 -27
- package/src/core/branch-summary-auto.js +206 -0
- package/src/core/chat-session.js +45 -169
- package/src/core/command-registry.js +200 -0
- package/src/core/constants.js +198 -0
- package/src/core/context-compressor.js +702 -326
- package/src/core/context-manager.js +0 -1
- package/src/core/enhanced-context-compressor.js +210 -0
- package/src/core/framework.js +260 -84
- package/src/core/jsonl-storage.js +253 -0
- package/src/core/plugin-base.js +7 -5
- package/src/core/plugin-manager.js +15 -10
- package/src/core/provider-registry.js +159 -0
- package/src/core/provider.js +2 -0
- package/src/core/session-entry.js +225 -0
- package/src/core/session-manager.js +701 -0
- package/src/core/storage-manager.js +494 -0
- package/src/core/sub-agent-config.js +1 -1
- package/src/core/subagent.js +16 -135
- package/src/core/token-counter.js +177 -58
- package/src/core/tool-executor.js +2 -70
- package/src/core/ui-extension-context.js +174 -0
- package/src/executors/mcp-executor.js +27 -16
- package/src/utils/chat-queue.js +11 -22
- package/src/utils/data-splitter.js +345 -0
- package/src/utils/logger.js +152 -180
- package/src/utils/message-validator.js +283 -0
- package/src/utils/plugin-helpers.js +2 -2
- package/src/utils/retry.js +168 -22
- package/website_v2/docs/api.html +1 -1
- package/website_v2/docs/configuration.html +2 -2
- package/website_v2/docs/plugin-development.html +4 -4
- package/website_v2/docs/project-structure.html +2 -2
- package/website_v2/docs/skill-development.html +2 -2
- package/website_v2/index.html +1 -1
- package/website_v2/styles/animations.css +7 -7
- package/.agent/agents/backend-dev.md +0 -102
- package/.agent/agents/data-analyst.md +0 -117
- package/.agent/agents/devops.md +0 -115
- package/.agent/agents/frontend-dev.md +0 -94
- package/.agent/agents/network-requester.md +0 -44
- package/.agent/agents/poster-designer.md +0 -52
- package/.agent/agents/product-manager.md +0 -85
- package/.agent/agents/qa-engineer.md +0 -100
- package/.agent/agents/security-engineer.md +0 -99
- package/.agent/agents/team-lead.md +0 -137
- package/.agent/agents/ui-designer.md +0 -116
- package/.agent/data/default.json +0 -58
- package/.agent/data/email/processed-emails.json +0 -1
- package/.agent/data/plugins-state.json +0 -199
- package/.agent/data/scheduler/tasks.json +0 -1
- package/.agent/data/web/web-config.json +0 -5
- package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
- package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
- package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
- package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
- package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
- package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
- package/.agent/data/weixin.json +0 -6
- package/.agent/mcp_config.json +0 -14
- package/.agent/memory/user/mof6gk94-kneeuh.md +0 -9
- package/.agent/package.json +0 -8
- package/.agent/plugins/marknative/README.md +0 -134
- package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
- package/.agent/plugins/marknative/fonts.zip +0 -0
- package/.agent/plugins/marknative/index.js +0 -256
- package/.agent/plugins/marknative/package.json +0 -12
- package/.agent/plugins/test-plugin.py +0 -99
- package/.agent/plugins.json +0 -14
- package/.agent/python-scripts/test_sample.py +0 -24
- package/.agent/sessions/cli_default.json +0 -247
- package/.agent/skills/agent-browser/SKILL.md +0 -311
- package/.agent/skills/agent-browser/TEST_PLAN.md +0 -200
- package/.agent/skills/sysinfo/SKILL.md +0 -38
- package/.agent/skills/sysinfo/system-info.sh +0 -130
- package/.agent/skills/workflow/SKILL.md +0 -324
- package/.agent/test-agent.js +0 -35
- package/.agent/weixin.json +0 -6
- package/.agent/workflows/email-digest.json +0 -50
- package/.agent/workflows/file-backup.json +0 -21
- package/.agent/workflows/get-ip-notify.json +0 -32
- package/.agent/workflows/news-aggregator.json +0 -93
- package/.agent/workflows/news-dashboard-v2.json +0 -94
- package/.agent/workflows/notification-batch.json +0 -32
- package/src/core/session-context.js +0 -346
- 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
|
+
};
|
package/src/core/plugin-base.js
CHANGED
|
@@ -286,10 +286,11 @@ class Plugin {
|
|
|
286
286
|
this._promptParts.push({ name, priority, provider });
|
|
287
287
|
|
|
288
288
|
// 如果主 agent 已存在,直接注册
|
|
289
|
-
|
|
290
|
-
|
|
289
|
+
const mainAgent = this._framework.getMainAgent();
|
|
290
|
+
if (mainAgent) {
|
|
291
|
+
mainAgent.registerPromptPart(name, priority, provider);
|
|
291
292
|
// 同步到所有子Agent
|
|
292
|
-
this._framework.
|
|
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.
|
|
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.
|
|
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(), '.
|
|
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. 扫描 .
|
|
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. 文件夹结构: .
|
|
420
|
-
* 2. 单文件结构: .
|
|
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(), '.
|
|
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, // .
|
|
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
|
-
// .
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
|
|
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
|
+
};
|
package/src/core/provider.js
CHANGED
|
@@ -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
|
};
|