my-ai-chat-framework 2.7.0 → 4.0.0
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/CHANGELOG.md +117 -0
- package/LICENSE +20 -20
- package/README.md +103 -16
- package/README_ZH.md +392 -0
- package/dist/my-ai-chat-framework.browser.es.js +1238 -421
- package/dist/my-ai-chat-framework.browser.es.js.map +1 -1
- package/dist/my-ai-chat-framework.browser.umd.js +1245 -420
- package/dist/my-ai-chat-framework.browser.umd.js.map +1 -1
- package/dist/my-ai-chat-framework.node.cjs.js +1245 -420
- package/dist/my-ai-chat-framework.node.cjs.js.map +1 -1
- package/docs/DEVELOPER.md +569 -0
- package/docs/README.md +33 -0
- package/package.json +31 -8
- package/src/adapters/openai.js +124 -23
- package/src/core/ChatService.js +596 -133
- package/src/core/Errors.js +72 -59
- package/src/core/MessageStore.js +27 -0
- package/src/core/Pipeline.js +48 -0
- package/src/core/SystemPromptStore.js +120 -118
- package/src/index.js +7 -4
- package/src/plugins/continuation.js +156 -0
- package/src/plugins/model-registry.js +236 -187
- package/src/plugins/tool-calling.js +245 -187
- package/src/utils/MessageFormatter.js +296 -204
- package/src/utils/typeCheck.js +11 -11
- package/src/utils/url.js +17 -17
|
@@ -2,212 +2,270 @@
|
|
|
2
2
|
* 工具调用插件
|
|
3
3
|
* 功能:拦截助手消息中的 tool_calls,执行对应的工具,将结果作为 tool 消息加入对话,
|
|
4
4
|
* 然后自动继续对话(通过 sendExisting / sendExistingStream),直到没有新的工具调用。
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* 设计要点:
|
|
7
7
|
* - 支持普通请求和流式请求(通过 isStream 标志区分)
|
|
8
8
|
* - 支持多次工具调用循环(maxIterations 防止无限循环)
|
|
9
|
-
* -
|
|
9
|
+
* - 并行执行工具
|
|
10
10
|
* - 工具执行失败时,仍然返回错误信息给 AI,而不是中断整个流程
|
|
11
11
|
* - 触发 tool-error 事件,方便用户监听工具执行异常
|
|
12
|
+
*
|
|
13
|
+
* 注意:请使用 createToolCallingPlugin() 工厂创建实例。
|
|
14
|
+
* 默认导出的 toolCallingPlugin 是兼容用的模块级单例,装到多个 ChatService
|
|
15
|
+
* 实例会互相覆盖(executor 表、工具定义、配置),新代码不要直接用单例。
|
|
12
16
|
*/
|
|
13
|
-
export const toolCallingPlugin =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
export const toolCallingPlugin = createToolCallingPlugin();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 创建工具调用插件实例(工厂)。
|
|
21
|
+
* @param {Object} [options] — { timeout?, maxIterations? }
|
|
22
|
+
* - 两者**默认都不限制**:不传就是"循环到没有工具调用为止 / 不超时"
|
|
23
|
+
* - 想限制才传(这是使用者的选择,不是框架替你做的决定)
|
|
24
|
+
*/
|
|
25
|
+
export function createToolCallingPlugin(options = {}) {
|
|
26
|
+
return {
|
|
27
|
+
name: 'tool-calling',
|
|
28
|
+
// 默认不限制循环轮数(旧版硬编码 5 轮,属于"框架替用户做决定",v4.0 移除)
|
|
29
|
+
maxIterations: options.maxIterations ?? Infinity,
|
|
30
|
+
_options: { ...options },
|
|
31
|
+
_tools: new Map(), // 工具名 -> { executor, description }
|
|
32
|
+
_toolDefs: [], // OpenAI 格式的工具定义(按注册顺序)
|
|
33
|
+
chatService: null,
|
|
29
34
|
|
|
30
35
|
/**
|
|
31
|
-
*
|
|
32
|
-
* @param {
|
|
33
|
-
* @param {
|
|
34
|
-
* @param {Function} executor - 异步执行函数,接收参数对象,返回结果(字符串或对象)
|
|
35
|
-
* @param {Object} parameters - JSON Schema 参数定义(可选,默认为空对象)
|
|
36
|
-
* @returns {ChatService} 返回 chatService 实例,支持链式调用
|
|
36
|
+
* 安装插件:挂 registerTool、注册 beforeRequest 钩子注入工具定义、注册响应处理钩子
|
|
37
|
+
* @param {ChatService} chatService - ChatService 实例
|
|
38
|
+
* @param {Object} [options] - 安装时配置(合并进 _options)
|
|
37
39
|
*/
|
|
38
|
-
chatService
|
|
39
|
-
|
|
40
|
-
this.
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
install(chatService, options = {}) {
|
|
41
|
+
this._options = { ...this._options, ...options };
|
|
42
|
+
this.chatService = chatService;
|
|
43
|
+
|
|
44
|
+
// 每次请求前把工具定义注入到请求配置(v3.0:beforeSend 车间,不再用 _hooks 钩子)
|
|
45
|
+
chatService.pipe({
|
|
46
|
+
name: 'tool-calling-inject',
|
|
47
|
+
phase: 'beforeSend',
|
|
48
|
+
run: ({ config }) => {
|
|
49
|
+
if (this._toolDefs.length) config.tools = this._toolDefs;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 注册工具
|
|
55
|
+
* @param {string} name - 工具名称(唯一标识)
|
|
56
|
+
* @param {string} description - 工具描述(告诉 AI 何时调用)
|
|
57
|
+
* @param {Function} executor - 异步执行函数,接收参数对象,返回结果(字符串或对象)
|
|
58
|
+
* @param {Object} parameters - JSON Schema 参数定义(可选,默认为空对象)
|
|
59
|
+
* @returns {ChatService} 返回 chatService 实例,支持链式调用
|
|
60
|
+
*/
|
|
61
|
+
const registerTool = (name, description, executor, parameters = {}) => {
|
|
62
|
+
// 保存执行器
|
|
63
|
+
this._tools.set(name, { executor, description });
|
|
64
|
+
|
|
65
|
+
// 构建 OpenAI 兼容的工具定义
|
|
66
|
+
// 参数写法:{ 参数名: { type, description, required? } }
|
|
67
|
+
// → 必填项收集到顶层 required 数组;properties 里不残留 required 字段(标准 JSON Schema 要求)
|
|
68
|
+
const properties = {};
|
|
69
|
+
const requiredKeys = [];
|
|
70
|
+
for (const [key, def] of Object.entries(parameters || {})) {
|
|
71
|
+
const { required: isRequired, ...schema } = def || {};
|
|
72
|
+
properties[key] = schema;
|
|
73
|
+
if (isRequired) requiredKeys.push(key);
|
|
74
|
+
}
|
|
75
|
+
const toolDefinition = {
|
|
76
|
+
type: 'function',
|
|
77
|
+
function: {
|
|
78
|
+
name,
|
|
79
|
+
description,
|
|
80
|
+
parameters: { type: 'object', properties, required: requiredKeys }
|
|
52
81
|
}
|
|
82
|
+
};
|
|
83
|
+
// 避免重复添加相同工具
|
|
84
|
+
const existing = this._toolDefs.find(t => t.function.name === name);
|
|
85
|
+
if (!existing) {
|
|
86
|
+
this._toolDefs.push(toolDefinition);
|
|
53
87
|
}
|
|
88
|
+
return chatService;
|
|
54
89
|
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
|
|
91
|
+
// 响应后处理(v3.0:afterSend 车间,不再占用 ChatService._processResponse 单座槽位)
|
|
92
|
+
chatService.pipe({
|
|
93
|
+
name: 'tool-calling-loop',
|
|
94
|
+
phase: 'afterSend',
|
|
95
|
+
run: async (ctx) => {
|
|
96
|
+
ctx.result = await this._handleWithTools(
|
|
97
|
+
ctx.result,
|
|
98
|
+
ctx.isStream,
|
|
99
|
+
ctx.onProgress || undefined,
|
|
100
|
+
ctx.onDone || undefined
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// 交给 use() 挂到 chat.plugins['tool-calling'](v4.0 命名空间)
|
|
106
|
+
return { registerTool, tools: this._tools, uninstall: (c) => this.uninstall(c) };
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/** 卸载:移除注入车间(工具定义注入 + 工具循环) */
|
|
110
|
+
uninstall(chat) {
|
|
111
|
+
if (!chat) return chat;
|
|
112
|
+
chat.unpipe?.('tool-calling-inject');
|
|
113
|
+
chat.unpipe?.('tool-calling-loop');
|
|
114
|
+
if (chat.plugins && chat.plugins['tool-calling']) delete chat.plugins['tool-calling'];
|
|
115
|
+
return chat;
|
|
116
|
+
},
|
|
81
117
|
|
|
82
118
|
/**
|
|
83
|
-
*
|
|
84
|
-
* @param {Object} initialResponse - 初始 AI
|
|
85
|
-
* @
|
|
119
|
+
* 核心处理逻辑:检查响应 → 执行工具 → 继续请求 → 循环
|
|
120
|
+
* @param {Object} initialResponse - 初始 AI 响应
|
|
121
|
+
* @param {boolean} isStream - 是否流式
|
|
122
|
+
* @param {Function} onProgress - 流式进度回调
|
|
123
|
+
* @param {Function} onDone - 流式完成回调
|
|
124
|
+
* @returns {Promise<Object>} 最终 AI 响应
|
|
86
125
|
*/
|
|
87
|
-
async
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
});
|
|
128
|
-
return {
|
|
129
|
-
tool_call_id: call.id,
|
|
130
|
-
error: `工具 ${toolName} 未注册`,
|
|
131
|
-
success: false
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
// 工具执行超时控制
|
|
137
|
-
const toolTimeout = chat.config.toolTimeout;
|
|
138
|
-
let execPromise = tool.executor(args);
|
|
139
|
-
if (toolTimeout && typeof toolTimeout === 'number' && toolTimeout > 0) {
|
|
140
|
-
const timeoutErr = new Error(`工具 ${toolName} 执行超时 (${toolTimeout}ms)`);
|
|
141
|
-
timeoutErr.name = 'ToolTimeoutError';
|
|
142
|
-
const timeoutPromise = new Promise((_, reject) =>
|
|
143
|
-
setTimeout(() => reject(timeoutErr), toolTimeout)
|
|
144
|
-
);
|
|
145
|
-
execPromise = Promise.race([execPromise, timeoutPromise]);
|
|
126
|
+
async _handleWithTools(initialResponse, isStream, onProgress, onDone) {
|
|
127
|
+
const self = this;
|
|
128
|
+
const chat = this.chatService;
|
|
129
|
+
let iteration = 0;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 递归处理工具调用
|
|
133
|
+
* @param {Object} initialResponse - 初始 AI 响应(可能是第一次请求的响应)
|
|
134
|
+
* @returns {Promise<Object>} 最终 AI 响应(不含 tool_calls)
|
|
135
|
+
*/
|
|
136
|
+
async function processResponse(initialResponse) {
|
|
137
|
+
let lastResponse = initialResponse;
|
|
138
|
+
while (iteration < self.maxIterations) {
|
|
139
|
+
// 检查 AI 返回的消息中是否包含工具调用请求
|
|
140
|
+
const toolCalls = lastResponse?.toolCalls;
|
|
141
|
+
if (!toolCalls || toolCalls.length === 0) break;
|
|
142
|
+
|
|
143
|
+
iteration++;
|
|
144
|
+
// 并行执行所有工具调用(彼此独立,不会因一个失败而阻塞其他)
|
|
145
|
+
const toolResults = await Promise.all(
|
|
146
|
+
toolCalls.map(async (call) => {
|
|
147
|
+
const toolName = call.function?.name;
|
|
148
|
+
|
|
149
|
+
// 解析参数(JSON.parse 可能失败,需单独 try/catch)
|
|
150
|
+
let args;
|
|
151
|
+
try {
|
|
152
|
+
args = JSON.parse(call.function?.arguments || '{}');
|
|
153
|
+
} catch (parseErr) {
|
|
154
|
+
chat.emit('tool-error', {
|
|
155
|
+
toolName: toolName || 'unknown',
|
|
156
|
+
error: parseErr,
|
|
157
|
+
toolCallId: call.id,
|
|
158
|
+
stage: 'parse',
|
|
159
|
+
timestamp: Date.now()
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
tool_call_id: call.id,
|
|
163
|
+
error: `参数解析失败: ${parseErr.message}`,
|
|
164
|
+
success: false
|
|
165
|
+
};
|
|
146
166
|
}
|
|
147
167
|
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
toolName,
|
|
164
|
-
error: err,
|
|
165
|
-
toolCallId: call.id,
|
|
166
|
-
stage,
|
|
167
|
-
timestamp: Date.now()
|
|
168
|
-
});
|
|
169
|
-
return {
|
|
170
|
-
tool_call_id: call.id,
|
|
171
|
-
error: err.message,
|
|
172
|
-
success: false
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
})
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
// 将工具执行结果作为 tool 消息添加到对话历史
|
|
179
|
-
for (const tr of toolResults) {
|
|
180
|
-
// 注意:addTool 的第二个参数是 tool_call_id
|
|
181
|
-
// 如果工具执行失败且没有内容,则传递错误信息作为 content
|
|
182
|
-
const content = tr.success ? tr.content : (tr.error || '工具执行失败');
|
|
183
|
-
chat.messages.addTool(content, tr.tool_call_id);
|
|
184
|
-
}
|
|
168
|
+
const tool = self._tools.get(toolName);
|
|
169
|
+
if (!tool) {
|
|
170
|
+
chat.emit('tool-error', {
|
|
171
|
+
toolName,
|
|
172
|
+
error: new Error(`Tool not registered: ${toolName}`),
|
|
173
|
+
toolCallId: call.id,
|
|
174
|
+
stage: 'lookup',
|
|
175
|
+
timestamp: Date.now()
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
tool_call_id: call.id,
|
|
179
|
+
error: `工具 ${toolName} 未注册`,
|
|
180
|
+
success: false
|
|
181
|
+
};
|
|
182
|
+
}
|
|
185
183
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
184
|
+
try {
|
|
185
|
+
// 工具执行超时:只在工厂 options.timeout 显式传入时生效(不传 = 不限制)
|
|
186
|
+
const toolTimeout = self._options.timeout;
|
|
187
|
+
let result;
|
|
188
|
+
if (typeof toolTimeout === 'number' && toolTimeout > 0) {
|
|
189
|
+
const controller = new AbortController(); // 通知支持 signal 的工具去清理
|
|
190
|
+
const timeoutErr = new Error(`工具 ${toolName} 执行超时 (${toolTimeout}ms)`);
|
|
191
|
+
timeoutErr.name = 'ToolTimeoutError';
|
|
192
|
+
let timer;
|
|
193
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
194
|
+
timer = setTimeout(() => { controller.abort(); reject(timeoutErr); }, toolTimeout);
|
|
195
|
+
});
|
|
196
|
+
const execPromise = Promise.resolve().then(() => tool.executor(args, { signal: controller.signal }));
|
|
197
|
+
execPromise.catch(() => {}); // 超时后工具的后续结果不再冒泡为 unhandled
|
|
198
|
+
try {
|
|
199
|
+
result = await Promise.race([execPromise, timeoutPromise]);
|
|
200
|
+
} finally {
|
|
201
|
+
clearTimeout(timer); // 工具先完成时不留悬挂的 timer(旧版泄漏点)
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
result = await tool.executor(args);
|
|
205
|
+
}
|
|
206
|
+
chat.emit('tool-success', {
|
|
207
|
+
toolName,
|
|
208
|
+
result,
|
|
209
|
+
toolCallId: call.id,
|
|
210
|
+
timestamp: Date.now()
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
tool_call_id: call.id,
|
|
214
|
+
content: typeof result === 'string' ? result : JSON.stringify(result),
|
|
215
|
+
success: true
|
|
216
|
+
};
|
|
217
|
+
} catch (err) {
|
|
218
|
+
const stage = err.name === 'ToolTimeoutError' ? 'timeout' : 'execute';
|
|
219
|
+
chat.emit('tool-error', {
|
|
220
|
+
toolName,
|
|
221
|
+
error: err,
|
|
222
|
+
toolCallId: call.id,
|
|
223
|
+
stage,
|
|
224
|
+
timestamp: Date.now()
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
tool_call_id: call.id,
|
|
228
|
+
error: err.message,
|
|
229
|
+
success: false
|
|
230
|
+
};
|
|
198
231
|
}
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// 将工具执行结果作为 tool 消息添加到对话历史
|
|
236
|
+
for (const tr of toolResults) {
|
|
237
|
+
// 注意:addTool 的第二个参数是 tool_call_id
|
|
238
|
+
// 如果工具执行失败且没有内容,则传递错误信息作为 content
|
|
239
|
+
const content = tr.success ? tr.content : (tr.error || '工具执行失败');
|
|
240
|
+
chat.messages.addTool(content, tr.tool_call_id);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 再次请求 AI,获取基于工具结果的回复
|
|
244
|
+
if (isStream) {
|
|
245
|
+
// 流式继续:使用 sendExistingStream
|
|
246
|
+
await new Promise((resolve) => {
|
|
247
|
+
chat.sendExistingStream(
|
|
248
|
+
(chunk) => {
|
|
249
|
+
if (onProgress) onProgress(chunk);
|
|
250
|
+
lastResponse = chunk;
|
|
251
|
+
},
|
|
252
|
+
(final) => {
|
|
253
|
+
lastResponse = final;
|
|
254
|
+
resolve();
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
// 非流式继续:使用 sendExisting
|
|
260
|
+
lastResponse = await chat.sendExisting();
|
|
261
|
+
}
|
|
204
262
|
}
|
|
263
|
+
return lastResponse;
|
|
205
264
|
}
|
|
206
|
-
return lastResponse;
|
|
207
|
-
}
|
|
208
265
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
};
|
|
266
|
+
// 初始响应已由 ChatService 发出,直接进入工具调用循环
|
|
267
|
+
const final = await processResponse(initialResponse);
|
|
268
|
+
return final;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|