foliko 2.0.22 → 2.0.23
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/.editorconfig +56 -56
- package/.lintstagedrc +7 -7
- package/.prettierignore +29 -29
- package/.prettierrc +11 -11
- package/Dockerfile +63 -63
- package/install.ps1 +129 -129
- package/install.sh +121 -121
- package/package.json +2 -5
- package/plugins/core/workflow/context.js +941 -941
- package/plugins/core/workflow/engine.js +66 -66
- package/plugins/core/workflow/js-runner.js +318 -318
- package/plugins/core/workflow/json-runner.js +323 -323
- package/plugins/core/workflow/stages/choice.js +74 -74
- package/plugins/core/workflow/stages/each.js +123 -123
- package/plugins/core/workflow/stages/parallel.js +69 -69
- package/skills/find-skills/SKILL.md +133 -133
- package/src/agent/chat.js +188 -221
- package/src/agent/sub.js +28 -25
- package/src/agent/tool-loop.js +648 -0
- package/src/llm/provider.js +12 -28
- package/src/plugin/base.js +17 -14
- package/src/plugin/manager.js +19 -0
- package/tests/core/chat-tool.test.js +187 -187
- package/tests/core/disable-thinking.test.js +64 -0
- package/tests/core/reasoning-content.test.js +129 -0
- package/tests/core/strip-stale-tool-calls.test.js +154 -0
- package/tests/core/tool-loop.test.js +208 -0
package/src/llm/provider.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 目标 ~300 行
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const {
|
|
10
|
+
const { OpenAI } = require('openai');
|
|
11
11
|
|
|
12
12
|
// ==================== Provider 默认配置 ====================
|
|
13
13
|
|
|
@@ -71,9 +71,9 @@ class LLMProvider {
|
|
|
71
71
|
this.options = { ...config.options };
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/** 创建
|
|
74
|
+
/** 创建 OpenAI 客户端实例 */
|
|
75
75
|
createModel() {
|
|
76
|
-
return
|
|
76
|
+
return _createOpenAIClient(this.provider, this.apiKey, this.baseURL);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/** 当前模型是否 thinking mode */
|
|
@@ -120,15 +120,14 @@ class ProviderRegistry {
|
|
|
120
120
|
getProviderNames() { return Array.from(this._providers.keys()); }
|
|
121
121
|
hasProvider(name) { return this._providers.has(name); }
|
|
122
122
|
|
|
123
|
-
/** 创建
|
|
123
|
+
/** 创建 OpenAI 客户端(自动 fallback 到注册的默认值) */
|
|
124
124
|
createClient(providerName, model, apiKey, baseURL) {
|
|
125
125
|
const provider = this._providers.get(providerName);
|
|
126
126
|
if (!provider) throw new Error(`Provider '${providerName}' not registered.`);
|
|
127
127
|
const finalApiKey = apiKey || provider.apiKey;
|
|
128
128
|
const finalBaseURL = baseURL || provider.baseURL;
|
|
129
|
-
const finalModel = model || provider.defaultModel;
|
|
130
129
|
if (!finalApiKey) throw new Error(`No API key for provider '${providerName}'`);
|
|
131
|
-
return
|
|
130
|
+
return _createOpenAIClient(providerName, finalApiKey, finalBaseURL);
|
|
132
131
|
}
|
|
133
132
|
|
|
134
133
|
unregisterProvider(name) {
|
|
@@ -155,39 +154,24 @@ function _registerDefaultProviders() {
|
|
|
155
154
|
}
|
|
156
155
|
_registerDefaultProviders();
|
|
157
156
|
|
|
158
|
-
// ====================
|
|
157
|
+
// ==================== OpenAI 客户端工厂 ====================
|
|
159
158
|
|
|
160
|
-
function
|
|
161
|
-
const providerName = (provider || 'deepseek').toLowerCase();
|
|
159
|
+
function _createOpenAIClient(providerName, apiKey, baseURL) {
|
|
162
160
|
const pcfg = DEFAULT_PROVIDERS[providerName];
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const streamOptions = { includeUsage: true };
|
|
166
|
-
if (providerName === 'deepseek') streamOptions.includeReasoningContent = true;
|
|
167
|
-
return createOpenAICompatible({
|
|
168
|
-
name: pcfg.name,
|
|
169
|
-
baseURL: baseURL || pcfg.baseURL,
|
|
170
|
-
apiKey: apiKey || 'dummy-key',
|
|
171
|
-
headers: provider === 'anthropic' ? { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' } : undefined,
|
|
172
|
-
models: { default: { id: model || 'deepseek-chat', streamOptions } },
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return createOpenAICompatible({
|
|
177
|
-
name: providerName || 'Custom',
|
|
178
|
-
baseURL,
|
|
161
|
+
return new OpenAI({
|
|
162
|
+
baseURL: baseURL || (pcfg ? pcfg.baseURL : undefined),
|
|
179
163
|
apiKey: apiKey || 'dummy-key',
|
|
180
|
-
|
|
164
|
+
// Anthropic 等需要额外 header 的 provider 在请求时加头
|
|
181
165
|
});
|
|
182
166
|
}
|
|
183
167
|
|
|
184
168
|
// ==================== 向后兼容 API ====================
|
|
185
169
|
|
|
186
170
|
function createAI(config) {
|
|
187
|
-
return
|
|
171
|
+
return _createOpenAIClient((config.provider || 'deepseek').toLowerCase(), config.apiKey, config.baseURL);
|
|
188
172
|
}
|
|
189
173
|
|
|
190
|
-
function createModel(model, client) { return client
|
|
174
|
+
function createModel(model, client) { return client; }
|
|
191
175
|
|
|
192
176
|
function getAvailableProviders() {
|
|
193
177
|
return Object.entries(DEFAULT_PROVIDERS).map(([k, v]) => ({ id: k, name: v.name, baseURL: v.baseURL }));
|
package/src/plugin/base.js
CHANGED
|
@@ -854,22 +854,25 @@ class Plugin {
|
|
|
854
854
|
// 解析文件路径:相对路径基于插件文件目录,绝对路径直接用
|
|
855
855
|
let fullPath = filePath;
|
|
856
856
|
if (!path.isAbsolute(filePath)) {
|
|
857
|
-
//
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
857
|
+
// 优先使用插件实例的 sourcePath(更可靠)
|
|
858
|
+
let pluginDir = null;
|
|
859
|
+
if (this.sourcePath) {
|
|
860
|
+
pluginDir = path.dirname(path.resolve(this.sourcePath));
|
|
861
|
+
}
|
|
862
|
+
// 备用:使用栈追踪获取插件文件路径
|
|
863
|
+
if (!pluginDir) {
|
|
864
|
+
const stack = new Error().stack.split('\n');
|
|
865
|
+
for (let i = 3; i < stack.length; i++) {
|
|
866
|
+
const line = stack[i];
|
|
867
|
+
if (line.includes('plugin/base.js') || line.includes('node:')) continue;
|
|
868
|
+
const match = line.match(/\((.*?):\d+:\d+\)/) || line.match(/at\s+(.*?):\d+:\d+/);
|
|
869
|
+
if (match && match[1] !== '[eval]') {
|
|
870
|
+
pluginDir = path.dirname(path.resolve(match[1]));
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
870
873
|
}
|
|
871
874
|
}
|
|
872
|
-
|
|
875
|
+
pluginDir = pluginDir || process.cwd();
|
|
873
876
|
fullPath = path.resolve(pluginDir, filePath);
|
|
874
877
|
}
|
|
875
878
|
|
package/src/plugin/manager.js
CHANGED
|
@@ -376,6 +376,25 @@ class PluginManager {
|
|
|
376
376
|
this._log.info(`Plugin reloaded: ${name}`);
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
/**
|
|
380
|
+
* 重载所有已加载的插件
|
|
381
|
+
*/
|
|
382
|
+
async reloadAll() {
|
|
383
|
+
const entries = Array.from(this._plugins.values()).filter(e => e.status === 'loaded');
|
|
384
|
+
const results = [];
|
|
385
|
+
for (const entry of entries) {
|
|
386
|
+
const name = entry.instance.name;
|
|
387
|
+
try {
|
|
388
|
+
await this.reload(name);
|
|
389
|
+
results.push({ name, success: true });
|
|
390
|
+
} catch (err) {
|
|
391
|
+
this._log.warn(`reloadAll: '${name}' failed: ${err.message}`);
|
|
392
|
+
results.push({ name, success: false, error: err.message });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return results;
|
|
396
|
+
}
|
|
397
|
+
|
|
379
398
|
/**
|
|
380
399
|
* 启动所有已加载、已启用、未启动的插件(按优先级排序)
|
|
381
400
|
*/
|
|
@@ -1,187 +1,187 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
|
|
3
|
-
// 测试 file-system 插件的 chat 工具修复
|
|
4
|
-
// 通过读源码 + 模拟 execute 来验证行为
|
|
5
|
-
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
|
|
9
|
-
const FileSystemPlugin = require('../../plugins/io/file-system/index.js');
|
|
10
|
-
|
|
11
|
-
function makeFakeFw(opts = {}) {
|
|
12
|
-
const llmConfig = opts.llmConfig || { model: 'test-model', provider: 'test-provider' };
|
|
13
|
-
const captured = { agentConfig: null, chatArgs: null, registeredTools: [] };
|
|
14
|
-
const fw = {
|
|
15
|
-
getCwd: () => process.cwd(),
|
|
16
|
-
pluginManager: {
|
|
17
|
-
get: (name) => {
|
|
18
|
-
if (name === 'ai') return { getConfig: () => llmConfig };
|
|
19
|
-
return null;
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
// ★ Plugin base class 的 tool.register 会先 push 到 _registeredTools,
|
|
23
|
-
// 然后查 registry.has(),has() 返回 false 才调 registry.register()。
|
|
24
|
-
// 所以 mock 必须实现 has() 否则注册流程被中断。
|
|
25
|
-
registerTool: (toolDef) => {
|
|
26
|
-
captured.registeredTools.push(toolDef);
|
|
27
|
-
},
|
|
28
|
-
toolRegistry: {
|
|
29
|
-
has: () => false,
|
|
30
|
-
register: (toolDef) => {
|
|
31
|
-
captured.registeredTools.push(toolDef);
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
createSubAgent: (config) => {
|
|
35
|
-
captured.agentConfig = config;
|
|
36
|
-
return {
|
|
37
|
-
chat: async (msg, opts) => {
|
|
38
|
-
captured.chatArgs = { msg, opts };
|
|
39
|
-
return { success: true, message: 'mock reply', steps: 1 };
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
return { fw, captured };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function getChatTool(captured) {
|
|
48
|
-
return captured.registeredTools.find(t => t.name === 'chat');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe('file-system 插件的 chat 工具', () => {
|
|
52
|
-
it('★ 关键:name 唯一(用 Date.now + random 避免冲突)', async () => {
|
|
53
|
-
const { fw, captured } = makeFakeFw();
|
|
54
|
-
const p = new FileSystemPlugin();
|
|
55
|
-
p.install(fw);
|
|
56
|
-
p.start(fw);
|
|
57
|
-
// 直接调工具的 execute
|
|
58
|
-
// find chat tool in _registeredTools
|
|
59
|
-
const chatTool = getChatTool(captured);
|
|
60
|
-
expect(chatTool).toBeTruthy();
|
|
61
|
-
if (!chatTool) return;
|
|
62
|
-
|
|
63
|
-
const r1 = await chatTool.execute({ message: 'hi' }, fw);
|
|
64
|
-
const r2 = await chatTool.execute({ message: 'hi2' }, fw);
|
|
65
|
-
|
|
66
|
-
expect(captured.agentConfig.name).toMatch(/^file-system-chat-\d+/);
|
|
67
|
-
// 两次调用应产生不同名字
|
|
68
|
-
const name1 = r1.metadata?.agent;
|
|
69
|
-
const name2 = r2.metadata?.agent;
|
|
70
|
-
expect(name1).not.toBe(name2);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('★ 关键:sessionId 隔离(避免污染主对话)', async () => {
|
|
74
|
-
const { fw, captured } = makeFakeFw();
|
|
75
|
-
const p = new FileSystemPlugin();
|
|
76
|
-
p.install(fw);
|
|
77
|
-
p.start(fw);
|
|
78
|
-
const chatTool = getChatTool(captured);
|
|
79
|
-
if (!chatTool) throw new Error('no chat tool');
|
|
80
|
-
|
|
81
|
-
// 1. 不传 sessionId → 自动生成
|
|
82
|
-
await chatTool.execute({ message: 'm1' }, fw);
|
|
83
|
-
expect(captured.chatArgs.opts.sessionId).toMatch(/^chat-\d+$/);
|
|
84
|
-
|
|
85
|
-
// 2. 传 sessionId → 使用传入的
|
|
86
|
-
await chatTool.execute({ message: 'm2', sessionId: 'my-session' }, fw);
|
|
87
|
-
expect(captured.chatArgs.opts.sessionId).toBe('my-session');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('★ 关键:跟随主 agent 的 LLM 配置', async () => {
|
|
91
|
-
const llmConfig = {
|
|
92
|
-
model: 'deepseek-chat',
|
|
93
|
-
provider: 'deepseek',
|
|
94
|
-
apiKey: 'sk-test',
|
|
95
|
-
baseURL: 'https://api.deepseek.com/v1',
|
|
96
|
-
};
|
|
97
|
-
const { fw, captured } = makeFakeFw({ llmConfig });
|
|
98
|
-
const p = new FileSystemPlugin();
|
|
99
|
-
p.install(fw);
|
|
100
|
-
p.start(fw);
|
|
101
|
-
const chatTool = getChatTool(captured);
|
|
102
|
-
if (!chatTool) throw new Error('no chat tool');
|
|
103
|
-
|
|
104
|
-
await chatTool.execute({ message: 'hi' }, fw);
|
|
105
|
-
expect(captured.agentConfig.model).toBe('deepseek-chat');
|
|
106
|
-
expect(captured.agentConfig.provider).toBe('deepseek');
|
|
107
|
-
expect(captured.agentConfig.apiKey).toBe('sk-test');
|
|
108
|
-
expect(captured.agentConfig.baseURL).toBe('https://api.deepseek.com/v1');
|
|
109
|
-
expect(captured.agentConfig.hidden).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('★ 关键:data 只返回 message 文本(不返回整个对象)', async () => {
|
|
113
|
-
const { fw, captured } = makeFakeFw();
|
|
114
|
-
const p = new FileSystemPlugin();
|
|
115
|
-
p.install(fw);
|
|
116
|
-
p.start(fw);
|
|
117
|
-
const chatTool = getChatTool(captured);
|
|
118
|
-
if (!chatTool) throw new Error('no chat tool');
|
|
119
|
-
|
|
120
|
-
const r = await chatTool.execute({ message: 'hi' }, fw);
|
|
121
|
-
expect(r.success).toBe(true);
|
|
122
|
-
expect(r.data).toBe('mock reply'); // 只是字符串,不是整个对象
|
|
123
|
-
expect(typeof r.data).toBe('string');
|
|
124
|
-
// metadata 含元信息
|
|
125
|
-
expect(r.metadata).toBeTruthy();
|
|
126
|
-
expect(r.metadata.steps).toBe(1);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('★ 关键:systemPrompt 默认值合理', async () => {
|
|
130
|
-
const { fw, captured } = makeFakeFw();
|
|
131
|
-
const p = new FileSystemPlugin();
|
|
132
|
-
p.install(fw);
|
|
133
|
-
p.start(fw);
|
|
134
|
-
const chatTool = getChatTool(captured);
|
|
135
|
-
if (!chatTool) throw new Error('no chat tool');
|
|
136
|
-
|
|
137
|
-
// 不传 systemPrompt → 用默认
|
|
138
|
-
await chatTool.execute({ message: 'hi' }, fw);
|
|
139
|
-
expect(captured.agentConfig.systemPrompt).toContain('文件系统');
|
|
140
|
-
|
|
141
|
-
// 传 systemPrompt → 覆盖
|
|
142
|
-
await chatTool.execute({
|
|
143
|
-
message: 'hi',
|
|
144
|
-
systemPrompt: '你是一个翻译助手',
|
|
145
|
-
}, fw);
|
|
146
|
-
expect(captured.agentConfig.systemPrompt).toBe('你是一个翻译助手');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('★ 关键:错误捕获(AI 抛错时不崩溃)', async () => {
|
|
150
|
-
const registeredTools = [];
|
|
151
|
-
const fw = {
|
|
152
|
-
getCwd: () => process.cwd(),
|
|
153
|
-
pluginManager: { get: () => null },
|
|
154
|
-
registerTool: (t) => registeredTools.push(t),
|
|
155
|
-
toolRegistry: { has: () => false, register: (t) => registeredTools.push(t) },
|
|
156
|
-
createSubAgent: () => ({
|
|
157
|
-
chat: async () => { throw new Error('AI 服务不可用'); },
|
|
158
|
-
}),
|
|
159
|
-
};
|
|
160
|
-
const p = new FileSystemPlugin();
|
|
161
|
-
p.install(fw);
|
|
162
|
-
p.start(fw);
|
|
163
|
-
const chatTool = registeredTools.find(t => t.name === 'chat');
|
|
164
|
-
if (!chatTool) throw new Error('no chat tool');
|
|
165
|
-
|
|
166
|
-
const r = await chatTool.execute({ message: 'hi' }, fw);
|
|
167
|
-
expect(r.success).toBe(false);
|
|
168
|
-
expect(r.error).toBe('AI 服务不可用');
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('★ inputSchema 含 sessionId 字段', () => {
|
|
172
|
-
// 直接读源码验证
|
|
173
|
-
const src = fs.readFileSync(
|
|
174
|
-
path.join(__dirname, '../../plugins/io/file-system/index.js'),
|
|
175
|
-
'utf-8'
|
|
176
|
-
);
|
|
177
|
-
expect(src).toMatch(/sessionId:\s*z\.string\(\)\.optional\(\)/);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('★ 删除死代码:const ai = framework.getAI()', () => {
|
|
181
|
-
const src = fs.readFileSync(
|
|
182
|
-
path.join(__dirname, '../../plugins/io/file-system/index.js'),
|
|
183
|
-
'utf-8'
|
|
184
|
-
);
|
|
185
|
-
expect(src).not.toMatch(/const\s+ai\s*=\s*framework\.getAI/);
|
|
186
|
-
});
|
|
187
|
-
});
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// 测试 file-system 插件的 chat 工具修复
|
|
4
|
+
// 通过读源码 + 模拟 execute 来验证行为
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const FileSystemPlugin = require('../../plugins/io/file-system/index.js');
|
|
10
|
+
|
|
11
|
+
function makeFakeFw(opts = {}) {
|
|
12
|
+
const llmConfig = opts.llmConfig || { model: 'test-model', provider: 'test-provider' };
|
|
13
|
+
const captured = { agentConfig: null, chatArgs: null, registeredTools: [] };
|
|
14
|
+
const fw = {
|
|
15
|
+
getCwd: () => process.cwd(),
|
|
16
|
+
pluginManager: {
|
|
17
|
+
get: (name) => {
|
|
18
|
+
if (name === 'ai') return { getConfig: () => llmConfig };
|
|
19
|
+
return null;
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
// ★ Plugin base class 的 tool.register 会先 push 到 _registeredTools,
|
|
23
|
+
// 然后查 registry.has(),has() 返回 false 才调 registry.register()。
|
|
24
|
+
// 所以 mock 必须实现 has() 否则注册流程被中断。
|
|
25
|
+
registerTool: (toolDef) => {
|
|
26
|
+
captured.registeredTools.push(toolDef);
|
|
27
|
+
},
|
|
28
|
+
toolRegistry: {
|
|
29
|
+
has: () => false,
|
|
30
|
+
register: (toolDef) => {
|
|
31
|
+
captured.registeredTools.push(toolDef);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
createSubAgent: (config) => {
|
|
35
|
+
captured.agentConfig = config;
|
|
36
|
+
return {
|
|
37
|
+
chat: async (msg, opts) => {
|
|
38
|
+
captured.chatArgs = { msg, opts };
|
|
39
|
+
return { success: true, message: 'mock reply', steps: 1 };
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
return { fw, captured };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getChatTool(captured) {
|
|
48
|
+
return captured.registeredTools.find(t => t.name === 'chat');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('file-system 插件的 chat 工具', () => {
|
|
52
|
+
it('★ 关键:name 唯一(用 Date.now + random 避免冲突)', async () => {
|
|
53
|
+
const { fw, captured } = makeFakeFw();
|
|
54
|
+
const p = new FileSystemPlugin();
|
|
55
|
+
p.install(fw);
|
|
56
|
+
p.start(fw);
|
|
57
|
+
// 直接调工具的 execute
|
|
58
|
+
// find chat tool in _registeredTools
|
|
59
|
+
const chatTool = getChatTool(captured);
|
|
60
|
+
expect(chatTool).toBeTruthy();
|
|
61
|
+
if (!chatTool) return;
|
|
62
|
+
|
|
63
|
+
const r1 = await chatTool.execute({ message: 'hi' }, fw);
|
|
64
|
+
const r2 = await chatTool.execute({ message: 'hi2' }, fw);
|
|
65
|
+
|
|
66
|
+
expect(captured.agentConfig.name).toMatch(/^file-system-chat-\d+/);
|
|
67
|
+
// 两次调用应产生不同名字
|
|
68
|
+
const name1 = r1.metadata?.agent;
|
|
69
|
+
const name2 = r2.metadata?.agent;
|
|
70
|
+
expect(name1).not.toBe(name2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('★ 关键:sessionId 隔离(避免污染主对话)', async () => {
|
|
74
|
+
const { fw, captured } = makeFakeFw();
|
|
75
|
+
const p = new FileSystemPlugin();
|
|
76
|
+
p.install(fw);
|
|
77
|
+
p.start(fw);
|
|
78
|
+
const chatTool = getChatTool(captured);
|
|
79
|
+
if (!chatTool) throw new Error('no chat tool');
|
|
80
|
+
|
|
81
|
+
// 1. 不传 sessionId → 自动生成
|
|
82
|
+
await chatTool.execute({ message: 'm1' }, fw);
|
|
83
|
+
expect(captured.chatArgs.opts.sessionId).toMatch(/^chat-\d+$/);
|
|
84
|
+
|
|
85
|
+
// 2. 传 sessionId → 使用传入的
|
|
86
|
+
await chatTool.execute({ message: 'm2', sessionId: 'my-session' }, fw);
|
|
87
|
+
expect(captured.chatArgs.opts.sessionId).toBe('my-session');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('★ 关键:跟随主 agent 的 LLM 配置', async () => {
|
|
91
|
+
const llmConfig = {
|
|
92
|
+
model: 'deepseek-chat',
|
|
93
|
+
provider: 'deepseek',
|
|
94
|
+
apiKey: 'sk-test',
|
|
95
|
+
baseURL: 'https://api.deepseek.com/v1',
|
|
96
|
+
};
|
|
97
|
+
const { fw, captured } = makeFakeFw({ llmConfig });
|
|
98
|
+
const p = new FileSystemPlugin();
|
|
99
|
+
p.install(fw);
|
|
100
|
+
p.start(fw);
|
|
101
|
+
const chatTool = getChatTool(captured);
|
|
102
|
+
if (!chatTool) throw new Error('no chat tool');
|
|
103
|
+
|
|
104
|
+
await chatTool.execute({ message: 'hi' }, fw);
|
|
105
|
+
expect(captured.agentConfig.model).toBe('deepseek-chat');
|
|
106
|
+
expect(captured.agentConfig.provider).toBe('deepseek');
|
|
107
|
+
expect(captured.agentConfig.apiKey).toBe('sk-test');
|
|
108
|
+
expect(captured.agentConfig.baseURL).toBe('https://api.deepseek.com/v1');
|
|
109
|
+
expect(captured.agentConfig.hidden).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('★ 关键:data 只返回 message 文本(不返回整个对象)', async () => {
|
|
113
|
+
const { fw, captured } = makeFakeFw();
|
|
114
|
+
const p = new FileSystemPlugin();
|
|
115
|
+
p.install(fw);
|
|
116
|
+
p.start(fw);
|
|
117
|
+
const chatTool = getChatTool(captured);
|
|
118
|
+
if (!chatTool) throw new Error('no chat tool');
|
|
119
|
+
|
|
120
|
+
const r = await chatTool.execute({ message: 'hi' }, fw);
|
|
121
|
+
expect(r.success).toBe(true);
|
|
122
|
+
expect(r.data).toBe('mock reply'); // 只是字符串,不是整个对象
|
|
123
|
+
expect(typeof r.data).toBe('string');
|
|
124
|
+
// metadata 含元信息
|
|
125
|
+
expect(r.metadata).toBeTruthy();
|
|
126
|
+
expect(r.metadata.steps).toBe(1);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('★ 关键:systemPrompt 默认值合理', async () => {
|
|
130
|
+
const { fw, captured } = makeFakeFw();
|
|
131
|
+
const p = new FileSystemPlugin();
|
|
132
|
+
p.install(fw);
|
|
133
|
+
p.start(fw);
|
|
134
|
+
const chatTool = getChatTool(captured);
|
|
135
|
+
if (!chatTool) throw new Error('no chat tool');
|
|
136
|
+
|
|
137
|
+
// 不传 systemPrompt → 用默认
|
|
138
|
+
await chatTool.execute({ message: 'hi' }, fw);
|
|
139
|
+
expect(captured.agentConfig.systemPrompt).toContain('文件系统');
|
|
140
|
+
|
|
141
|
+
// 传 systemPrompt → 覆盖
|
|
142
|
+
await chatTool.execute({
|
|
143
|
+
message: 'hi',
|
|
144
|
+
systemPrompt: '你是一个翻译助手',
|
|
145
|
+
}, fw);
|
|
146
|
+
expect(captured.agentConfig.systemPrompt).toBe('你是一个翻译助手');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('★ 关键:错误捕获(AI 抛错时不崩溃)', async () => {
|
|
150
|
+
const registeredTools = [];
|
|
151
|
+
const fw = {
|
|
152
|
+
getCwd: () => process.cwd(),
|
|
153
|
+
pluginManager: { get: () => null },
|
|
154
|
+
registerTool: (t) => registeredTools.push(t),
|
|
155
|
+
toolRegistry: { has: () => false, register: (t) => registeredTools.push(t) },
|
|
156
|
+
createSubAgent: () => ({
|
|
157
|
+
chat: async () => { throw new Error('AI 服务不可用'); },
|
|
158
|
+
}),
|
|
159
|
+
};
|
|
160
|
+
const p = new FileSystemPlugin();
|
|
161
|
+
p.install(fw);
|
|
162
|
+
p.start(fw);
|
|
163
|
+
const chatTool = registeredTools.find(t => t.name === 'chat');
|
|
164
|
+
if (!chatTool) throw new Error('no chat tool');
|
|
165
|
+
|
|
166
|
+
const r = await chatTool.execute({ message: 'hi' }, fw);
|
|
167
|
+
expect(r.success).toBe(false);
|
|
168
|
+
expect(r.error).toBe('AI 服务不可用');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('★ inputSchema 含 sessionId 字段', () => {
|
|
172
|
+
// 直接读源码验证
|
|
173
|
+
const src = fs.readFileSync(
|
|
174
|
+
path.join(__dirname, '../../plugins/io/file-system/index.js'),
|
|
175
|
+
'utf-8'
|
|
176
|
+
);
|
|
177
|
+
expect(src).toMatch(/sessionId:\s*z\.string\(\)\.optional\(\)/);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('★ 删除死代码:const ai = framework.getAI()', () => {
|
|
181
|
+
const src = fs.readFileSync(
|
|
182
|
+
path.join(__dirname, '../../plugins/io/file-system/index.js'),
|
|
183
|
+
'utf-8'
|
|
184
|
+
);
|
|
185
|
+
expect(src).not.toMatch(/const\s+ai\s*=\s*framework\.getAI/);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// 测试 FOLIKO_DISABLE_THINKING 环境变量
|
|
4
|
+
|
|
5
|
+
describe('★ FOLIKO_DISABLE_THINKING 环境变量', () => {
|
|
6
|
+
it('★ 关键:环境变量被识别(forceDisabled 逻辑)', () => {
|
|
7
|
+
const src = require('fs').readFileSync(
|
|
8
|
+
require('path').join(__dirname, '../../src/agent/chat.js'),
|
|
9
|
+
'utf-8'
|
|
10
|
+
);
|
|
11
|
+
expect(src).toMatch(/FOLIKO_DISABLE_THINKING/);
|
|
12
|
+
expect(src).toMatch(/forceDisabled\s*=\s*process\.env\.FOLIKO_DISABLE_THINKING/);
|
|
13
|
+
expect(src).toMatch(/!forceDisabled/);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('★ 逻辑:3 个条件同时满足才启用 thinking,缺一不可', () => {
|
|
17
|
+
const src = require('fs').readFileSync(
|
|
18
|
+
require('path').join(__dirname, '../../src/agent/chat.js'),
|
|
19
|
+
'utf-8'
|
|
20
|
+
);
|
|
21
|
+
// enableThinking 表达式必须包含 forceDisabled 否定
|
|
22
|
+
const match = src.match(/const enableThinking\s*=\s*([^;]+);/);
|
|
23
|
+
expect(match).toBeTruthy();
|
|
24
|
+
const expr = match[1];
|
|
25
|
+
// 期望:!forceDisabled && config.thinkingMode === true && isThinkingModel(this.model)
|
|
26
|
+
expect(expr).toMatch(/!forceDisabled/);
|
|
27
|
+
expect(expr).toMatch(/config\.thinkingMode\s*===\s*true/);
|
|
28
|
+
expect(expr).toMatch(/isThinkingModel/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('★ 行为:F=1 时即使 thinkingMode=true 也强制关闭', () => {
|
|
32
|
+
const THINKING_MODELS = ['deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-reasoner'];
|
|
33
|
+
function isThinkingModel(model) {
|
|
34
|
+
if (!model) return false;
|
|
35
|
+
const lower = model.toLowerCase();
|
|
36
|
+
return THINKING_MODELS.some(tm => lower.includes(tm) || lower === tm);
|
|
37
|
+
}
|
|
38
|
+
// 模拟 4 种组合
|
|
39
|
+
const testCases = [
|
|
40
|
+
// [model, thinkingMode, forceDisabled, expected]
|
|
41
|
+
['deepseek-v4-pro', true, '1', false], // 强制关闭
|
|
42
|
+
['deepseek-v4-pro', true, null, true], // 正常启用
|
|
43
|
+
['deepseek-v4-pro', false, '1', false],
|
|
44
|
+
['deepseek-v4-pro', false, null, false],
|
|
45
|
+
['deepseek-chat', true, '1', false], // 强制关闭
|
|
46
|
+
['deepseek-chat', true, null, false], // 模型不是 thinking
|
|
47
|
+
['deepseek-reasoner', true, '1', false], // 强制关闭
|
|
48
|
+
['deepseek-reasoner', true, null, true], // 启用
|
|
49
|
+
];
|
|
50
|
+
for (const [model, thinkingMode, forceDisabled, expected] of testCases) {
|
|
51
|
+
const enableThinking = !forceDisabled && thinkingMode === true && isThinkingModel(model);
|
|
52
|
+
expect(enableThinking).toBe(expected);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('★ 文档:使用示例(注释或代码说明)', () => {
|
|
57
|
+
const src = require('fs').readFileSync(
|
|
58
|
+
require('path').join(__dirname, '../../src/agent/chat.js'),
|
|
59
|
+
'utf-8'
|
|
60
|
+
);
|
|
61
|
+
// 应该有 "FOLIKO_DISABLE_THINKING=1" 的使用说明
|
|
62
|
+
expect(src).toMatch(/FOLIKO_DISABLE_THINKING=1/);
|
|
63
|
+
});
|
|
64
|
+
});
|