foliko 2.0.22 → 2.0.24

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.
@@ -7,7 +7,7 @@
7
7
  * 目标 ~300 行
8
8
  */
9
9
 
10
- const { createOpenAICompatible } = require('@ai-sdk/openai-compatible');
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
- /** 创建 AI SDK model 实例 */
74
+ /** 创建 OpenAI 客户端实例 */
75
75
  createModel() {
76
- return _createAIClient(this.provider, this.model, this.apiKey, this.baseURL)(this.model);
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
- /** 创建 AI SDK model 实例(自动 fallback 到注册的默认值) */
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 _createAIClient(providerName, finalModel, finalApiKey, finalBaseURL)(finalModel);
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
- // ==================== 内部 AI SDK 客户端工厂 ====================
157
+ // ==================== OpenAI 客户端工厂 ====================
159
158
 
160
- function _createAIClient(provider, model, apiKey, baseURL) {
161
- const providerName = (provider || 'deepseek').toLowerCase();
159
+ function _createOpenAIClient(providerName, apiKey, baseURL) {
162
160
  const pcfg = DEFAULT_PROVIDERS[providerName];
163
-
164
- if (pcfg) {
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
- models: { default: { id: model || 'gpt-4' } },
164
+ // Anthropic 等需要额外 header provider 在请求时加头
181
165
  });
182
166
  }
183
167
 
184
168
  // ==================== 向后兼容 API ====================
185
169
 
186
170
  function createAI(config) {
187
- return _createAIClient(config.provider, config.model, config.apiKey, config.baseURL);
171
+ return _createOpenAIClient((config.provider || 'deepseek').toLowerCase(), config.apiKey, config.baseURL);
188
172
  }
189
173
 
190
- function createModel(model, client) { return client(model); }
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 }));
@@ -854,22 +854,26 @@ class Plugin {
854
854
  // 解析文件路径:相对路径基于插件文件目录,绝对路径直接用
855
855
  let fullPath = filePath;
856
856
  if (!path.isAbsolute(filePath)) {
857
- // 使用栈追踪获取插件文件路径
858
- // 栈结构: Error, _createFilePromptProvider, _autoRegisterPrompts, 子类start, plugin-manager调用start, ...
859
- const stack = new Error().stack.split('\n');
860
- let pluginFile;
861
- for (let i = 3; i < stack.length; i++) { // 从索引3开始跳过 base.js 的方法
862
- const line = stack[i];
863
- // 排除 base.js 自身和 node: 内置模块
864
- if (line.includes('plugin/base.js') || line.includes('node:')) continue;
865
- // 提取文件路径 (filename:line:col) (filename)
866
- const match = line.match(/\((.*?):\d+:\d+\)/) || line.match(/at\s+(.*?):\d+:\d+/);
867
- if (match && match[1] !== '[eval]') {
868
- pluginFile = match[1];
869
- break;
857
+ // 优先使用 pluginManager 中存储的 sourcePath(最可靠)
858
+ let pluginDir = null;
859
+ const pmEntry = this._framework?.pluginManager?.get?.(this.name);
860
+ if (pmEntry?.sourcePath) {
861
+ pluginDir = path.dirname(path.resolve(pmEntry.sourcePath));
862
+ }
863
+ // 备用:使用栈追踪获取插件文件路径
864
+ if (!pluginDir) {
865
+ const stack = new Error().stack.split('\n');
866
+ for (let i = 3; i < stack.length; i++) {
867
+ const line = stack[i];
868
+ if (line.includes('plugin/base.js') || line.includes('node:')) continue;
869
+ const match = line.match(/\((.*?):\d+:\d+\)/) || line.match(/at\s+(.*?):\d+:\d+/);
870
+ if (match && match[1] !== '[eval]') {
871
+ pluginDir = path.dirname(path.resolve(match[1]));
872
+ break;
873
+ }
870
874
  }
871
875
  }
872
- const pluginDir = pluginFile ? path.dirname(path.resolve(pluginFile)) : process.cwd();
876
+ pluginDir = pluginDir || process.cwd();
873
877
  fullPath = path.resolve(pluginDir, filePath);
874
878
  }
875
879
 
@@ -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
+ });