foliko 2.0.30 → 2.0.31

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.
@@ -0,0 +1,850 @@
1
+ # Plugin 开发指南
2
+
3
+ > 本指南详细说明如何在 Foliko 框架中开发插件。
4
+
5
+ ---
6
+
7
+ ## 目录
8
+
9
+ - [1. 概述](#1-概述)
10
+ - [2. 插件存放位置](#2-插件存放位置)
11
+ - [3. 用户插件开发](#3-用户插件开发)
12
+ - [4. 内置插件开发](#4-内置插件开发)
13
+ - [5. 插件属性](#5-插件属性)
14
+ - [6. 生命周期](#6-生命周期)
15
+ - [7. 工具注册](#7-工具注册)
16
+ - [8. 扩展工具](#8-扩展工具)
17
+ - [9. 子 Agent](#9-子-agent)
18
+ - [10. 工作流](#10-工作流)
19
+ - [11. Prompt Parts](#11-prompt-parts)
20
+ - [12. Skill 管理](#12-skill-管理)
21
+ - [13. 依赖管理](#13-依赖管理)
22
+ - [14. 系统插件访问](#14-系统插件访问)
23
+ - [15. 最佳实践](#15-最佳实践)
24
+
25
+ ---
26
+
27
+ ## 1. 概述
28
+
29
+ Foliko 是一个基于插件的 Agent 框架,核心保持简单,通过插件扩展功能。
30
+
31
+ ### 插件 vs 技能
32
+
33
+ | 类型 | 存储位置 | 说明 |
34
+ |------|----------|------|
35
+ | **Plugin(插件)** | `.foliko/plugins/` | 扩展功能模块 |
36
+ | **Skill(技能)** | `.foliko/skills/` | 为 Agent 提供指导的知识包 |
37
+
38
+ ---
39
+
40
+ ## 2. 插件存放位置
41
+
42
+ ### 用户插件 `.foliko/plugins/`
43
+
44
+ 用户自定义插件放在项目根目录的 `.foliko/plugins/` 下,**自动加载**,无需手动注册。
45
+
46
+ **推荐使用文件夹结构**(适合复杂插件):
47
+
48
+ ```
49
+ 项目目录/
50
+ └── .foliko/
51
+ └── plugins/
52
+ ├── my-plugin/ ✅ 文件夹结构(推荐)
53
+ │ ├── package.json # 可选,main 字段指定入口
54
+ │ ├── index.js # 默认入口
55
+ │ └── node_modules/ # 可选,插件私有依赖
56
+ ├── another-plugin/ ✅ 另一个文件夹插件
57
+ │ └── index.js
58
+ └── legacy.js ✅ 单文件仍支持
59
+ ```
60
+
61
+ ### 内置插件 `plugins/`
62
+
63
+ 框架内置插件位于仓库根目录的 `plugins/` 目录,需要 `require` Plugin 基类。
64
+
65
+ ---
66
+
67
+ ## 3. 用户插件开发
68
+
69
+ ### 文件夹结构(推荐)
70
+
71
+ ```
72
+ .foliko/plugins/my-plugin/
73
+ ├── package.json # 可选
74
+ └── index.js # 入口
75
+ ```
76
+
77
+ ```javascript
78
+ // .foliko/plugins/my-plugin/index.js
79
+ const { z } = require('zod');
80
+
81
+ module.exports = function (Plugin) {
82
+ return class MyPlugin extends Plugin {
83
+ constructor(config = {}) {
84
+ super();
85
+ this.name = 'my-plugin';
86
+ this.version = '1.0.0';
87
+ this.description = '我的工具插件';
88
+ this.priority = 10;
89
+ }
90
+
91
+ // 方式1:声明式工具定义
92
+ tools = {
93
+ my_tool: {
94
+ description: '我的工具',
95
+ inputSchema: z.object({
96
+ param: z.string().describe('参数描述'),
97
+ }),
98
+ execute: async (args) => {
99
+ return { success: true, result: args.param };
100
+ },
101
+ },
102
+ };
103
+
104
+ // 方式2:生命周期方法
105
+ onStart(framework) {
106
+ // 注册额外工具
107
+ this.tool.register({
108
+ name: 'another_tool',
109
+ description: '另一个工具',
110
+ inputSchema: z.object({}),
111
+ execute: async (args) => ({ success: true }),
112
+ });
113
+ }
114
+
115
+ uninstall(framework) {
116
+ // 清理资源
117
+ }
118
+ };
119
+ };
120
+ ```
121
+
122
+ ### 单文件结构(仍支持)
123
+
124
+ ```javascript
125
+ // .foliko/plugins/my-plugin.js
126
+ const { z } = require('zod');
127
+
128
+ module.exports = function (Plugin) {
129
+ return class MyPlugin extends Plugin {
130
+ constructor(config = {}) {
131
+ super();
132
+ this.name = 'my-plugin';
133
+ this.version = '1.0.0';
134
+ this.description = '我的工具插件';
135
+ this.priority = 10;
136
+ }
137
+
138
+ tools = {
139
+ my_tool: {
140
+ description: '我的工具',
141
+ inputSchema: z.object({
142
+ param: z.string().describe('参数描述'),
143
+ }),
144
+ execute: async (args) => {
145
+ return { success: true, result: args.param };
146
+ },
147
+ },
148
+ };
149
+
150
+ onStart(framework) {}
151
+ uninstall(framework) {}
152
+ };
153
+ };
154
+ ```
155
+
156
+ **注意**:如果文件夹和同名 `.js` 文件同时存在,**文件夹优先**。
157
+
158
+ ---
159
+
160
+ ## 4. 内置插件开发
161
+
162
+ 内置插件需要显式引入 Plugin 基类:
163
+
164
+ ```javascript
165
+ // plugins/my-plugin.js
166
+ const { Plugin } = require('../src/plugin/base');
167
+ const { z } = require('zod');
168
+
169
+ class MyPlugin extends Plugin {
170
+ constructor(config = {}) {
171
+ super();
172
+ this.name = 'my-plugin';
173
+ this.version = '1.0.0';
174
+ this.description = '我的工具插件';
175
+ this.priority = 10;
176
+ }
177
+
178
+ tools = {
179
+ my_tool: {
180
+ description: '我的工具',
181
+ inputSchema: z.object({
182
+ param: z.string().describe('参数描述'),
183
+ }),
184
+ execute: async (args) => {
185
+ return { success: true, result: args.param };
186
+ },
187
+ },
188
+ };
189
+
190
+ onStart(framework) {
191
+ // 注册额外工具
192
+ this.tool.register({
193
+ name: 'another_tool',
194
+ description: '另一个工具',
195
+ inputSchema: z.object({}),
196
+ execute: async (args) => ({ success: true }),
197
+ });
198
+ }
199
+
200
+ uninstall(framework) {}
201
+ }
202
+
203
+ module.exports = MyPlugin;
204
+ ```
205
+
206
+ ---
207
+
208
+ ## 5. 插件属性
209
+
210
+ | 属性 | 类型 | 必须 | 说明 |
211
+ |------|------|------|------|
212
+ | `name` | string | ✅ | 唯一名称 |
213
+ | `version` | string | ❌ | 版本号,默认 '1.0.0' |
214
+ | `description` | string | ❌ | 插件描述 |
215
+ | `priority` | number | ❌ | 加载优先级,默认 100 |
216
+ | `tools` | object | ❌ | 声明式工具定义 |
217
+ | `agents` | array | ❌ | 声明式子 Agent 定义 |
218
+ | `prompts` | array | ❌ | 声明式 prompt parts |
219
+ | `enabled` | boolean | ❌ | 是否启用,默认 true |
220
+ | `system` | boolean | ❌ | 是否系统插件,默认 false |
221
+
222
+ ---
223
+
224
+ ## 6. 生命周期
225
+
226
+ | 方法 | 调用时机 | 必须实现 | 说明 |
227
+ |------|----------|----------|------|
228
+ | `install(framework)` | 插件安装时 | ✅ | 初始化,获取 framework |
229
+ | `onStart(framework)` | start/enable/reload 时 | 推荐 | 注册工具、扩展、子代理 |
230
+ | `onStop()` | disable 时 | 可选 | 清理资源 |
231
+ | `reload(framework)` | 热重载时 | 可选 | prompts 自动清理/注册 |
232
+ | `uninstall(framework)` | 卸载时 | 推荐 | 清理资源 |
233
+
234
+ ### 生命周期流程
235
+
236
+ ```
237
+ install() → onStart() → (reload()) → onStop() → uninstall()
238
+ ```
239
+
240
+ ### onStart / onStop(推荐方式)
241
+
242
+ **推荐使用 `onStart` 注册工具和扩展**,基类自动处理生命周期:
243
+
244
+ ```javascript
245
+ class MyPlugin extends Plugin {
246
+ name = 'my-plugin';
247
+
248
+ onStart(framework) {
249
+ // 注册 AI SDK 工具(直接调用)
250
+ this.tool.register({
251
+ name: 'hello',
252
+ description: '打招呼',
253
+ inputSchema: framework.z.object({
254
+ name: framework.z.string().describe('姓名')
255
+ }),
256
+ execute: async (args) => ({ message: `Hello, ${args.name}!` }),
257
+ });
258
+
259
+ // 注册扩展工具(通过 ext_call 调用)
260
+ this.extension.register({
261
+ name: 'greet_ext',
262
+ description: '打招呼扩展',
263
+ inputSchema: framework.z.object({
264
+ name: framework.z.string()
265
+ }),
266
+ execute: async (args) => ({ message: `Hi, ${args.name}!` }),
267
+ });
268
+ }
269
+
270
+ onStop() {
271
+ // 清理资源(如果需要)
272
+ }
273
+ }
274
+ ```
275
+
276
+ ### 生命周期自动管理
277
+
278
+ | 操作 | 效果 |
279
+ |------|------|
280
+ | `disable` | 自动移除工具/扩展(保留注册记录) |
281
+ | `enable` | 自动重新注册工具/扩展 |
282
+ | `reload` | 移除后重新注册 |
283
+ | `uninstall` | 移除并清空注册记录 |
284
+
285
+ ---
286
+
287
+ ## 7. 工具注册
288
+
289
+ ### 方式1:`this.tools = {}`(声明式)
290
+
291
+ 插件将工具定义在 `this.tools = {}` 中,ExtensionExecutor 会自动扫描并注册:
292
+
293
+ ```javascript
294
+ class MyPlugin extends Plugin {
295
+ name = 'my-plugin';
296
+
297
+ tools = {
298
+ my_tool: {
299
+ description: '我的工具',
300
+ inputSchema: z.object({
301
+ param: z.string().describe('参数描述'),
302
+ }),
303
+ execute: async (args) => {
304
+ return { success: true, result: args.param };
305
+ },
306
+ },
307
+ };
308
+ }
309
+ ```
310
+
311
+ ### 方式2:`this.tool.register()`(推荐)
312
+
313
+ 在 `onStart` 中使用,支持完整的生命周期管理:
314
+
315
+ ```javascript
316
+ onStart(framework) {
317
+ this.tool.register({
318
+ name: 'my_tool',
319
+ description: '我的工具',
320
+ inputSchema: framework.z.object({
321
+ param: framework.z.string().describe('参数描述')
322
+ }),
323
+ execute: async (args) => {
324
+ return { success: true, result: args.param };
325
+ }
326
+ });
327
+ }
328
+ ```
329
+
330
+ ### 方式3:`this.registerTool()`(旧方式)
331
+
332
+ ```javascript
333
+ install(framework) {
334
+ this.registerTool('my_tool', {
335
+ description: '我的工具',
336
+ inputSchema: framework.z.object({
337
+ param: framework.z.string().describe('参数描述')
338
+ }),
339
+ execute: async (args) => ({ success: true })
340
+ });
341
+ return this;
342
+ }
343
+ ```
344
+
345
+ ### 工具操作
346
+
347
+ ```javascript
348
+ // 注册
349
+ this.tool.register({ name: 'xxx', ... });
350
+
351
+ // 移除
352
+ this.tool.remove('xxx');
353
+
354
+ // 执行
355
+ const result = await this.tool.execute('any-tool', { arg: 'value' });
356
+ ```
357
+
358
+ ---
359
+
360
+ ## 8. 扩展工具
361
+
362
+ 扩展工具通过 `ext_call` 间接调用,适合需要额外封装的工具。
363
+
364
+ ### 注册扩展工具
365
+
366
+ ```javascript
367
+ onStart(framework) {
368
+ this.extension.register({
369
+ name: 'my_ext_tool',
370
+ description: '扩展工具',
371
+ inputSchema: framework.z.object({
372
+ param: framework.z.string()
373
+ }),
374
+ execute: async (args) => ({ success: true, data: args.param })
375
+ });
376
+ }
377
+ ```
378
+
379
+ ### 调用扩展工具
380
+
381
+ ```javascript
382
+ // 通过 ext_call 调用
383
+ const result = await ext_call({
384
+ plugin: 'my-plugin',
385
+ tool: 'my_ext_tool',
386
+ args: { param: 'value' }
387
+ });
388
+ ```
389
+
390
+ ### 扩展工具操作
391
+
392
+ ```javascript
393
+ // 注册
394
+ this.extension.register({ name: 'xxx', ... });
395
+
396
+ // 移除
397
+ this.extension.remove('xxx');
398
+
399
+ // 执行(当前插件)
400
+ const r1 = await this.extension.execute('xxx', { arg: 'value' });
401
+
402
+ // 执行(指定插件)
403
+ const r2 = await this.extension.execute('plugin-name', 'xxx', { arg: 'value' });
404
+ ```
405
+
406
+ ---
407
+
408
+ ## 9. 子 Agent
409
+
410
+ ### 配置式注册
411
+
412
+ ```javascript
413
+ class MyPlugin extends Plugin {
414
+ name = 'my-plugin';
415
+
416
+ agents = [
417
+ {
418
+ name: 'code-agent',
419
+ role: '代码专家',
420
+ description: '处理代码开发任务',
421
+ tools: {
422
+ compile: {
423
+ description: '编译代码',
424
+ inputSchema: z.object({
425
+ language: z.string(),
426
+ code: z.string(),
427
+ }),
428
+ execute: async (args) => ({ success: true }),
429
+ },
430
+ },
431
+ parentTools: ['read_file', 'write_file'],
432
+ },
433
+ ];
434
+ }
435
+ ```
436
+
437
+ ### 手动注册
438
+
439
+ ```javascript
440
+ onStart(framework) {
441
+ this.agent.register({
442
+ name: 'code-agent',
443
+ role: '代码专家',
444
+ description: '处理代码开发任务',
445
+ tools: {
446
+ compile: {
447
+ description: '编译代码',
448
+ inputSchema: framework.z.object({
449
+ language: framework.z.string(),
450
+ code: framework.z.string(),
451
+ }),
452
+ execute: async (args) => ({ success: true }),
453
+ },
454
+ },
455
+ parentTools: ['read_file', 'write_file'],
456
+ });
457
+ }
458
+ ```
459
+
460
+ ### 子 Agent 配置说明
461
+
462
+ | 字段 | 类型 | 说明 |
463
+ |------|------|------|
464
+ | `name` | string | 子 Agent 名称(唯一标识) |
465
+ | `role` | string | 角色描述 |
466
+ | `description` | string | 详细描述 |
467
+ | `tools` | object | 自定义工具,只属于此子 Agent |
468
+ | `parentTools` | array | 从父 Agent 继承的工具名称列表 |
469
+
470
+ ### 子 Agent 操作
471
+
472
+ ```javascript
473
+ // 获取
474
+ const agent = this.agent.get('my-agent');
475
+
476
+ // 移除
477
+ this.agent.remove('my-agent');
478
+ ```
479
+
480
+ ---
481
+
482
+ ## 10. 工作流
483
+
484
+ ### 注册工作流
485
+
486
+ ```javascript
487
+ onStart(framework) {
488
+ this.workflow.register('my-workflow', {
489
+ name: 'my-workflow',
490
+ description: '我的工作流',
491
+ inputSchema: framework.z.object({
492
+ msg: framework.z.string()
493
+ }),
494
+ steps: [
495
+ { type: 'tool', tool: 'hello', args: { name: '{{inputs.msg}}' } },
496
+ { type: 'action', code: 'return { result: inputs.msg + "!" }' }
497
+ ]
498
+ });
499
+ }
500
+ ```
501
+
502
+ ### 工作流操作
503
+
504
+ ```javascript
505
+ // 执行
506
+ const result = await this.workflow.execute('my-workflow', { msg: 'hello' });
507
+
508
+ // 移除
509
+ this.workflow.remove('my-workflow');
510
+ ```
511
+
512
+ ---
513
+
514
+ ## 11. Prompt Parts
515
+
516
+ ### 配置式注册
517
+
518
+ ```javascript
519
+ class MyPlugin extends Plugin {
520
+ name = 'my-plugin';
521
+
522
+ prompts = [
523
+ {
524
+ name: 'my-prompt',
525
+ scope: 'global', // 'global' | 'main' | agentId
526
+ priority: 50, // 数字越小越靠前
527
+ description: '我的提示词',
528
+ provider() {
529
+ return '这是动态生成的提示词内容';
530
+ },
531
+ },
532
+ ];
533
+ }
534
+ ```
535
+
536
+ ### 手动注册
537
+
538
+ ```javascript
539
+ onStart(framework) {
540
+ this.prompt.register('my-prompt', () => 'Prompt content', {
541
+ scope: 'global',
542
+ priority: 50
543
+ });
544
+ }
545
+ ```
546
+
547
+ ### Prompt 操作
548
+
549
+ ```javascript
550
+ // 移除
551
+ this.prompt.remove('my-prompt');
552
+ ```
553
+
554
+ ---
555
+
556
+ ## 12. Skill 管理
557
+
558
+ ### 注册 Skill
559
+
560
+ ```javascript
561
+ onStart(framework) {
562
+ this.skill.register(
563
+ 'my-skill',
564
+ '# 我的技能\n\n这是一个技能说明',
565
+ {
566
+ description: '技能描述',
567
+ tags: ['test'],
568
+ },
569
+ {
570
+ commands: [
571
+ {
572
+ name: 'do',
573
+ description: '执行操作',
574
+ options: [
575
+ { flags: '-x <x>', description: '参数', required: true },
576
+ ],
577
+ execute: async (args) => ({ x: args.x }),
578
+ },
579
+ ],
580
+ }
581
+ );
582
+ }
583
+ ```
584
+
585
+ ### Skill 操作
586
+
587
+ ```javascript
588
+ // 检查是否存在
589
+ this.skill.has('my-skill');
590
+
591
+ // 获取详情
592
+ this.skill.get('my-skill');
593
+ this.skill.details('my-skill');
594
+
595
+ // 列出
596
+ this.skill.list();
597
+ this.skill.listOwned(); // 本插件注册的
598
+
599
+ // 执行
600
+ await this.skill.execute('my-skill:do', { x: 1 });
601
+
602
+ // 移除
603
+ this.skill.remove('my-skill');
604
+ ```
605
+
606
+ ---
607
+
608
+ ## 13. 依赖管理
609
+
610
+ ### 依赖安装位置
611
+
612
+ | 插件类型 | 安装位置 | 说明 |
613
+ |----------|----------|------|
614
+ | **文件夹插件** | `.foliko/plugins/插件名/node_modules/` | 插件自包含,可独立迁移 |
615
+ | **单文件插件** | `.foliko/node_modules/` | 共享依赖 |
616
+
617
+ ### 安装工具
618
+
619
+ ```javascript
620
+ // 安装到默认位置 .foliko/node_modules
621
+ install({ package: 'zod' });
622
+
623
+ // 安装到指定目录(文件夹插件)
624
+ install({ package: 'axios', path: '.foliko/plugins/my-plugin' });
625
+
626
+ // 从 package.json 安装
627
+ install({ file: './my-plugin/package.json', path: '.foliko/plugins/my-plugin' });
628
+ ```
629
+
630
+ ### 重要依赖版本要求
631
+
632
+ **zod 必须使用 `"zod": "^3.25.76"` 版本**
633
+
634
+ ---
635
+
636
+ ## 14. 系统插件访问
637
+
638
+ ### 可访问的系统插件
639
+
640
+ | 插件名 | 说明 | 可用方法 |
641
+ |--------|------|----------|
642
+ | `storage` | 键值对存储 | `get(key)`, `set(key, value)`, `delete(key)` |
643
+ | `session` | 会话管理 | `getSession(id)`, `getHistory(id)` |
644
+ | `audit` | 审计日志 | `log(type, data)` |
645
+ | `scheduler` | 定时任务 | `addTask()`, `removeTask()` |
646
+ | `file-system` | 文件操作 | 工具已注册: `read_file`, `write_file` |
647
+ | `tools` | 工具管理 | `list()`, `reload()` |
648
+ | `ai` | AI 配置 | `getAIClient()` |
649
+
650
+ ### 访问示例
651
+
652
+ ```javascript
653
+ install(framework) {
654
+ // 访问 storage 插件
655
+ const storage = this._framework.pluginManager.get('storage');
656
+
657
+ // 访问 session 插件
658
+ const session = this._framework.pluginManager.get('session');
659
+
660
+ // 访问 audit 插件
661
+ const audit = this._framework.pluginManager.get('audit');
662
+ }
663
+ ```
664
+
665
+ ### Storage 插件 API
666
+
667
+ ```javascript
668
+ const storage = this._framework.pluginManager.get('storage');
669
+
670
+ // 方式1:通过工具调用(推荐)
671
+ // storage_get({ key: 'xxx', namespace: 'yyy' })
672
+ // storage_set({ key: 'xxx', value: {...}, namespace: 'yyy' })
673
+
674
+ // 方式2:直接调用内部方法
675
+ const value = storage.getStore().get('key');
676
+ storage.setDirect('key', value);
677
+ storage.deleteDirect('key');
678
+ ```
679
+
680
+ ### Session 插件 API
681
+
682
+ ```javascript
683
+ const session = this._framework.pluginManager.get('session');
684
+
685
+ // 获取会话
686
+ const sess = session.getSession(sessionId);
687
+
688
+ // 获取历史消息
689
+ const messages = session.getHistory(sessionId);
690
+
691
+ // 添加消息
692
+ session.addMessage(sessionId, { role: 'user', content: 'hello' });
693
+ ```
694
+
695
+ ---
696
+
697
+ ## 15. 最佳实践
698
+
699
+ ### 核心规则
700
+
701
+ | 规则 | 说明 |
702
+ |------|------|
703
+ | **优先使用 this.tools = {}** | ExtensionExecutor 自动扫描并注册到系统提示词 |
704
+ | **优先使用文件夹结构** | 复杂插件推荐用文件夹,便于管理依赖和代码 |
705
+ | **必须用 inputSchema** | `inputSchema: z.object({})`(不是 parameters!) |
706
+ | **必须用 zod 定义参数** | `param: z.string().describe('描述')` |
707
+ | **install 必须返回 this** | 确保链式调用 |
708
+ | **文件夹与文件重名时** | 文件夹优先,同名 `.js` 文件会被忽略 |
709
+ | **工具自动出现在系统提示词** | 定义在 `this.tools` 中会自动注册 |
710
+
711
+ ### 开发流程
712
+
713
+ 1. **创建插件**(优先使用文件夹结构)
714
+ 2. **安装依赖**:`install { package: "包名", path: ".foliko/plugins/插件名" }`
715
+ 3. **热重载**:`reload_plugins`
716
+ 4. **检查加载状态**
717
+
718
+ ### 引入第三方库
719
+
720
+ **必须先安装依赖,再热重载!**
721
+
722
+ ```javascript
723
+ // .foliko/plugins/my-plugin/index.js
724
+ const { z } = require('zod');
725
+
726
+ module.exports = function (Plugin) {
727
+ return class MyPlugin extends Plugin {
728
+ name = 'my-plugin';
729
+
730
+ tools = {
731
+ fetch_data: {
732
+ description: '获取远程数据',
733
+ inputSchema: z.object({
734
+ url: z.string().describe('API URL'),
735
+ }),
736
+ execute: async (args) => {
737
+ // 安装到插件自己的目录
738
+ await this._framework.callTool('install', {
739
+ package: 'axios',
740
+ path: '.foliko/plugins/my-plugin',
741
+ });
742
+
743
+ // 引入插件目录下的 node_modules
744
+ const axios = require('.foliko/plugins/my-plugin/node_modules/axios');
745
+ const response = await axios.get(args.url);
746
+ return { success: true, data: response.data };
747
+ },
748
+ },
749
+ };
750
+ };
751
+ };
752
+ ```
753
+
754
+ ### 错误处理
755
+
756
+ ```javascript
757
+ onStart(framework) {
758
+ try {
759
+ this.tool.register({ ... });
760
+ } catch (error) {
761
+ framework.logger.error(`[${this.name}] Failed to register tool: ${error.message}`);
762
+ }
763
+ }
764
+
765
+ uninstall(framework) {
766
+ // 确保清理所有资源
767
+ this.tool.remove('my_tool');
768
+ this.extension.remove('my_ext');
769
+ this.agent.remove('my-agent');
770
+ }
771
+ ```
772
+
773
+ ---
774
+
775
+ ## 示例插件
776
+
777
+ ### 完整示例:天气插件
778
+
779
+ ```javascript
780
+ // .foliko/plugins/weather/index.js
781
+ const { z } = require('zod');
782
+
783
+ module.exports = function (Plugin) {
784
+ return class WeatherPlugin extends Plugin {
785
+ constructor() {
786
+ super();
787
+ this.name = 'weather';
788
+ this.version = '1.0.0';
789
+ this.description = '天气查询插件';
790
+ }
791
+
792
+ tools = {
793
+ get_weather: {
794
+ description: '查询指定城市的天气',
795
+ inputSchema: z.object({
796
+ city: z.string().describe('城市名称'),
797
+ units: z.enum(['celsius', 'fahrenheit']).optional().describe('温度单位,默认 celsius'),
798
+ }),
799
+ execute: async (args) => {
800
+ // 这里应该是真实的 API 调用
801
+ return {
802
+ city: args.city,
803
+ temperature: 25,
804
+ units: args.units || 'celsius',
805
+ condition: 'Sunny',
806
+ };
807
+ },
808
+ },
809
+ };
810
+
811
+ onStart(framework) {
812
+ // 注册额外的工具
813
+ this.tool.register({
814
+ name: 'get_forecast',
815
+ description: '获取天气预报',
816
+ inputSchema: framework.z.object({
817
+ city: framework.z.string().describe('城市名称'),
818
+ days: framework.z.number().min(1).max(7).default(3).describe('预报天数'),
819
+ }),
820
+ execute: async (args) => {
821
+ return {
822
+ city: args.city,
823
+ forecast: Array.from({ length: args.days }, (_, i) => ({
824
+ day: i + 1,
825
+ temp: 20 + Math.floor(Math.random() * 10),
826
+ condition: ['Sunny', 'Cloudy', 'Rainy'][Math.floor(Math.random() * 3)],
827
+ })),
828
+ };
829
+ },
830
+ });
831
+ }
832
+
833
+ onStop() {
834
+ // 清理资源
835
+ }
836
+
837
+ uninstall(framework) {
838
+ // 完全清理
839
+ }
840
+ };
841
+ };
842
+ ```
843
+
844
+ ---
845
+
846
+ ## 相关文档
847
+
848
+ - [扩展系统](./extensions.md) - ext_call、ext_skill 统一调用
849
+ - [Public API](./public-api.md) - 框架公共 API 参考
850
+ - [架构文档](./architecture.md) - 系统架构设计
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foliko",
3
- "version": "2.0.30",
3
+ "version": "2.0.31",
4
4
  "description": "简约的插件化 Agent 框架",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
@@ -194,21 +194,13 @@ class ToolLoop extends EventEmitter {
194
194
  api.push({ role: 'user', content: text || '' });
195
195
  }
196
196
  } else if (msg.role === 'assistant') {
197
- // ★ 检测是否有 reasoning 内容(thinking mode 必须携带)
198
- const hasReasoningInContent = Array.isArray(msg.content) && msg.content.some(p => p && p.type === 'reasoning');
199
- const hasReasoningTopLevel = msg.reasoning_content || msg.reasoning;
197
+ // ★ 提取 text + tool_calls(兼容 content=string、content=array、顶层 tool_calls 三种格式)
198
+ let text = '';
199
+ const toolCalls = [];
200
200
 
201
201
  if (typeof msg.content === 'string') {
202
- if (msg.tool_calls && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
203
- // thinking mode 下,无 reasoning 则自动补 placeholder
204
- const reasonVal = hasReasoningTopLevel || (this._thinkingMode ? '...' : '');
205
- api.push({ role: 'assistant', content: msg.content || null, tool_calls: msg.tool_calls, ...(reasonVal ? { reasoning_content: reasonVal } : {}) });
206
- } else {
207
- api.push({ role: 'assistant', content: msg.content });
208
- }
202
+ text = msg.content;
209
203
  } else if (Array.isArray(msg.content)) {
210
- let text = '';
211
- const toolCalls = [];
212
204
  for (const part of msg.content) {
213
205
  if (!part) continue;
214
206
  if (part.type === 'text') text += part.text;
@@ -223,15 +215,24 @@ class ToolLoop extends EventEmitter {
223
215
  });
224
216
  }
225
217
  }
218
+ }
226
219
 
227
- if (toolCalls.length > 0) {
228
- const reasonVal = hasReasoningTopLevel || (this._thinkingMode ? '...' : '');
229
- api.push({ role: 'assistant', content: text || null, tool_calls: toolCalls, ...(reasonVal ? { reasoning_content: reasonVal } : {}) });
230
- } else {
231
- api.push({ role: 'assistant', content: text });
220
+ // 合并顶层 tool_calls(OpenAI 格式,来自 session 文件)
221
+ const existingIds = new Set(toolCalls.map(tc => tc.id));
222
+ if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
223
+ for (const tc of msg.tool_calls) {
224
+ if (!existingIds.has(tc.id)) toolCalls.push(tc);
225
+ existingIds.add(tc.id);
232
226
  }
227
+ }
228
+
229
+ if (toolCalls.length > 0) {
230
+ // ★ 核心:thinking mode 下,所有带 tool_calls 的消息都必须有 reasoning_content
231
+ // 无条件注入,不依赖 hasReasoningTopLevel 检测
232
+ const reasonVal = msg.reasoning_content || msg.reasoning || (this._thinkingMode ? '...' : '');
233
+ api.push({ role: 'assistant', content: text || null, tool_calls: toolCalls, ...(reasonVal ? { reasoning_content: reasonVal } : {}) });
233
234
  } else {
234
- api.push({ role: 'assistant', content: String(msg.content ?? '') });
235
+ api.push({ role: 'assistant', content: text || '' });
235
236
  }
236
237
  } else if (msg.role === 'tool') {
237
238
  if (Array.isArray(msg.content)) {