my-ai-chat-framework 2.0.0 → 3.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 +65 -0
- package/LICENSE +20 -20
- package/README.md +329 -45
- package/README_ZH.md +375 -0
- package/dist/my-ai-chat-framework.browser.es.js +1581 -207
- package/dist/my-ai-chat-framework.browser.es.js.map +1 -1
- package/dist/my-ai-chat-framework.browser.umd.js +1594 -211
- package/dist/my-ai-chat-framework.browser.umd.js.map +1 -1
- package/dist/my-ai-chat-framework.node.cjs.js +1594 -211
- package/dist/my-ai-chat-framework.node.cjs.js.map +1 -1
- package/docs/DEVELOPER.md +479 -0
- package/package.json +26 -3
- package/src/adapters/openai.js +289 -64
- package/src/core/ChatService.js +652 -110
- package/src/core/Errors.js +60 -0
- package/src/core/EventEmitter.js +19 -0
- package/src/core/MessageStore.js +66 -0
- package/src/core/Pipeline.js +48 -0
- package/src/core/SystemPromptStore.js +118 -0
- package/src/index.js +12 -14
- package/src/plugins/model-registry.js +223 -0
- package/src/plugins/tool-calling.js +229 -105
- package/src/utils/MessageFormatter.js +204 -0
- package/src/utils/typeCheck.js +12 -0
- package/src/utils/url.js +18 -0
- package/.env +0 -15
- package/test.js +0 -107
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//#region src/core/EventEmitter.js
|
|
2
2
|
/**
|
|
3
3
|
* 极简事件发射器
|
|
4
|
+
* 支持同步/异步事件,handler 返回 Promise 时自动 await
|
|
4
5
|
*/
|
|
5
6
|
var EventEmitter = class {
|
|
6
7
|
constructor() {
|
|
@@ -17,6 +18,9 @@ var EventEmitter = class {
|
|
|
17
18
|
if (handlers.length) this._events.set(event, handlers);
|
|
18
19
|
else this._events.delete(event);
|
|
19
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* 同步触发事件
|
|
23
|
+
*/
|
|
20
24
|
emit(event, data) {
|
|
21
25
|
if (!this._events.has(event)) return;
|
|
22
26
|
for (const handler of this._events.get(event)) try {
|
|
@@ -25,6 +29,18 @@ var EventEmitter = class {
|
|
|
25
29
|
console.error(`事件 ${event} 处理出错:`, err);
|
|
26
30
|
}
|
|
27
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* 异步触发事件,逐个 await handler
|
|
34
|
+
* handler 返回 Promise 时自动等待
|
|
35
|
+
*/
|
|
36
|
+
async emitAsync(event, data) {
|
|
37
|
+
if (!this._events.has(event)) return;
|
|
38
|
+
for (const handler of this._events.get(event)) try {
|
|
39
|
+
await handler(data);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(`事件 ${event} 处理出错:`, err);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
28
44
|
};
|
|
29
45
|
//#endregion
|
|
30
46
|
//#region src/core/MessageStore.js
|
|
@@ -35,6 +51,10 @@ var EventEmitter = class {
|
|
|
35
51
|
* id?: string,
|
|
36
52
|
* role: 'user'|'assistant'|'system'|'tool',
|
|
37
53
|
* content: string,
|
|
54
|
+
* images?: Array<string>, // 图片引用(URL / id),框架不存储图片数据
|
|
55
|
+
* _ephemeral?: boolean, // 临时消息:仅底部连续时参与请求,续写后转正
|
|
56
|
+
* _complete?: boolean, // 流式是否完成(false=未完成/中断)
|
|
57
|
+
* prefix?: boolean, // 前缀续写标记
|
|
38
58
|
* toolCalls?: Array,
|
|
39
59
|
* toolCallId?: string,
|
|
40
60
|
* timestamp?: number,
|
|
@@ -84,6 +104,22 @@ var MessageStore = class {
|
|
|
84
104
|
metadata
|
|
85
105
|
});
|
|
86
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* 快速添加一次性的、带续写标记的 assistant 消息
|
|
109
|
+
* 适用于思维链引导、临时注入等场景
|
|
110
|
+
* @param {string} content — 引导内容
|
|
111
|
+
* @param {Object} [options] — { reasoningContent, prefix }
|
|
112
|
+
* @returns {Object} 添加的消息
|
|
113
|
+
*/
|
|
114
|
+
addOnceAssistant(content, options = {}) {
|
|
115
|
+
return this.add({
|
|
116
|
+
role: "assistant",
|
|
117
|
+
content,
|
|
118
|
+
reasoningContent: options.reasoningContent,
|
|
119
|
+
prefix: options.prefix !== false,
|
|
120
|
+
_ephemeral: true
|
|
121
|
+
});
|
|
122
|
+
}
|
|
87
123
|
getLast() {
|
|
88
124
|
return this._messages[this._messages.length - 1] || null;
|
|
89
125
|
}
|
|
@@ -102,151 +138,1172 @@ var MessageStore = class {
|
|
|
102
138
|
if (lastAssistantIndex >= 0) return this._messages.splice(lastAssistantIndex + 1);
|
|
103
139
|
return [];
|
|
104
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* 撤回到指定消息 id 的上一个用户消息,删除两者之间的所有消息
|
|
143
|
+
* 适用于重发
|
|
144
|
+
* @param {string} id — 目标消息 id
|
|
145
|
+
* @returns {Array} 被删除的消息列表
|
|
146
|
+
*/
|
|
147
|
+
undoToPreviousUser(id) {
|
|
148
|
+
let targetIndex = -1;
|
|
149
|
+
let previousUserIndex = -1;
|
|
150
|
+
for (let i = this._messages.length - 1; i >= 0; i--) {
|
|
151
|
+
if (this._messages[i].id === id) targetIndex = i;
|
|
152
|
+
if (targetIndex >= 0 && this._messages[i].role === "user") {
|
|
153
|
+
previousUserIndex = i;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (targetIndex >= 0 && previousUserIndex >= 0) return this._messages.splice(previousUserIndex + 1, targetIndex - previousUserIndex);
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 更新消息:按 id 查找并合并 changes,不新增消息
|
|
162
|
+
* @param {string} id — 消息 id
|
|
163
|
+
* @param {Object} changes — 要合并的字段
|
|
164
|
+
* @returns {Object|null} 更新后的消息,未找到返回 null
|
|
165
|
+
*/
|
|
166
|
+
update(id, changes) {
|
|
167
|
+
for (const msg of this._messages) if (msg.id === id) {
|
|
168
|
+
Object.assign(msg, changes);
|
|
169
|
+
msg.timestamp = changes.timestamp || Date.now();
|
|
170
|
+
return msg;
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/core/SystemPromptStore.js
|
|
177
|
+
/**
|
|
178
|
+
* SystemPromptStore — 系统提示词存储
|
|
179
|
+
*
|
|
180
|
+
* 职责:管理多条 system prompt 的增删改查与开关
|
|
181
|
+
* 与 MessageStore 分离,因为 system prompt 是"AI 行为准则",不是对话事件
|
|
182
|
+
*
|
|
183
|
+
* 每条记录格式:
|
|
184
|
+
* {
|
|
185
|
+
* id: string,
|
|
186
|
+
* content: string,
|
|
187
|
+
* enabled: boolean,
|
|
188
|
+
* timestamp: number
|
|
189
|
+
* }
|
|
190
|
+
*/
|
|
191
|
+
var SystemPromptStore = class {
|
|
192
|
+
constructor() {
|
|
193
|
+
this._prompts = [];
|
|
194
|
+
this._idCounter = 0;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 添加一条 system prompt
|
|
198
|
+
* @param {string} content — 提示词内容
|
|
199
|
+
* @param {boolean} [enabled=true] — 是否启用
|
|
200
|
+
* @returns {Object} 添加的记录
|
|
201
|
+
*/
|
|
202
|
+
add(content, enabled = true) {
|
|
203
|
+
if (!content || typeof content !== "string" || !content.trim()) throw new Error("[SystemPromptStore] content 必须是非空字符串");
|
|
204
|
+
const record = {
|
|
205
|
+
id: `sys_${Date.now()}_${++this._idCounter}`,
|
|
206
|
+
content: content.trim(),
|
|
207
|
+
enabled: Boolean(enabled),
|
|
208
|
+
timestamp: Date.now()
|
|
209
|
+
};
|
|
210
|
+
this._prompts.push(record);
|
|
211
|
+
return record;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 删除指定索引的 system prompt
|
|
215
|
+
* @param {number} index
|
|
216
|
+
* @returns {Object} 被删除的记录
|
|
217
|
+
*/
|
|
218
|
+
remove(index) {
|
|
219
|
+
if (index < 0 || index >= this._prompts.length) throw new Error(`[SystemPromptStore] 索引越界: ${index}`);
|
|
220
|
+
return this._prompts.splice(index, 1)[0];
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* 切换指定索引的启用/禁用状态
|
|
224
|
+
* @param {number} index
|
|
225
|
+
* @returns {boolean} 切换后的状态
|
|
226
|
+
*/
|
|
227
|
+
toggle(index) {
|
|
228
|
+
if (index < 0 || index >= this._prompts.length) throw new Error(`[SystemPromptStore] 索引越界: ${index}`);
|
|
229
|
+
this._prompts[index].enabled = !this._prompts[index].enabled;
|
|
230
|
+
return this._prompts[index].enabled;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* 更新指定索引的 content
|
|
234
|
+
* @param {number} index
|
|
235
|
+
* @param {string} content
|
|
236
|
+
*/
|
|
237
|
+
update(index, content) {
|
|
238
|
+
if (index < 0 || index >= this._prompts.length) throw new Error(`[SystemPromptStore] 索引越界: ${index}`);
|
|
239
|
+
if (!content || typeof content !== "string" || !content.trim()) throw new Error("[SystemPromptStore] content 必须是非空字符串");
|
|
240
|
+
this._prompts[index].content = content.trim();
|
|
241
|
+
this._prompts[index].timestamp = Date.now();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* 清空并用一条 content 替换(便捷方法,常用于 config.system = '...')
|
|
245
|
+
* @param {string} content
|
|
246
|
+
*/
|
|
247
|
+
set(content) {
|
|
248
|
+
this._prompts = [];
|
|
249
|
+
if (content && typeof content === "string" && content.trim()) this.add(content, true);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* 按 enabled 筛选后,转为适配器可用的格式
|
|
253
|
+
* @returns {Array<{role:'system', content:string}>}
|
|
254
|
+
*/
|
|
255
|
+
getEnabled() {
|
|
256
|
+
return this._prompts.filter((p) => p.enabled).map((p) => ({
|
|
257
|
+
role: "system",
|
|
258
|
+
content: p.content
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 返回所有记录(含 enabled 状态,用于 UI 展示)
|
|
263
|
+
* @returns {Array}
|
|
264
|
+
*/
|
|
265
|
+
getAll() {
|
|
266
|
+
return [...this._prompts];
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* 清空所有 system prompt
|
|
270
|
+
*/
|
|
271
|
+
clear() {
|
|
272
|
+
this._prompts = [];
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/core/Errors.js
|
|
277
|
+
/**
|
|
278
|
+
* 自定义错误类
|
|
279
|
+
* 用于区分不同类型的错误,方便用户通过 `error.name` 或 `instanceof` 处理
|
|
280
|
+
*/
|
|
281
|
+
var APIError = class extends Error {
|
|
282
|
+
/**
|
|
283
|
+
* @param {string} message - 错误消息(通常来自 API 响应)
|
|
284
|
+
* @param {number} statusCode - HTTP 状态码
|
|
285
|
+
* @param {any} originalError - 原始错误对象或相关信息
|
|
286
|
+
* @param {string} responseText - 原始响应文本(如果有)
|
|
287
|
+
*/
|
|
288
|
+
constructor(message, statusCode, originalError, responseText) {
|
|
289
|
+
super(message);
|
|
290
|
+
this.name = "APIError";
|
|
291
|
+
this.statusCode = statusCode;
|
|
292
|
+
this.originalError = originalError;
|
|
293
|
+
this.responseText = responseText;
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
var NetworkError = class extends Error {
|
|
297
|
+
/**
|
|
298
|
+
* @param {string} message - 错误消息
|
|
299
|
+
* @param {any} originalError - 原始错误对象或相关信息
|
|
300
|
+
*/
|
|
301
|
+
constructor(message, originalError) {
|
|
302
|
+
super(message);
|
|
303
|
+
this.name = "NetworkError";
|
|
304
|
+
this.originalError = originalError;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
var ConfigurationError = class extends Error {
|
|
308
|
+
/**
|
|
309
|
+
* @param {string} message - 错误消息
|
|
310
|
+
*/
|
|
311
|
+
constructor(message) {
|
|
312
|
+
super(message);
|
|
313
|
+
this.name = "ConfigurationError";
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
var ParsingError = class extends Error {
|
|
317
|
+
/**
|
|
318
|
+
* @param {string} message - 错误消息
|
|
319
|
+
* @param {any} originalError - 原始错误对象或相关信息
|
|
320
|
+
* @param {string} responseText - 原始响应文本(如果有)
|
|
321
|
+
**/
|
|
322
|
+
constructor(message, originalError, responseText) {
|
|
323
|
+
super(message);
|
|
324
|
+
this.name = "ParsingError";
|
|
325
|
+
this.originalError = originalError;
|
|
326
|
+
this.responseText = responseText;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/core/Pipeline.js
|
|
331
|
+
/**
|
|
332
|
+
* Pipeline —— 极简顺序管道(v3.0 提案 · 阶段 1)
|
|
333
|
+
*
|
|
334
|
+
* 三个概念(对照 pipeline-demo.html):
|
|
335
|
+
* - 小车 ctx —— 本次请求的全部"状态",车间之间只通过它交接
|
|
336
|
+
* - 车间 stage —— { name, run(ctx) },只干一件事,不认别的车间
|
|
337
|
+
* - 调度 run() —— for + await,顺序执行(线性,无"回程";洋葱能力留给阶段 2 的 after 车间)
|
|
338
|
+
*/
|
|
339
|
+
var Pipeline = class {
|
|
340
|
+
constructor() {
|
|
341
|
+
this._stages = [];
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* 注册一个车间(追加到末尾)。
|
|
345
|
+
* @param {{name: string, run: (ctx: object) => void | Promise<void>}} stage
|
|
346
|
+
* @returns {Pipeline} this,支持链式
|
|
347
|
+
*/
|
|
348
|
+
register(stage) {
|
|
349
|
+
if (!stage || typeof stage.run !== "function") throw new TypeError("Pipeline.register: stage 需要 { name, run(ctx) },运行 run 必须是函数");
|
|
350
|
+
this._stages.push(stage);
|
|
351
|
+
return this;
|
|
352
|
+
}
|
|
353
|
+
/** 按名字移除车间(移除不存在的名字是安全的) */
|
|
354
|
+
unregister(name) {
|
|
355
|
+
this._stages = this._stages.filter((s) => s.name !== name);
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
/** 列出当前车间名(调试用) */
|
|
359
|
+
names() {
|
|
360
|
+
return this._stages.map((s) => s.name);
|
|
361
|
+
}
|
|
362
|
+
/** 把小车开过所有车间;返回 ctx(车间可改 ctx 上的字段) */
|
|
363
|
+
async run(ctx) {
|
|
364
|
+
for (const stage of this._stages) await stage.run(ctx);
|
|
365
|
+
return ctx;
|
|
366
|
+
}
|
|
105
367
|
};
|
|
106
368
|
//#endregion
|
|
107
369
|
//#region src/core/ChatService.js
|
|
108
370
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
371
|
+
* @typedef {Object} ChatAdapter 适配器协议(v3.0:写同级适配器只需实现这 4+1 个方法)
|
|
372
|
+
* @property {Function} buildRequest (messages, config, systemPrompts) => requestBody
|
|
373
|
+
* @property {Function} send (body, config, opts) => Promise<apiResponse>
|
|
374
|
+
* @property {Function} stream (body, config, onProgress, onDone, opts) => Promise<void>
|
|
375
|
+
* @property {Function} parseResponse (apiResponse) => assistantMessage
|
|
376
|
+
* @property {Function} [getRequestDefaults] () => 默认请求参数(可选)
|
|
377
|
+
* @property {Function} [install] (chatService, options) 插件式安装(可选)
|
|
378
|
+
*/
|
|
379
|
+
/**
|
|
380
|
+
* @typedef {Object} PipelineContext 管道小车 ctx(public pipe 车间可见/可改的全部字段)
|
|
381
|
+
* @property {Object} options 原始请求选项
|
|
382
|
+
* @property {*} userInput 用户输入(可为 undefined)
|
|
383
|
+
* @property {boolean} addUser 是否已新增/将新增用户消息
|
|
384
|
+
* @property {MessageStore} messages 消息列表(可读可改:push / unshift / update)
|
|
385
|
+
* @property {SystemPromptStore} systemPrompts 系统提示词(可读可改)
|
|
386
|
+
* @property {Object} config 合并后的请求级配置(prepareInput 后生效)
|
|
387
|
+
* @property {boolean} isStream 本次是否流式
|
|
388
|
+
* @property {Function|null} onProgress 流式进度回调
|
|
389
|
+
* @property {Function|null} onDone 流式完成回调
|
|
390
|
+
* @property {string|null} mergeToEntry 续写目标 id(null = 普通发送)
|
|
391
|
+
* @property {*} body 请求体(buildRequest 后产出)
|
|
392
|
+
* @property {*} result 最终结果(send 后产出)
|
|
111
393
|
*/
|
|
394
|
+
/**
|
|
395
|
+
* @typedef {Object} PipeStage 车间(v3.0 公开扩展单元)
|
|
396
|
+
* @property {string} name 车间名(唯一,不能与内部车间重名)
|
|
397
|
+
* @property {string} [phase] 'beforeSend'(默认)| 'afterSend'
|
|
398
|
+
* @property {Function} run (ctx: PipelineContext) => void | Promise<void>
|
|
399
|
+
*/
|
|
400
|
+
var SESSION_CONFIG_KEYS = [
|
|
401
|
+
"model",
|
|
402
|
+
"temperature",
|
|
403
|
+
"maxTokens",
|
|
404
|
+
"modelParams",
|
|
405
|
+
"system",
|
|
406
|
+
"retry",
|
|
407
|
+
"ephemeralContinue"
|
|
408
|
+
];
|
|
112
409
|
var ChatService = class extends EventEmitter {
|
|
113
410
|
constructor(config = {}) {
|
|
114
411
|
super();
|
|
115
|
-
this.config = {
|
|
116
|
-
apiKey: config.apiKey || "",
|
|
117
|
-
model: config.model || "gpt-3.5-turbo",
|
|
118
|
-
...config
|
|
119
|
-
};
|
|
412
|
+
this.config = { ...config };
|
|
120
413
|
this.messages = new MessageStore();
|
|
121
|
-
this.
|
|
414
|
+
this.systemPrompts = new SystemPromptStore();
|
|
122
415
|
this._adapter = null;
|
|
123
|
-
this.
|
|
416
|
+
this._abortController = null;
|
|
417
|
+
this._isGenerating = false;
|
|
418
|
+
this._hooks = new EventEmitter();
|
|
419
|
+
this._processResponse = null;
|
|
420
|
+
const model = this.config.model || this.config.modelParams?.model;
|
|
421
|
+
if (!model || typeof model !== "string" || !model.trim()) throw new ConfigurationError("缺少 model 配置");
|
|
422
|
+
this._assertValidConfig(this.config);
|
|
423
|
+
if (typeof config.system === "string" && config.system.trim()) this.systemPrompts.set(config.system);
|
|
424
|
+
if (config.adapter) this.setAdapter(config.adapter);
|
|
425
|
+
this._userStages = [];
|
|
426
|
+
this._pipeline = this._buildPipeline();
|
|
124
427
|
}
|
|
125
428
|
/**
|
|
126
|
-
*
|
|
127
|
-
* @
|
|
429
|
+
* 校验配置中的可校验字段(model 必填由构造器负责,这里只校验存在性)
|
|
430
|
+
* @throws {ConfigurationError}
|
|
128
431
|
*/
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
432
|
+
_assertValidConfig(cfg) {
|
|
433
|
+
const temperature = cfg.modelParams?.temperature ?? cfg.temperature;
|
|
434
|
+
if (temperature !== void 0 && (typeof temperature !== "number" || temperature < 0 || temperature > 2)) throw new ConfigurationError(`temperature 必须在 0-2 之间,当前值: ${temperature}`);
|
|
435
|
+
const maxTokens = cfg.modelParams?.maxTokens ?? cfg.maxTokens;
|
|
436
|
+
if (maxTokens !== void 0 && (typeof maxTokens !== "number" || maxTokens < 1 || !Number.isInteger(maxTokens))) throw new ConfigurationError(`maxTokens 必须为正整数,当前值: ${maxTokens}`);
|
|
437
|
+
const model = cfg.model ?? cfg.modelParams?.model;
|
|
438
|
+
if (model !== void 0 && (typeof model !== "string" || !model.trim())) throw new ConfigurationError("model 必须是非空字符串");
|
|
439
|
+
}
|
|
440
|
+
use(plugin, options = {}) {
|
|
441
|
+
plugin.install(this, options);
|
|
134
442
|
return this;
|
|
135
443
|
}
|
|
136
444
|
/**
|
|
137
|
-
*
|
|
445
|
+
* 往请求管道里挂一个"车间"。
|
|
446
|
+
* - phase 'beforeSend'(默认):在内部 beforeRequest 钩子之后、构建请求体之前执行
|
|
447
|
+
* - phase 'afterSend' :在内部发送(流式/非流式)完成、结果落位之后执行(可改 ctx.result)
|
|
448
|
+
* 不注册任何车间 = 行为与版本 2.8.x 完全一致。
|
|
449
|
+
* @param {PipeStage} stage
|
|
450
|
+
* @returns {ChatService} this
|
|
451
|
+
* @throws {ConfigurationError} 参数非法 / 名字被占用 / 与内部车间重名
|
|
138
452
|
*/
|
|
453
|
+
pipe(stage) {
|
|
454
|
+
if (!stage || typeof stage !== "object" || typeof stage.run !== "function") throw new ConfigurationError("pipe: 需要 { name, phase?, run(ctx) },run 必须是函数");
|
|
455
|
+
if (typeof stage.name !== "string" || !stage.name.trim()) throw new ConfigurationError("pipe: 需要 name(车间名,非空字符串)");
|
|
456
|
+
if ([
|
|
457
|
+
"prepareInput",
|
|
458
|
+
"beforeSend",
|
|
459
|
+
"autoContinue",
|
|
460
|
+
"buildRequest",
|
|
461
|
+
"send"
|
|
462
|
+
].includes(stage.name)) throw new ConfigurationError(`pipe: "${stage.name}" 是内部车间名,请换一个名字`);
|
|
463
|
+
if (this._userStages.some((s) => s.name === stage.name)) throw new ConfigurationError(`pipe: 车间 "${stage.name}" 已存在,请先 chat.unpipe("${stage.name}")`);
|
|
464
|
+
const phase = stage.phase === "afterSend" ? "afterSend" : "beforeSend";
|
|
465
|
+
this._userStages.push({
|
|
466
|
+
name: stage.name,
|
|
467
|
+
phase,
|
|
468
|
+
run: stage.run
|
|
469
|
+
});
|
|
470
|
+
this._rebuildPipeline();
|
|
471
|
+
return this;
|
|
472
|
+
}
|
|
473
|
+
/** 移除一个用户车间(移除不存在的名字是安全的)。@returns {ChatService} this */
|
|
474
|
+
unpipe(name) {
|
|
475
|
+
const before = this._userStages.length;
|
|
476
|
+
this._userStages = this._userStages.filter((s) => s.name !== name);
|
|
477
|
+
if (this._userStages.length !== before) this._rebuildPipeline();
|
|
478
|
+
return this;
|
|
479
|
+
}
|
|
480
|
+
/** 查看当前管道里所有车间名(调试/说明用,含内部车间) */
|
|
481
|
+
get pipelineStages() {
|
|
482
|
+
return this._pipeline.names();
|
|
483
|
+
}
|
|
484
|
+
/** 重建内部管道:内部 5 车间 + 用户车间按 phase 插入 */
|
|
485
|
+
_buildPipeline() {
|
|
486
|
+
const p = new Pipeline();
|
|
487
|
+
p.register({
|
|
488
|
+
name: "prepareInput",
|
|
489
|
+
run: (ctx) => this._stagePrepareInput(ctx)
|
|
490
|
+
});
|
|
491
|
+
p.register({
|
|
492
|
+
name: "beforeSend",
|
|
493
|
+
run: (ctx) => this._stageBeforeSend(ctx)
|
|
494
|
+
});
|
|
495
|
+
for (const s of this._userStages) if (s.phase === "beforeSend") p.register({
|
|
496
|
+
name: s.name,
|
|
497
|
+
run: (ctx) => s.run(ctx)
|
|
498
|
+
});
|
|
499
|
+
p.register({
|
|
500
|
+
name: "autoContinue",
|
|
501
|
+
run: (ctx) => this._stageAutoContinue(ctx)
|
|
502
|
+
});
|
|
503
|
+
p.register({
|
|
504
|
+
name: "buildRequest",
|
|
505
|
+
run: (ctx) => this._stageBuildRequest(ctx)
|
|
506
|
+
});
|
|
507
|
+
p.register({
|
|
508
|
+
name: "send",
|
|
509
|
+
run: (ctx) => this._stageSend(ctx)
|
|
510
|
+
});
|
|
511
|
+
for (const s of this._userStages) if (s.phase === "afterSend") p.register({
|
|
512
|
+
name: s.name,
|
|
513
|
+
run: (ctx) => s.run(ctx)
|
|
514
|
+
});
|
|
515
|
+
return p;
|
|
516
|
+
}
|
|
517
|
+
_rebuildPipeline() {
|
|
518
|
+
this._pipeline = this._buildPipeline();
|
|
519
|
+
}
|
|
139
520
|
setAdapter(adapter) {
|
|
521
|
+
assertAdapter(adapter);
|
|
140
522
|
this._adapter = adapter;
|
|
141
|
-
|
|
523
|
+
}
|
|
524
|
+
abort() {
|
|
525
|
+
if (this._abortController) this._abortController.abort();
|
|
526
|
+
}
|
|
527
|
+
get isGenerating() {
|
|
528
|
+
return this._isGenerating;
|
|
529
|
+
}
|
|
530
|
+
async continueLast() {
|
|
531
|
+
const target = this._prepareContinue();
|
|
532
|
+
this.messages.update(target.id, { prefix: true });
|
|
533
|
+
try {
|
|
534
|
+
await this._request({
|
|
535
|
+
addUser: false,
|
|
536
|
+
isStream: false,
|
|
537
|
+
mergeToEntry: target.id
|
|
538
|
+
});
|
|
539
|
+
this.messages.update(target.id, {
|
|
540
|
+
_complete: true,
|
|
541
|
+
prefix: void 0,
|
|
542
|
+
_ephemeral: false
|
|
543
|
+
});
|
|
544
|
+
return target;
|
|
545
|
+
} catch (err) {
|
|
546
|
+
this.messages.update(target.id, {
|
|
547
|
+
_complete: true,
|
|
548
|
+
prefix: void 0,
|
|
549
|
+
_ephemeral: false
|
|
550
|
+
});
|
|
551
|
+
if (err.name === "AbortError") {
|
|
552
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
553
|
+
return target;
|
|
554
|
+
}
|
|
555
|
+
throw err;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async continueLastStream(onProgress, onDone) {
|
|
559
|
+
const target = this._prepareContinue();
|
|
560
|
+
this.messages.update(target.id, { prefix: true });
|
|
561
|
+
const baseLen = (target.content || "").length;
|
|
562
|
+
try {
|
|
563
|
+
await this._request({
|
|
564
|
+
addUser: false,
|
|
565
|
+
isStream: true,
|
|
566
|
+
mergeToEntry: target.id,
|
|
567
|
+
onProgress: (chunk) => {
|
|
568
|
+
if (onProgress) onProgress({
|
|
569
|
+
...chunk,
|
|
570
|
+
content: (chunk.content || "").slice(baseLen)
|
|
571
|
+
});
|
|
572
|
+
},
|
|
573
|
+
onDone: (final) => {
|
|
574
|
+
if (onDone) onDone(final);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
this.messages.update(target.id, {
|
|
578
|
+
_complete: true,
|
|
579
|
+
prefix: void 0,
|
|
580
|
+
_ephemeral: false
|
|
581
|
+
});
|
|
582
|
+
return target;
|
|
583
|
+
} catch (err) {
|
|
584
|
+
this.messages.update(target.id, {
|
|
585
|
+
_complete: true,
|
|
586
|
+
prefix: void 0,
|
|
587
|
+
_ephemeral: false
|
|
588
|
+
});
|
|
589
|
+
if (err.name === "AbortError") {
|
|
590
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
591
|
+
return target;
|
|
592
|
+
}
|
|
593
|
+
throw err;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
_prepareContinue() {
|
|
597
|
+
const msgs = this.messages.getAll();
|
|
598
|
+
const last = msgs[msgs.length - 1];
|
|
599
|
+
if (last && (last.role === "assistant" || last._ephemeral)) return last;
|
|
600
|
+
throw new Error("最后一条消息不是 assistant,无法续写");
|
|
142
601
|
}
|
|
143
602
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
603
|
+
* 运行时修改配置(仅会话层字段)。
|
|
604
|
+
*
|
|
605
|
+
* 白名单:model / temperature / maxTokens / modelParams / system / retry / ephemeralContinue。
|
|
606
|
+
* - 运输层(apiKey/baseUrl/headers):归适配器,要改就换适配器实例
|
|
607
|
+
* - 请求参数默认值:走适配器工厂 options 或请求级覆盖(chat.send(x, params))
|
|
608
|
+
* - 能力表:归 modelRegistry 插件
|
|
609
|
+
*
|
|
610
|
+
* 与构造器一样经过校验,不能注入非法值。
|
|
611
|
+
* @throws {ConfigurationError} 非白名单字段或非法值
|
|
146
612
|
*/
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
613
|
+
updateConfig(partial) {
|
|
614
|
+
for (const key of Object.keys(partial)) if (!SESSION_CONFIG_KEYS.includes(key)) throw new ConfigurationError(`updateConfig 不支持修改 "${key}"。允许的会话层字段: ${SESSION_CONFIG_KEYS.join(", ")}`);
|
|
615
|
+
this._assertValidConfig(partial);
|
|
616
|
+
Object.assign(this.config, partial);
|
|
617
|
+
if (typeof partial.system === "string") this.systemPrompts.set(partial.system);
|
|
618
|
+
this.emit("config-updated", {
|
|
619
|
+
changes: partial,
|
|
620
|
+
timestamp: Date.now()
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* 合并请求参数(近者优先):
|
|
625
|
+
* 适配器默认(getRequestDefaults) ← 会话配置(this.config) ← 本次覆盖(params)
|
|
626
|
+
* modelParams 深层合并;平铺便捷键(temperature/maxTokens/reasoningEffort)自动折叠进 modelParams
|
|
627
|
+
*/
|
|
628
|
+
_mergeRequestConfig(params = {}) {
|
|
629
|
+
const adapterDefaults = this._adapter?.getRequestDefaults?.() || {};
|
|
630
|
+
const requestConfig = {
|
|
631
|
+
...adapterDefaults,
|
|
632
|
+
...this.config,
|
|
633
|
+
...params
|
|
154
634
|
};
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
635
|
+
const modelParams = {
|
|
636
|
+
...adapterDefaults.modelParams || {},
|
|
637
|
+
...this.config.modelParams || {},
|
|
638
|
+
...params.modelParams || {}
|
|
639
|
+
};
|
|
640
|
+
for (const key of [
|
|
641
|
+
"temperature",
|
|
642
|
+
"maxTokens",
|
|
643
|
+
"reasoningEffort"
|
|
644
|
+
]) {
|
|
645
|
+
const value = params[key] ?? this.config[key] ?? adapterDefaults[key];
|
|
646
|
+
if (value !== void 0) modelParams[key] = value;
|
|
647
|
+
}
|
|
648
|
+
requestConfig.modelParams = modelParams;
|
|
649
|
+
return requestConfig;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* 内部调度(v3.0 提案 · 阶段 1):造一辆小车 ctx,开过内部管道,返回结果。
|
|
653
|
+
* 行为与旧版 _request 完全一致,只是把固定线拆成了车间(见 _stage* 方法)。
|
|
654
|
+
*
|
|
655
|
+
* ctx 字段(车间可见):
|
|
656
|
+
* options —— 原始请求选项
|
|
657
|
+
* userInput —— 用户输入
|
|
658
|
+
* addUser —— 是否新增用户消息
|
|
659
|
+
* messages —— MessageStore(可读可改)
|
|
660
|
+
* systemPrompts —— SystemPromptStore
|
|
661
|
+
* config —— 合并后的请求级配置(prepareInput 车间产出)
|
|
662
|
+
* isStream —— 本次是否流式
|
|
663
|
+
* onProgress —— 流式进度回调(可为 null)
|
|
664
|
+
* onDone —— 流式完成回调(可为 null)
|
|
665
|
+
* mergeToEntry —— 续写目标 id(空 = 普通发送)
|
|
666
|
+
* body —— adapter 构建出的请求体(buildRequest 车间产出)
|
|
667
|
+
* result —— 最终结果(send 车间产出)
|
|
668
|
+
*/
|
|
669
|
+
async _request(options = {}) {
|
|
670
|
+
const ctx = {
|
|
671
|
+
options,
|
|
672
|
+
userInput: options.userInput,
|
|
673
|
+
addUser: options.addUser !== false,
|
|
674
|
+
messages: this.messages,
|
|
675
|
+
systemPrompts: this.systemPrompts,
|
|
676
|
+
config: null,
|
|
677
|
+
isStream: !!options.isStream,
|
|
678
|
+
onProgress: options.onProgress || null,
|
|
679
|
+
onDone: options.onDone || null,
|
|
680
|
+
mergeToEntry: options.mergeToEntry || null,
|
|
681
|
+
body: null,
|
|
682
|
+
result: null
|
|
683
|
+
};
|
|
684
|
+
try {
|
|
685
|
+
await this._pipeline.run(ctx);
|
|
686
|
+
return ctx.result;
|
|
687
|
+
} catch (error) {
|
|
688
|
+
if (error.name === "AbortError") throw error;
|
|
689
|
+
this.emit("error", {
|
|
690
|
+
error,
|
|
691
|
+
timestamp: Date.now()
|
|
692
|
+
});
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/** 车间 1:加用户消息 → 合并请求参数 → 广播 sending */
|
|
697
|
+
async _stagePrepareInput(ctx) {
|
|
698
|
+
this._addUserMessage(ctx.userInput, ctx.addUser);
|
|
699
|
+
ctx.config = this._mergeRequestConfig(ctx.options.params);
|
|
700
|
+
this.emit("sending", {
|
|
701
|
+
addUser: ctx.addUser,
|
|
702
|
+
userInput: ctx.userInput,
|
|
703
|
+
timestamp: Date.now()
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
/** 车间 2:发送前钩子(兼容桥:事件钩子照发;阶段 4 会迁到 beforeSend 公开位置) */
|
|
707
|
+
async _stageBeforeSend(ctx) {
|
|
708
|
+
await this._hooks.emitAsync("beforeRequest", {
|
|
709
|
+
messages: ctx.messages,
|
|
710
|
+
config: ctx.config,
|
|
711
|
+
options: ctx.options
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
/** 车间 3:自动续写检测(ephemeralContinue 时,底部若有 prefix 消息则续写) */
|
|
715
|
+
async _stageAutoContinue(ctx) {
|
|
716
|
+
if (this.config.ephemeralContinue && !ctx.mergeToEntry) {
|
|
717
|
+
const msgs = ctx.messages.getAll();
|
|
718
|
+
const last = msgs[msgs.length - 1];
|
|
719
|
+
if (last && last.prefix && (last.role === "assistant" || last._ephemeral)) ctx.mergeToEntry = last.id;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/** 车间 4:构建请求体 */
|
|
723
|
+
async _stageBuildRequest(ctx) {
|
|
724
|
+
ctx.body = this._adapter.buildRequest(ctx.messages.getAll(), ctx.config, ctx.systemPrompts.getEnabled());
|
|
725
|
+
}
|
|
726
|
+
/** 车间 5:发送(流式/非流式 + 重试 + 占位消息 + _processResponse 工具钩子) */
|
|
727
|
+
async _stageSend(ctx) {
|
|
728
|
+
const retryCfg = this.config.retry || {};
|
|
729
|
+
ctx.result = await this._withRetry(ctx.body, {
|
|
730
|
+
isStream: ctx.isStream,
|
|
731
|
+
onProgress: ctx.onProgress,
|
|
732
|
+
onDone: ctx.onDone,
|
|
733
|
+
mergeToEntry: ctx.mergeToEntry,
|
|
734
|
+
config: ctx.config,
|
|
735
|
+
maxRetries: retryCfg.maxRetries ?? 0,
|
|
736
|
+
retryDelay: retryCfg.retryDelay ?? 1e3
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
_addUserMessage(userInput, addUser) {
|
|
740
|
+
if (addUser && userInput !== void 0) {
|
|
741
|
+
const msg = typeof userInput === "string" ? {
|
|
742
|
+
role: "user",
|
|
743
|
+
content: userInput
|
|
744
|
+
} : {
|
|
745
|
+
role: "user",
|
|
746
|
+
...userInput
|
|
747
|
+
};
|
|
748
|
+
this.messages.add(msg);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/** retry 循环,占位消息只 push 一次 */
|
|
752
|
+
async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, config, maxRetries, retryDelay }) {
|
|
753
|
+
let lastError = null;
|
|
754
|
+
const adapterOptions = { signal: this._abortController?.signal };
|
|
755
|
+
let placeholder = null;
|
|
756
|
+
let base = null;
|
|
757
|
+
if (isStream) {
|
|
758
|
+
if (mergeToEntry) {
|
|
759
|
+
placeholder = this.messages.getAll().find((m) => m.id === mergeToEntry);
|
|
760
|
+
if (placeholder) base = {
|
|
761
|
+
content: placeholder.content || "",
|
|
762
|
+
reasoning: placeholder.reasoningContent || ""
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
if (!placeholder) placeholder = this.messages.add({
|
|
766
|
+
role: "assistant",
|
|
767
|
+
content: "",
|
|
768
|
+
_complete: false
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
772
|
+
if (attempt > 0) {
|
|
773
|
+
this.emit("retry", {
|
|
774
|
+
attempt,
|
|
775
|
+
maxRetries,
|
|
776
|
+
lastError,
|
|
777
|
+
timestamp: Date.now()
|
|
778
|
+
});
|
|
779
|
+
await new Promise((r) => setTimeout(r, retryDelay));
|
|
780
|
+
}
|
|
781
|
+
try {
|
|
782
|
+
this._isGenerating = true;
|
|
783
|
+
if (isStream) return await this._stream(body, adapterOptions, {
|
|
784
|
+
placeholder,
|
|
785
|
+
base,
|
|
786
|
+
mergeToEntry,
|
|
787
|
+
onProgress,
|
|
788
|
+
onDone,
|
|
789
|
+
config
|
|
790
|
+
});
|
|
791
|
+
else {
|
|
792
|
+
const resp = await this._adapter.send(body, config, adapterOptions);
|
|
793
|
+
let result = this._handleResult(this._adapter.parseResponse(resp), mergeToEntry);
|
|
794
|
+
if (this._processResponse && !mergeToEntry) result = await this._processResponse(result, { isStream: false });
|
|
795
|
+
return result;
|
|
796
|
+
}
|
|
797
|
+
} catch (error) {
|
|
798
|
+
lastError = error;
|
|
799
|
+
if (error.name === "AbortError" || this._abortController?.signal.aborted) {
|
|
800
|
+
this._isGenerating = false;
|
|
801
|
+
throw error;
|
|
802
|
+
}
|
|
803
|
+
if (error instanceof NetworkError && attempt < maxRetries) continue;
|
|
804
|
+
this._isGenerating = false;
|
|
805
|
+
throw error;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
this._isGenerating = false;
|
|
809
|
+
throw lastError;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* 流式处理:ChatService 维护占位消息,adapter 只负责解析 SSE
|
|
813
|
+
*
|
|
814
|
+
* 参数说明:
|
|
815
|
+
* - placeholder:流式期间实时更新的目标消息(续写时 = target 本身,普通发送时 = 新建的空消息)
|
|
816
|
+
* - base:续写时保存的旧内容快照,用于和 API 新内容拼接
|
|
817
|
+
* - mergeToEntry:续写目标的 id,有值时走续写逻辑
|
|
818
|
+
* - onProgress:外部回调,每收到一个 chunk 触发
|
|
819
|
+
* - onDone:流结束回调
|
|
820
|
+
*/
|
|
821
|
+
async _stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone, config }) {
|
|
822
|
+
let finalMsg = null;
|
|
823
|
+
await this._adapter.stream(body, config, (snap) => {
|
|
824
|
+
placeholder.content = mergeToEntry && base ? base.content + (snap.content || "") : snap.content || "";
|
|
825
|
+
if (snap.reasoningContent) placeholder.reasoningContent = mergeToEntry && base ? base.reasoning + snap.reasoningContent : snap.reasoningContent;
|
|
826
|
+
if (snap.toolCalls) placeholder.toolCalls = [...snap.toolCalls];
|
|
827
|
+
this.emit("stream-progress", snap);
|
|
828
|
+
if (onProgress) onProgress(snap);
|
|
829
|
+
}, async (final) => {
|
|
830
|
+
finalMsg = final;
|
|
831
|
+
if (this._processResponse && !mergeToEntry) finalMsg = await this._processResponse(finalMsg, {
|
|
832
|
+
isStream: true,
|
|
833
|
+
onProgress,
|
|
834
|
+
onDone
|
|
835
|
+
});
|
|
836
|
+
if (mergeToEntry && base) {
|
|
837
|
+
const changes = {
|
|
838
|
+
content: base.content + (final.content || ""),
|
|
839
|
+
_complete: true,
|
|
840
|
+
_ephemeral: false,
|
|
841
|
+
prefix: void 0
|
|
842
|
+
};
|
|
843
|
+
if (final.reasoningContent) changes.reasoningContent = base.reasoning + final.reasoningContent;
|
|
844
|
+
if (final.toolCalls) changes.toolCalls = final.toolCalls;
|
|
845
|
+
this.messages.update(mergeToEntry, changes);
|
|
846
|
+
const updated = this.messages.getAll().find((m) => m.id === mergeToEntry);
|
|
847
|
+
this.emit("message", updated);
|
|
848
|
+
if (onDone) onDone(updated);
|
|
849
|
+
} else {
|
|
850
|
+
placeholder._complete = true;
|
|
851
|
+
this.emit("message", finalMsg);
|
|
852
|
+
if (onDone) onDone(finalMsg);
|
|
853
|
+
}
|
|
854
|
+
}, adapterOptions);
|
|
855
|
+
return mergeToEntry || finalMsg;
|
|
856
|
+
}
|
|
857
|
+
/** 非流式结果落位 */
|
|
858
|
+
_handleResult(assistantMsg, mergeToEntry) {
|
|
859
|
+
if (mergeToEntry) {
|
|
860
|
+
const entry = this.messages.getAll().find((m) => m.id === mergeToEntry);
|
|
861
|
+
if (!entry) {
|
|
862
|
+
this.messages.add(assistantMsg);
|
|
863
|
+
this.emit("message", assistantMsg);
|
|
864
|
+
return assistantMsg;
|
|
865
|
+
}
|
|
866
|
+
const changes = {
|
|
867
|
+
content: (entry.content || "") + (assistantMsg.content || ""),
|
|
868
|
+
_complete: true,
|
|
869
|
+
_ephemeral: false,
|
|
870
|
+
prefix: void 0
|
|
871
|
+
};
|
|
872
|
+
if (assistantMsg.reasoningContent) changes.reasoningContent = (entry.reasoningContent || "") + assistantMsg.reasoningContent;
|
|
873
|
+
if (assistantMsg.toolCalls) changes.toolCalls = assistantMsg.toolCalls;
|
|
874
|
+
this.messages.update(mergeToEntry, changes);
|
|
875
|
+
const updated = this.messages.getAll().find((m) => m.id === mergeToEntry);
|
|
876
|
+
this.emit("message", updated);
|
|
877
|
+
return updated;
|
|
878
|
+
}
|
|
160
879
|
this.messages.add(assistantMsg);
|
|
161
880
|
this.emit("message", assistantMsg);
|
|
162
881
|
return assistantMsg;
|
|
163
882
|
}
|
|
164
883
|
/**
|
|
165
|
-
*
|
|
166
|
-
* @param {string|
|
|
167
|
-
* @param {
|
|
168
|
-
* @param {function} onDone - 完成时调用,参数为最终消息对象
|
|
884
|
+
* 发送消息。
|
|
885
|
+
* @param {string|Object} userInput - 用户输入
|
|
886
|
+
* @param {Object} [params] - 请求级参数覆盖(model/temperature/modelParams 等,仅本次生效)
|
|
169
887
|
*/
|
|
170
|
-
async
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
this.emit("stream-progress", { ...accumulated });
|
|
190
|
-
if (onProgress) onProgress({ ...accumulated });
|
|
191
|
-
}, (finalMessage) => {
|
|
192
|
-
this.messages.add(finalMessage);
|
|
193
|
-
this.emit("message", finalMessage);
|
|
194
|
-
if (onDone) onDone(finalMessage);
|
|
195
|
-
});
|
|
888
|
+
async send(userInput, params) {
|
|
889
|
+
this._abortController = new AbortController();
|
|
890
|
+
try {
|
|
891
|
+
return await this._request({
|
|
892
|
+
userInput,
|
|
893
|
+
addUser: true,
|
|
894
|
+
isStream: false,
|
|
895
|
+
params
|
|
896
|
+
});
|
|
897
|
+
} catch (err) {
|
|
898
|
+
if (err.name === "AbortError") {
|
|
899
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
throw err;
|
|
903
|
+
} finally {
|
|
904
|
+
this._abortController = null;
|
|
905
|
+
this._isGenerating = false;
|
|
906
|
+
}
|
|
196
907
|
}
|
|
197
908
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
909
|
+
* 流式发送。
|
|
910
|
+
* @param {string|Object} userInput - 用户输入
|
|
911
|
+
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
912
|
+
* @param {Function} [onProgress] - 流式进度回调
|
|
913
|
+
* @param {Function} [onDone] - 流式完成回调
|
|
200
914
|
*/
|
|
201
|
-
|
|
202
|
-
|
|
915
|
+
async stream(userInput, params, onProgress, onDone) {
|
|
916
|
+
if (typeof params === "function") {
|
|
917
|
+
onDone = onProgress;
|
|
918
|
+
onProgress = params;
|
|
919
|
+
params = void 0;
|
|
920
|
+
}
|
|
921
|
+
this._abortController = new AbortController();
|
|
922
|
+
try {
|
|
923
|
+
return await this._request({
|
|
924
|
+
userInput,
|
|
925
|
+
addUser: true,
|
|
926
|
+
isStream: true,
|
|
927
|
+
onProgress,
|
|
928
|
+
onDone,
|
|
929
|
+
params
|
|
930
|
+
});
|
|
931
|
+
} catch (err) {
|
|
932
|
+
if (err.name === "AbortError") {
|
|
933
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
throw err;
|
|
937
|
+
} finally {
|
|
938
|
+
this._abortController = null;
|
|
939
|
+
this._isGenerating = false;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* 重发当前消息(不加用户消息)。
|
|
944
|
+
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
945
|
+
*/
|
|
946
|
+
async sendExisting(params) {
|
|
947
|
+
this._abortController = new AbortController();
|
|
948
|
+
try {
|
|
949
|
+
return await this._request({
|
|
950
|
+
addUser: false,
|
|
951
|
+
isStream: false,
|
|
952
|
+
params
|
|
953
|
+
});
|
|
954
|
+
} catch (err) {
|
|
955
|
+
if (err.name === "AbortError") {
|
|
956
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
throw err;
|
|
960
|
+
} finally {
|
|
961
|
+
this._abortController = null;
|
|
962
|
+
this._isGenerating = false;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* 流式重发(不加用户消息)。
|
|
967
|
+
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
968
|
+
* @param {Function} [onProgress] - 流式进度回调
|
|
969
|
+
* @param {Function} [onDone] - 流式完成回调
|
|
970
|
+
*/
|
|
971
|
+
async sendExistingStream(params, onProgress, onDone) {
|
|
972
|
+
if (typeof params === "function") {
|
|
973
|
+
onDone = onProgress;
|
|
974
|
+
onProgress = params;
|
|
975
|
+
params = void 0;
|
|
976
|
+
}
|
|
977
|
+
this._abortController = new AbortController();
|
|
978
|
+
try {
|
|
979
|
+
return await this._request({
|
|
980
|
+
addUser: false,
|
|
981
|
+
isStream: true,
|
|
982
|
+
onProgress,
|
|
983
|
+
onDone,
|
|
984
|
+
params
|
|
985
|
+
});
|
|
986
|
+
} catch (err) {
|
|
987
|
+
if (err.name === "AbortError") {
|
|
988
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
throw err;
|
|
992
|
+
} finally {
|
|
993
|
+
this._abortController = null;
|
|
994
|
+
this._isGenerating = false;
|
|
995
|
+
}
|
|
203
996
|
}
|
|
204
997
|
};
|
|
998
|
+
/**
|
|
999
|
+
* 校验 adapter 是否满足协议(v3.0 · 阶段 3)。
|
|
1000
|
+
* 缺少必要方法时抛 ConfigurationError,并列出缺少的方法名——写"同级适配器"不再靠猜。
|
|
1001
|
+
* @param {*} adapter
|
|
1002
|
+
* @throws {ConfigurationError}
|
|
1003
|
+
*/
|
|
1004
|
+
function assertAdapter(adapter) {
|
|
1005
|
+
if (!adapter || typeof adapter !== "object") throw new ConfigurationError("setAdapter: adapter 必须是对象(如 openaiAdapter / createOpenAIAdapter() 实例)");
|
|
1006
|
+
const missing = [
|
|
1007
|
+
"buildRequest",
|
|
1008
|
+
"send",
|
|
1009
|
+
"stream",
|
|
1010
|
+
"parseResponse"
|
|
1011
|
+
].filter((k) => typeof adapter[k] !== "function");
|
|
1012
|
+
if (missing.length) throw new ConfigurationError(`setAdapter: adapter 缺少必要方法: ${missing.join(", ")}(协议见 ChatService.js 顶部的 @typedef ChatAdapter)`);
|
|
1013
|
+
}
|
|
1014
|
+
//#endregion
|
|
1015
|
+
//#region src/utils/url.js
|
|
1016
|
+
/**
|
|
1017
|
+
* URL 工具函数
|
|
1018
|
+
*/
|
|
1019
|
+
/**
|
|
1020
|
+
* 拼接 baseUrl 和 path
|
|
1021
|
+
* 注意:path 应以 '/' 开头,否则会替换 baseUrl 的最后一段
|
|
1022
|
+
* @param {string} baseUrl - 基础 URL(如 https://api.deepseek.com)
|
|
1023
|
+
* @param {string} path - 路径(如 /v1/chat/completions)
|
|
1024
|
+
* @returns {string} 完整的 URL
|
|
1025
|
+
*/
|
|
1026
|
+
function joinUrl(baseUrl, path) {
|
|
1027
|
+
return baseUrl.replace(/\/$/, "") + (path.startsWith("/") ? path : "/" + path);
|
|
1028
|
+
}
|
|
1029
|
+
//#endregion
|
|
1030
|
+
//#region src/utils/typeCheck.js
|
|
1031
|
+
/**
|
|
1032
|
+
* 类型判断工具
|
|
1033
|
+
*/
|
|
1034
|
+
/**
|
|
1035
|
+
* 判断一个值是否为字符串
|
|
1036
|
+
* @param {any} value - 要检查的值
|
|
1037
|
+
* @returns {boolean} 如果是字符串则返回 true
|
|
1038
|
+
*/
|
|
1039
|
+
function isString(value) {
|
|
1040
|
+
return typeof value === "string";
|
|
1041
|
+
}
|
|
1042
|
+
//#endregion
|
|
1043
|
+
//#region src/utils/MessageFormatter.js
|
|
1044
|
+
/**
|
|
1045
|
+
* MessageFormatter — 可注册的消息格式转换器
|
|
1046
|
+
*
|
|
1047
|
+
* 职责:
|
|
1048
|
+
* 1. 内置 OpenA I兼容格式的转换逻辑
|
|
1049
|
+
* 2. 支持 register(name, fn) 注册自定义格式(如 Anthropic、Gemini 等)
|
|
1050
|
+
* 3. 统一处理 capabilities 过滤(reasoning、vision 等)
|
|
1051
|
+
* 4. 所有适配器通过此工具获取 API 消息数组,消除重复代码
|
|
1052
|
+
*
|
|
1053
|
+
* 用法:
|
|
1054
|
+
* import { MessageFormatter } from 'my-ai-chat-framework';
|
|
1055
|
+
*
|
|
1056
|
+
* // 使用内置格式
|
|
1057
|
+
* const msgs = MessageFormatter.format({
|
|
1058
|
+
* messages, systemPrompts, capabilities, resolveImage
|
|
1059
|
+
* }); // 默认 'openai'
|
|
1060
|
+
*
|
|
1061
|
+
* // 注册自定义格式
|
|
1062
|
+
* MessageFormatter.register('anthropic', ({ messages, systemPrompts, capabilities }) => {
|
|
1063
|
+
* // 返回 Anthropic 格式的消息数组
|
|
1064
|
+
* });
|
|
1065
|
+
*/
|
|
1066
|
+
var IS_URL = /^https?:\/\//i;
|
|
1067
|
+
function defaultResolveImage(imageId) {
|
|
1068
|
+
if (IS_URL.test(imageId)) return { url: imageId };
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
function buildMultimodalContent(textContent, images, resolveImage) {
|
|
1072
|
+
const content = [{
|
|
1073
|
+
type: "text",
|
|
1074
|
+
text: textContent
|
|
1075
|
+
}];
|
|
1076
|
+
for (const ref of images) {
|
|
1077
|
+
const resolved = resolveImage(ref);
|
|
1078
|
+
if (!resolved) continue;
|
|
1079
|
+
if (resolved.url) content.push({
|
|
1080
|
+
type: "image_url",
|
|
1081
|
+
image_url: { url: resolved.url }
|
|
1082
|
+
});
|
|
1083
|
+
else if (resolved.data) {
|
|
1084
|
+
const mime = resolved.mimeType || "image/png";
|
|
1085
|
+
content.push({
|
|
1086
|
+
type: "image_url",
|
|
1087
|
+
image_url: { url: `data:${mime};base64,${resolved.data}` }
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return content;
|
|
1092
|
+
}
|
|
1093
|
+
/** OpenAI 兼容格式 */
|
|
1094
|
+
function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
|
|
1095
|
+
const rImg = typeof resolveImage === "function" ? resolveImage : defaultResolveImage;
|
|
1096
|
+
const result = [];
|
|
1097
|
+
for (const sp of systemPrompts) if (sp?.content && isString(sp.content) && sp.content.trim()) result.push({
|
|
1098
|
+
role: "system",
|
|
1099
|
+
content: sp.content.trim()
|
|
1100
|
+
});
|
|
1101
|
+
let ephemEnd = messages.length;
|
|
1102
|
+
for (let i = messages.length - 1; i >= 0; i--) if (messages[i]._ephemeral) ephemEnd = i;
|
|
1103
|
+
else break;
|
|
1104
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1105
|
+
const msg = messages[i];
|
|
1106
|
+
if (msg._ephemeral) {
|
|
1107
|
+
if (i < ephemEnd) continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (msg.role === "system" && !msg._ephemeral) continue;
|
|
1110
|
+
if (msg.role === "tool") {
|
|
1111
|
+
result.push({
|
|
1112
|
+
role: "tool",
|
|
1113
|
+
content: msg.content || "",
|
|
1114
|
+
tool_call_id: msg.toolCallId
|
|
1115
|
+
});
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
if (msg.role === "assistant" && msg.toolCalls?.length) {
|
|
1119
|
+
const entry = {
|
|
1120
|
+
role: "assistant",
|
|
1121
|
+
content: msg.content || "",
|
|
1122
|
+
tool_calls: msg.toolCalls
|
|
1123
|
+
};
|
|
1124
|
+
if (msg.prefix) entry.prefix = true;
|
|
1125
|
+
if (capabilities?.reasoning && msg.reasoningContent) entry.reasoning_content = msg.reasoningContent;
|
|
1126
|
+
result.push(entry);
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
const hasText = msg.content && isString(msg.content) && msg.content.trim();
|
|
1130
|
+
const hasImages = msg.images && Array.isArray(msg.images) && msg.images.length > 0;
|
|
1131
|
+
if (!hasText && !hasImages) {
|
|
1132
|
+
if (msg.role === "assistant" && msg.prefix) {
|
|
1133
|
+
const prefixEntry = {
|
|
1134
|
+
role: "assistant",
|
|
1135
|
+
content: "",
|
|
1136
|
+
prefix: true
|
|
1137
|
+
};
|
|
1138
|
+
if (capabilities?.reasoning && msg.reasoningContent) prefixEntry.reasoning_content = msg.reasoningContent;
|
|
1139
|
+
result.push(prefixEntry);
|
|
1140
|
+
}
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
const entry = { role: msg.role };
|
|
1144
|
+
if (hasImages) {
|
|
1145
|
+
if (!capabilities?.vision) throw new Error("[MessageFormatter] 消息包含图片但模型不支持视觉(capabilities.vision=false)。请切换模型或移除图片。");
|
|
1146
|
+
entry.content = buildMultimodalContent(msg.content || "", msg.images, rImg);
|
|
1147
|
+
} else entry.content = msg.content;
|
|
1148
|
+
if (msg.prefix) entry.prefix = true;
|
|
1149
|
+
result.push(entry);
|
|
1150
|
+
}
|
|
1151
|
+
return result;
|
|
1152
|
+
}
|
|
1153
|
+
var MessageFormatter = {
|
|
1154
|
+
_formats: new Map([["openai", toOpenAI]]),
|
|
1155
|
+
register(name, fn) {
|
|
1156
|
+
if (!name || typeof name !== "string" || !name.trim()) throw new Error("[MessageFormatter] 格式名称必须是非空字符串");
|
|
1157
|
+
if (typeof fn !== "function") throw new Error("[MessageFormatter] 转换函数必须是 function");
|
|
1158
|
+
this._formats.set(name.trim(), fn);
|
|
1159
|
+
},
|
|
1160
|
+
unregister(name) {
|
|
1161
|
+
if (name === "openai") throw new Error("[MessageFormatter] 内置格式 \"openai\" 不可移除");
|
|
1162
|
+
this._formats.delete(name);
|
|
1163
|
+
},
|
|
1164
|
+
listFormats() {
|
|
1165
|
+
return [...this._formats.keys()];
|
|
1166
|
+
},
|
|
1167
|
+
format(options = {}) {
|
|
1168
|
+
const formatName = options.format || "openai";
|
|
1169
|
+
const fn = this._formats.get(formatName);
|
|
1170
|
+
if (!fn) throw new Error(`[MessageFormatter] 未知格式 "${formatName}"。可用格式: ${[...this._formats.keys()].join(", ")}`);
|
|
1171
|
+
return fn({
|
|
1172
|
+
messages: options.messages || [],
|
|
1173
|
+
systemPrompts: options.systemPrompts || [],
|
|
1174
|
+
capabilities: options.capabilities || {},
|
|
1175
|
+
resolveImage: options.resolveImage
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
function assembleMessages(options = {}) {
|
|
1180
|
+
return MessageFormatter.format({
|
|
1181
|
+
...options,
|
|
1182
|
+
format: "openai"
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
205
1185
|
//#endregion
|
|
206
1186
|
//#region src/adapters/openai.js
|
|
207
1187
|
/**
|
|
208
1188
|
* OpenAI 兼容 API 适配器
|
|
209
1189
|
* 支持 DeepSeek 等完全兼容 OpenAI 接口的服务
|
|
210
1190
|
*/
|
|
1191
|
+
var TRANSPORT_KEYS = [
|
|
1192
|
+
"apiKey",
|
|
1193
|
+
"baseUrl",
|
|
1194
|
+
"apiUrl",
|
|
1195
|
+
"path",
|
|
1196
|
+
"headers"
|
|
1197
|
+
];
|
|
211
1198
|
var openaiAdapter = {
|
|
212
1199
|
name: "openai",
|
|
213
|
-
install(chatService) {
|
|
1200
|
+
install(chatService, options = {}) {
|
|
1201
|
+
if (this._transport) this._transport = {
|
|
1202
|
+
...this._transport,
|
|
1203
|
+
...options
|
|
1204
|
+
};
|
|
214
1205
|
chatService.setAdapter(this);
|
|
215
1206
|
},
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
return
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
1207
|
+
_resolveConfig(config) {
|
|
1208
|
+
const transport = this._transport || {};
|
|
1209
|
+
const resolved = {
|
|
1210
|
+
...config,
|
|
1211
|
+
...transport
|
|
1212
|
+
};
|
|
1213
|
+
if (transport.modelParams) resolved.modelParams = {
|
|
1214
|
+
...transport.modelParams,
|
|
1215
|
+
...config.modelParams || {}
|
|
1216
|
+
};
|
|
1217
|
+
return resolved;
|
|
1218
|
+
},
|
|
1219
|
+
getRequestDefaults() {
|
|
1220
|
+
if (!this._transport) return {};
|
|
1221
|
+
const defaults = {};
|
|
1222
|
+
for (const [key, value] of Object.entries(this._transport)) if (!TRANSPORT_KEYS.includes(key) && value !== void 0) defaults[key] = value;
|
|
1223
|
+
return defaults;
|
|
1224
|
+
},
|
|
1225
|
+
buildRequest(messages, config, systemPrompts = []) {
|
|
1226
|
+
config = this._resolveConfig(config);
|
|
1227
|
+
const model = config.model || config.modelParams?.model;
|
|
1228
|
+
if (!model) throw new Error("Missing required config: model (either at top level or in modelParams)");
|
|
1229
|
+
const mp = config.modelParams || {};
|
|
1230
|
+
const temperature = mp.temperature ?? config.temperature ?? .7;
|
|
1231
|
+
const maxTokens = mp.maxTokens ?? config.maxTokens ?? 2e3;
|
|
1232
|
+
const reasoningEffort = mp.reasoningEffort ?? config.reasoningEffort;
|
|
1233
|
+
const requestBody = {
|
|
1234
|
+
model,
|
|
1235
|
+
messages: MessageFormatter.format({
|
|
1236
|
+
messages,
|
|
1237
|
+
systemPrompts,
|
|
1238
|
+
capabilities: config.capabilities || {},
|
|
1239
|
+
resolveImage: config.resolveImage,
|
|
1240
|
+
format: config.messageFormat
|
|
1241
|
+
}),
|
|
1242
|
+
temperature,
|
|
1243
|
+
max_tokens: maxTokens,
|
|
231
1244
|
stream: false
|
|
232
1245
|
};
|
|
1246
|
+
if (mp.topP !== void 0) requestBody.top_p = mp.topP;
|
|
1247
|
+
if (mp.frequencyPenalty !== void 0) requestBody.frequency_penalty = mp.frequencyPenalty;
|
|
1248
|
+
if (mp.presencePenalty !== void 0) requestBody.presence_penalty = mp.presencePenalty;
|
|
1249
|
+
if (mp.stop !== void 0) requestBody.stop = mp.stop;
|
|
1250
|
+
if (mp.responseFormat !== void 0) requestBody.response_format = mp.responseFormat;
|
|
1251
|
+
if (mp.seed !== void 0) requestBody.seed = mp.seed;
|
|
1252
|
+
if (config.tools && Array.isArray(config.tools) && config.tools.length > 0) {
|
|
1253
|
+
requestBody.tools = config.tools;
|
|
1254
|
+
requestBody.tool_choice = "auto";
|
|
1255
|
+
}
|
|
1256
|
+
if (reasoningEffort !== void 0) requestBody.reasoning_effort = reasoningEffort;
|
|
1257
|
+
const consumedKeys = new Set([
|
|
1258
|
+
"model",
|
|
1259
|
+
"temperature",
|
|
1260
|
+
"maxTokens",
|
|
1261
|
+
"reasoningEffort",
|
|
1262
|
+
"topP",
|
|
1263
|
+
"frequencyPenalty",
|
|
1264
|
+
"presencePenalty",
|
|
1265
|
+
"stop",
|
|
1266
|
+
"responseFormat",
|
|
1267
|
+
"seed"
|
|
1268
|
+
]);
|
|
1269
|
+
for (const [key, value] of Object.entries(mp)) if (!consumedKeys.has(key) && value !== void 0) requestBody[key] = value;
|
|
1270
|
+
return requestBody;
|
|
233
1271
|
},
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
1272
|
+
_getUrl(config) {
|
|
1273
|
+
config = config || {};
|
|
1274
|
+
const { apiUrl, baseUrl, path } = config;
|
|
1275
|
+
const defaultPath = "/chat/completions";
|
|
1276
|
+
if (apiUrl && isString(apiUrl)) return apiUrl;
|
|
1277
|
+
if (baseUrl && isString(baseUrl)) return joinUrl(baseUrl, path && isString(path) ? path : defaultPath);
|
|
1278
|
+
return "https://api.openai.com/v1/chat/completions";
|
|
1279
|
+
},
|
|
1280
|
+
async send(requestBody, config, options = {}) {
|
|
1281
|
+
try {
|
|
1282
|
+
config = this._resolveConfig(config);
|
|
1283
|
+
const url = this._getUrl(config);
|
|
1284
|
+
const headers = {
|
|
238
1285
|
"Content-Type": "application/json",
|
|
239
|
-
"Authorization": `Bearer ${config.apiKey}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
1286
|
+
"Authorization": `Bearer ${config.apiKey}`,
|
|
1287
|
+
...config.headers || {},
|
|
1288
|
+
...options.headers || {}
|
|
1289
|
+
};
|
|
1290
|
+
const response = await fetch(url, {
|
|
1291
|
+
method: "POST",
|
|
1292
|
+
headers,
|
|
1293
|
+
body: JSON.stringify(requestBody),
|
|
1294
|
+
signal: options.signal
|
|
1295
|
+
});
|
|
1296
|
+
if (!response.ok) await this.handleErrorResponse(response);
|
|
1297
|
+
return await response.json();
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
if (error instanceof APIError) throw error;
|
|
1300
|
+
if (error.name === "AbortError") throw error;
|
|
1301
|
+
if (error.name === "TypeError" && error.message.includes("fetch")) throw new NetworkError(`Request failed: ${error.message}`, error);
|
|
1302
|
+
throw error;
|
|
246
1303
|
}
|
|
247
|
-
return await response.json();
|
|
248
1304
|
},
|
|
249
1305
|
parseResponse(apiResponse) {
|
|
1306
|
+
if (!apiResponse || !apiResponse.choices || !apiResponse.choices[0] || !apiResponse.choices[0].message) throw new ParsingError("Invalid API response: missing choices or message", apiResponse, JSON.stringify(apiResponse));
|
|
250
1307
|
const msg = apiResponse.choices[0].message;
|
|
251
1308
|
const internal = {
|
|
252
1309
|
role: msg.role,
|
|
@@ -256,32 +1313,44 @@ var openaiAdapter = {
|
|
|
256
1313
|
if (msg.reasoning_content) internal.reasoningContent = msg.reasoning_content;
|
|
257
1314
|
return internal;
|
|
258
1315
|
},
|
|
259
|
-
async stream(requestBody, config, onProgress, onDone) {
|
|
1316
|
+
async stream(requestBody, config, onProgress, onDone, options = {}) {
|
|
1317
|
+
config = this._resolveConfig(config);
|
|
260
1318
|
const streamBody = {
|
|
261
1319
|
...requestBody,
|
|
262
1320
|
stream: true
|
|
263
1321
|
};
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
},
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
1322
|
+
const url = this._getUrl(config);
|
|
1323
|
+
let response;
|
|
1324
|
+
const headers = {
|
|
1325
|
+
"Content-Type": "application/json",
|
|
1326
|
+
"Authorization": `Bearer ${config.apiKey}`,
|
|
1327
|
+
"Accept": "text/event-stream",
|
|
1328
|
+
...config.headers || {},
|
|
1329
|
+
...options.headers || {}
|
|
1330
|
+
};
|
|
1331
|
+
try {
|
|
1332
|
+
response = await fetch(url, {
|
|
1333
|
+
method: "POST",
|
|
1334
|
+
headers,
|
|
1335
|
+
body: JSON.stringify(streamBody),
|
|
1336
|
+
signal: options.signal
|
|
1337
|
+
});
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
if (error.name === "AbortError") throw error;
|
|
1340
|
+
if (error.name === "TypeError" && error.message.includes("fetch")) throw new NetworkError(`Stream request failed: ${error.message}`, error);
|
|
1341
|
+
throw error;
|
|
276
1342
|
}
|
|
1343
|
+
if (!response.ok) await this.handleErrorResponse(response);
|
|
277
1344
|
const reader = response.body.getReader();
|
|
278
1345
|
const decoder = new TextDecoder();
|
|
279
1346
|
let buffer = "";
|
|
280
1347
|
let accumulated = {
|
|
281
1348
|
role: "assistant",
|
|
282
|
-
content: ""
|
|
1349
|
+
content: "",
|
|
1350
|
+
reasoningContent: ""
|
|
283
1351
|
};
|
|
284
1352
|
while (true) {
|
|
1353
|
+
if (options.signal?.aborted) break;
|
|
285
1354
|
const { done, value } = await reader.read();
|
|
286
1355
|
if (done) break;
|
|
287
1356
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -289,22 +1358,34 @@ var openaiAdapter = {
|
|
|
289
1358
|
buffer = lines.pop();
|
|
290
1359
|
for (const line of lines) {
|
|
291
1360
|
const dataLine = line.replace(/^data: /, "").trim();
|
|
292
|
-
if (!dataLine
|
|
1361
|
+
if (!dataLine) continue;
|
|
1362
|
+
if (dataLine === "[DONE]") break;
|
|
293
1363
|
try {
|
|
294
1364
|
const delta = JSON.parse(dataLine).choices?.[0]?.delta;
|
|
295
1365
|
if (!delta) continue;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
1366
|
+
if (delta.content) accumulated.content += delta.content;
|
|
1367
|
+
if (delta.tool_calls) {
|
|
1368
|
+
if (!accumulated.toolCalls) accumulated.toolCalls = [];
|
|
1369
|
+
for (const toolCallDelta of delta.tool_calls) {
|
|
1370
|
+
const index = toolCallDelta.index;
|
|
1371
|
+
if (!accumulated.toolCalls[index]) accumulated.toolCalls[index] = {
|
|
1372
|
+
id: "",
|
|
1373
|
+
type: "function",
|
|
1374
|
+
function: {
|
|
1375
|
+
name: "",
|
|
1376
|
+
arguments: ""
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
if (toolCallDelta.id) accumulated.toolCalls[index].id = toolCallDelta.id;
|
|
1380
|
+
if (toolCallDelta.type) accumulated.toolCalls[index].type = toolCallDelta.type;
|
|
1381
|
+
if (toolCallDelta.function?.name) accumulated.toolCalls[index].function.name += toolCallDelta.function.name;
|
|
1382
|
+
if (toolCallDelta.function?.arguments) accumulated.toolCalls[index].function.arguments += toolCallDelta.function.arguments;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (delta.reasoning_content) accumulated.reasoningContent += delta.reasoning_content;
|
|
305
1386
|
onProgress({ ...accumulated });
|
|
306
1387
|
} catch (e) {
|
|
307
|
-
console.warn("
|
|
1388
|
+
console.warn("stream parsing failed :", e, dataLine);
|
|
308
1389
|
}
|
|
309
1390
|
}
|
|
310
1391
|
}
|
|
@@ -314,81 +1395,211 @@ var openaiAdapter = {
|
|
|
314
1395
|
toolCalls: accumulated.toolCalls,
|
|
315
1396
|
reasoningContent: accumulated.reasoningContent
|
|
316
1397
|
});
|
|
1398
|
+
},
|
|
1399
|
+
async handleErrorResponse(response) {
|
|
1400
|
+
let errorText = await response.text();
|
|
1401
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
1402
|
+
try {
|
|
1403
|
+
const errorJson = JSON.parse(errorText);
|
|
1404
|
+
errorMessage = errorJson.error?.message || errorJson.message || errorText.slice(0, 200);
|
|
1405
|
+
} catch (e) {
|
|
1406
|
+
errorMessage = errorText.length > 200 ? errorText.slice(0, 200) + "..." : errorText;
|
|
1407
|
+
}
|
|
1408
|
+
throw new APIError(errorMessage, response.status, null, errorText);
|
|
317
1409
|
}
|
|
318
1410
|
};
|
|
1411
|
+
/**
|
|
1412
|
+
* 创建 OpenAI 兼容适配器实例(工厂)。
|
|
1413
|
+
*
|
|
1414
|
+
* 与单例 openaiAdapter 的区别:每个实例自持一份运输配置(apiKey/baseUrl/apiUrl/path/headers),
|
|
1415
|
+
* 互不干扰——解决"同一适配器装到多个 ChatService 互相覆盖"的单例陷阱。
|
|
1416
|
+
*
|
|
1417
|
+
* 用法:
|
|
1418
|
+
* const adapter = createOpenAIAdapter({ apiKey, baseUrl, modelParams: { temperature: 0.8 } });
|
|
1419
|
+
* const chat = new ChatService({ adapter, model: 'deepseek-chat' });
|
|
1420
|
+
* chat.use(adapter); // 或直接 new ChatService({ adapter })
|
|
1421
|
+
*
|
|
1422
|
+
* 非运输键(model/modelParams/messageFormat/resolveImage/capabilities 等)会作为
|
|
1423
|
+
* "请求默认参数"供会话层合并,单次请求仍可覆盖。
|
|
1424
|
+
*/
|
|
1425
|
+
function createOpenAIAdapter(options = {}) {
|
|
1426
|
+
return {
|
|
1427
|
+
...openaiAdapter,
|
|
1428
|
+
_transport: { ...options }
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
319
1431
|
//#endregion
|
|
320
1432
|
//#region src/plugins/tool-calling.js
|
|
321
1433
|
/**
|
|
322
1434
|
* 工具调用插件
|
|
323
|
-
*
|
|
1435
|
+
* 功能:拦截助手消息中的 tool_calls,执行对应的工具,将结果作为 tool 消息加入对话,
|
|
1436
|
+
* 然后自动继续对话(通过 sendExisting / sendExistingStream),直到没有新的工具调用。
|
|
1437
|
+
*
|
|
1438
|
+
* 设计要点:
|
|
1439
|
+
* - 支持普通请求和流式请求(通过 isStream 标志区分)
|
|
1440
|
+
* - 支持多次工具调用循环(maxIterations 防止无限循环)
|
|
1441
|
+
* - 并行执行工具
|
|
1442
|
+
* - 工具执行失败时,仍然返回错误信息给 AI,而不是中断整个流程
|
|
1443
|
+
* - 触发 tool-error 事件,方便用户监听工具执行异常
|
|
1444
|
+
*
|
|
1445
|
+
* 注意:请使用 createToolCallingPlugin() 工厂创建实例。
|
|
1446
|
+
* 默认导出的 toolCallingPlugin 是兼容用的模块级单例,装到多个 ChatService
|
|
1447
|
+
* 实例会互相覆盖(executor 表、工具定义、配置),新代码不要直接用单例。
|
|
324
1448
|
*/
|
|
325
|
-
var toolCallingPlugin =
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
1449
|
+
var toolCallingPlugin = createToolCallingPlugin();
|
|
1450
|
+
/**
|
|
1451
|
+
* 创建工具调用插件实例(工厂)。
|
|
1452
|
+
* @param {Object} [options] — { timeout, maxIterations }
|
|
1453
|
+
*/
|
|
1454
|
+
function createToolCallingPlugin(options = {}) {
|
|
1455
|
+
return {
|
|
1456
|
+
name: "tool-calling",
|
|
1457
|
+
maxIterations: options.maxIterations || 5,
|
|
1458
|
+
_options: { ...options },
|
|
1459
|
+
_tools: /* @__PURE__ */ new Map(),
|
|
1460
|
+
_toolDefs: [],
|
|
1461
|
+
chatService: null,
|
|
1462
|
+
install(chatService, options = {}) {
|
|
1463
|
+
this._options = {
|
|
1464
|
+
...this._options,
|
|
1465
|
+
...options
|
|
1466
|
+
};
|
|
1467
|
+
this.chatService = chatService;
|
|
1468
|
+
chatService.pipe({
|
|
1469
|
+
name: "tool-calling-inject",
|
|
1470
|
+
phase: "beforeSend",
|
|
1471
|
+
run: ({ config }) => {
|
|
1472
|
+
if (this._toolDefs.length) config.tools = this._toolDefs;
|
|
1473
|
+
}
|
|
335
1474
|
});
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const toolName = call.function?.name;
|
|
360
|
-
const args = JSON.parse(call.function?.arguments || "{}");
|
|
361
|
-
const tool = self._tools.get(toolName);
|
|
362
|
-
if (!tool) {
|
|
363
|
-
console.warn(`未找到工具: ${toolName}`);
|
|
364
|
-
toolResults.push({
|
|
365
|
-
tool_call_id: call.id,
|
|
366
|
-
error: `工具 ${toolName} 未注册`,
|
|
367
|
-
success: false
|
|
368
|
-
});
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
try {
|
|
372
|
-
const result = await tool.executor(args);
|
|
373
|
-
toolResults.push({
|
|
374
|
-
tool_call_id: call.id,
|
|
375
|
-
content: typeof result === "string" ? result : JSON.stringify(result),
|
|
376
|
-
success: true
|
|
377
|
-
});
|
|
378
|
-
} catch (err) {
|
|
379
|
-
console.error(`工具 ${toolName} 执行失败:`, err);
|
|
380
|
-
toolResults.push({
|
|
381
|
-
tool_call_id: call.id,
|
|
382
|
-
error: err.message,
|
|
383
|
-
success: false
|
|
384
|
-
});
|
|
1475
|
+
/**
|
|
1476
|
+
* 注册工具
|
|
1477
|
+
* @param {string} name - 工具名称(唯一标识)
|
|
1478
|
+
* @param {string} description - 工具描述(告诉 AI 何时调用)
|
|
1479
|
+
* @param {Function} executor - 异步执行函数,接收参数对象,返回结果(字符串或对象)
|
|
1480
|
+
* @param {Object} parameters - JSON Schema 参数定义(可选,默认为空对象)
|
|
1481
|
+
* @returns {ChatService} 返回 chatService 实例,支持链式调用
|
|
1482
|
+
*/
|
|
1483
|
+
chatService.registerTool = (name, description, executor, parameters = {}) => {
|
|
1484
|
+
this._tools.set(name, {
|
|
1485
|
+
executor,
|
|
1486
|
+
description
|
|
1487
|
+
});
|
|
1488
|
+
const toolDefinition = {
|
|
1489
|
+
type: "function",
|
|
1490
|
+
function: {
|
|
1491
|
+
name,
|
|
1492
|
+
description,
|
|
1493
|
+
parameters: {
|
|
1494
|
+
type: "object",
|
|
1495
|
+
properties: parameters,
|
|
1496
|
+
required: Object.keys(parameters).filter((key) => parameters[key]?.required)
|
|
1497
|
+
}
|
|
385
1498
|
}
|
|
1499
|
+
};
|
|
1500
|
+
if (!this._toolDefs.find((t) => t.function.name === name)) this._toolDefs.push(toolDefinition);
|
|
1501
|
+
return chatService;
|
|
1502
|
+
};
|
|
1503
|
+
chatService.pipe({
|
|
1504
|
+
name: "tool-calling-loop",
|
|
1505
|
+
phase: "afterSend",
|
|
1506
|
+
run: async (ctx) => {
|
|
1507
|
+
ctx.result = await this._handleWithTools(ctx.result, ctx.isStream, ctx.onProgress || void 0, ctx.onDone || void 0);
|
|
386
1508
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
1509
|
+
});
|
|
1510
|
+
},
|
|
1511
|
+
async _handleWithTools(initialResponse, isStream, onProgress, onDone) {
|
|
1512
|
+
const self = this;
|
|
1513
|
+
const chat = this.chatService;
|
|
1514
|
+
let iteration = 0;
|
|
1515
|
+
/**
|
|
1516
|
+
* 递归处理工具调用
|
|
1517
|
+
* @param {Object} initialResponse - 初始 AI 响应(可能是第一次请求的响应)
|
|
1518
|
+
* @returns {Promise<Object>} 最终 AI 响应(不含 tool_calls)
|
|
1519
|
+
*/
|
|
1520
|
+
async function processResponse(initialResponse) {
|
|
1521
|
+
let lastResponse = initialResponse;
|
|
1522
|
+
while (iteration < self.maxIterations) {
|
|
1523
|
+
const toolCalls = lastResponse?.toolCalls;
|
|
1524
|
+
if (!toolCalls || toolCalls.length === 0) break;
|
|
1525
|
+
iteration++;
|
|
1526
|
+
const toolResults = await Promise.all(toolCalls.map(async (call) => {
|
|
1527
|
+
const toolName = call.function?.name;
|
|
1528
|
+
let args;
|
|
1529
|
+
try {
|
|
1530
|
+
args = JSON.parse(call.function?.arguments || "{}");
|
|
1531
|
+
} catch (parseErr) {
|
|
1532
|
+
chat.emit("tool-error", {
|
|
1533
|
+
toolName: toolName || "unknown",
|
|
1534
|
+
error: parseErr,
|
|
1535
|
+
toolCallId: call.id,
|
|
1536
|
+
stage: "parse",
|
|
1537
|
+
timestamp: Date.now()
|
|
1538
|
+
});
|
|
1539
|
+
return {
|
|
1540
|
+
tool_call_id: call.id,
|
|
1541
|
+
error: `参数解析失败: ${parseErr.message}`,
|
|
1542
|
+
success: false
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
const tool = self._tools.get(toolName);
|
|
1546
|
+
if (!tool) {
|
|
1547
|
+
chat.emit("tool-error", {
|
|
1548
|
+
toolName,
|
|
1549
|
+
error: /* @__PURE__ */ new Error(`Tool not registered: ${toolName}`),
|
|
1550
|
+
toolCallId: call.id,
|
|
1551
|
+
stage: "lookup",
|
|
1552
|
+
timestamp: Date.now()
|
|
1553
|
+
});
|
|
1554
|
+
return {
|
|
1555
|
+
tool_call_id: call.id,
|
|
1556
|
+
error: `工具 ${toolName} 未注册`,
|
|
1557
|
+
success: false
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
try {
|
|
1561
|
+
const toolTimeout = self._options.timeout ?? chat.config.toolTimeout;
|
|
1562
|
+
let execPromise = tool.executor(args);
|
|
1563
|
+
if (toolTimeout && typeof toolTimeout === "number" && toolTimeout > 0) {
|
|
1564
|
+
const timeoutErr = /* @__PURE__ */ new Error(`工具 ${toolName} 执行超时 (${toolTimeout}ms)`);
|
|
1565
|
+
timeoutErr.name = "ToolTimeoutError";
|
|
1566
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(timeoutErr), toolTimeout));
|
|
1567
|
+
execPromise = Promise.race([execPromise, timeoutPromise]);
|
|
1568
|
+
}
|
|
1569
|
+
const result = await execPromise;
|
|
1570
|
+
chat.emit("tool-success", {
|
|
1571
|
+
toolName,
|
|
1572
|
+
result,
|
|
1573
|
+
toolCallId: call.id,
|
|
1574
|
+
timestamp: Date.now()
|
|
1575
|
+
});
|
|
1576
|
+
return {
|
|
1577
|
+
tool_call_id: call.id,
|
|
1578
|
+
content: typeof result === "string" ? result : JSON.stringify(result),
|
|
1579
|
+
success: true
|
|
1580
|
+
};
|
|
1581
|
+
} catch (err) {
|
|
1582
|
+
const stage = err.name === "ToolTimeoutError" ? "timeout" : "execute";
|
|
1583
|
+
chat.emit("tool-error", {
|
|
1584
|
+
toolName,
|
|
1585
|
+
error: err,
|
|
1586
|
+
toolCallId: call.id,
|
|
1587
|
+
stage,
|
|
1588
|
+
timestamp: Date.now()
|
|
1589
|
+
});
|
|
1590
|
+
return {
|
|
1591
|
+
tool_call_id: call.id,
|
|
1592
|
+
error: err.message,
|
|
1593
|
+
success: false
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
}));
|
|
1597
|
+
for (const tr of toolResults) {
|
|
1598
|
+
const content = tr.success ? tr.content : tr.error || "工具执行失败";
|
|
1599
|
+
chat.messages.addTool(content, tr.tool_call_id);
|
|
1600
|
+
}
|
|
1601
|
+
if (isStream) await new Promise((resolve) => {
|
|
1602
|
+
chat.sendExistingStream((chunk) => {
|
|
392
1603
|
if (onProgress) onProgress(chunk);
|
|
393
1604
|
lastResponse = chunk;
|
|
394
1605
|
}, (final) => {
|
|
@@ -396,35 +1607,198 @@ var toolCallingPlugin = {
|
|
|
396
1607
|
resolve();
|
|
397
1608
|
});
|
|
398
1609
|
});
|
|
399
|
-
|
|
400
|
-
const reqBody = chat._adapter.buildRequest(chat.messages.getAll(), chat.config);
|
|
401
|
-
const apiResp = await chat._adapter.send(reqBody, chat.config);
|
|
402
|
-
lastResponse = chat._adapter.parseResponse(apiResp);
|
|
1610
|
+
else lastResponse = await chat.sendExisting();
|
|
403
1611
|
}
|
|
1612
|
+
return lastResponse;
|
|
404
1613
|
}
|
|
405
|
-
return
|
|
1614
|
+
return await processResponse(initialResponse);
|
|
406
1615
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
//#endregion
|
|
1619
|
+
//#region src/plugins/model-registry.js
|
|
1620
|
+
/**
|
|
1621
|
+
* model-registry 插件
|
|
1622
|
+
*
|
|
1623
|
+
* 职责:
|
|
1624
|
+
* 1. 内置常用模型的能力标签表(model → capabilities 映射)
|
|
1625
|
+
* 2. 安装时自动根据 config.model 查表,写入 config.capabilities
|
|
1626
|
+
* 3. 监听 config-updated 事件,切换模型时自动同步能力
|
|
1627
|
+
* 4. 提供 chat.registerModel(),允许用户追加/覆盖自定义模型
|
|
1628
|
+
*
|
|
1629
|
+
* 能力标签字段(全部可选,缺失视为 false / 未知):
|
|
1630
|
+
* {
|
|
1631
|
+
* reasoning: boolean — 是否返回 reasoning_content(思维链)
|
|
1632
|
+
* vision: boolean — 是否支持图片输入
|
|
1633
|
+
* toolCalling: boolean — 是否支持 function calling
|
|
1634
|
+
* streaming: boolean — 是否支持流式传输
|
|
1635
|
+
* maxInputTokens: number — 最大输入 token 数
|
|
1636
|
+
* maxOutputTokens: number — 最大输出 token 数
|
|
1637
|
+
* }
|
|
1638
|
+
*
|
|
1639
|
+
* 使用:
|
|
1640
|
+
* chat.use(modelRegistryPlugin);
|
|
1641
|
+
* // config.capabilities 现在已自动填充
|
|
1642
|
+
* chat.registerModel('my-custom-model', { toolCalling: true });
|
|
1643
|
+
*/
|
|
1644
|
+
var BUILTIN_MODELS = {
|
|
1645
|
+
"deepseek-chat": {
|
|
1646
|
+
reasoning: false,
|
|
1647
|
+
vision: false,
|
|
1648
|
+
toolCalling: true,
|
|
1649
|
+
streaming: true,
|
|
1650
|
+
maxInputTokens: 128e3,
|
|
1651
|
+
maxOutputTokens: 8192
|
|
1652
|
+
},
|
|
1653
|
+
"deepseek-reasoner": {
|
|
1654
|
+
reasoning: true,
|
|
1655
|
+
vision: false,
|
|
1656
|
+
toolCalling: true,
|
|
1657
|
+
streaming: true,
|
|
1658
|
+
maxInputTokens: 128e3,
|
|
1659
|
+
maxOutputTokens: 8192
|
|
1660
|
+
},
|
|
1661
|
+
"gpt-4o": {
|
|
1662
|
+
reasoning: false,
|
|
1663
|
+
vision: true,
|
|
1664
|
+
toolCalling: true,
|
|
1665
|
+
streaming: true,
|
|
1666
|
+
maxInputTokens: 128e3,
|
|
1667
|
+
maxOutputTokens: 16384
|
|
1668
|
+
},
|
|
1669
|
+
"gpt-4o-mini": {
|
|
1670
|
+
reasoning: false,
|
|
1671
|
+
vision: true,
|
|
1672
|
+
toolCalling: true,
|
|
1673
|
+
streaming: true,
|
|
1674
|
+
maxInputTokens: 128e3,
|
|
1675
|
+
maxOutputTokens: 16384
|
|
1676
|
+
},
|
|
1677
|
+
"gpt-3.5-turbo": {
|
|
1678
|
+
reasoning: false,
|
|
1679
|
+
vision: false,
|
|
1680
|
+
toolCalling: true,
|
|
1681
|
+
streaming: true,
|
|
1682
|
+
maxInputTokens: 16385,
|
|
1683
|
+
maxOutputTokens: 4096
|
|
1684
|
+
},
|
|
1685
|
+
"o1": {
|
|
1686
|
+
reasoning: true,
|
|
1687
|
+
vision: false,
|
|
1688
|
+
toolCalling: false,
|
|
1689
|
+
streaming: false,
|
|
1690
|
+
maxInputTokens: 2e5,
|
|
1691
|
+
maxOutputTokens: 1e5
|
|
1692
|
+
},
|
|
1693
|
+
"o3-mini": {
|
|
1694
|
+
reasoning: true,
|
|
1695
|
+
vision: false,
|
|
1696
|
+
toolCalling: true,
|
|
1697
|
+
streaming: true,
|
|
1698
|
+
maxInputTokens: 2e5,
|
|
1699
|
+
maxOutputTokens: 1e5
|
|
1700
|
+
},
|
|
1701
|
+
"claude-3.5-sonnet": {
|
|
1702
|
+
reasoning: false,
|
|
1703
|
+
vision: true,
|
|
1704
|
+
toolCalling: true,
|
|
1705
|
+
streaming: true,
|
|
1706
|
+
maxInputTokens: 2e5,
|
|
1707
|
+
maxOutputTokens: 8192
|
|
1708
|
+
},
|
|
1709
|
+
"claude-3.5-haiku": {
|
|
1710
|
+
reasoning: false,
|
|
1711
|
+
vision: true,
|
|
1712
|
+
toolCalling: true,
|
|
1713
|
+
streaming: true,
|
|
1714
|
+
maxInputTokens: 2e5,
|
|
1715
|
+
maxOutputTokens: 8192
|
|
416
1716
|
}
|
|
417
1717
|
};
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
EventEmitter,
|
|
424
|
-
openaiAdapter,
|
|
425
|
-
toolCallingPlugin
|
|
1718
|
+
var DEFAULT_CAPABILITIES = {
|
|
1719
|
+
streaming: true,
|
|
1720
|
+
toolCalling: false,
|
|
1721
|
+
reasoning: false,
|
|
1722
|
+
vision: false
|
|
426
1723
|
};
|
|
1724
|
+
/**
|
|
1725
|
+
* 注意:请使用 createModelRegistryPlugin() 工厂创建实例。
|
|
1726
|
+
* 默认导出的 modelRegistryPlugin 是兼容用的模块级单例,装到多个 ChatService
|
|
1727
|
+
* 实例会互相覆盖(注册表、实例引用),新代码不要直接用单例。
|
|
1728
|
+
*/
|
|
1729
|
+
var modelRegistryPlugin = createModelRegistryPlugin();
|
|
1730
|
+
/**
|
|
1731
|
+
* 创建模型能力注册表插件实例(工厂)。
|
|
1732
|
+
* @param {Object} [options] — { models: { '模型名': capabilities } }(合并进内置表)
|
|
1733
|
+
*/
|
|
1734
|
+
function createModelRegistryPlugin(options = {}) {
|
|
1735
|
+
return {
|
|
1736
|
+
name: "model-registry",
|
|
1737
|
+
_options: { ...options },
|
|
1738
|
+
_registry: new Map([...Object.entries(BUILTIN_MODELS), ...Object.entries(options.models || {})]),
|
|
1739
|
+
chat: null,
|
|
1740
|
+
install(chatService, options = {}) {
|
|
1741
|
+
this._options = {
|
|
1742
|
+
...this._options,
|
|
1743
|
+
...options
|
|
1744
|
+
};
|
|
1745
|
+
this.chat = chatService;
|
|
1746
|
+
/**
|
|
1747
|
+
* 注册/覆盖一个模型的能力标签
|
|
1748
|
+
* @param {string} name — 模型名称
|
|
1749
|
+
* @param {Object} capabilities — 能力标签对象(部分字段即可,未提供的取默认值)
|
|
1750
|
+
* @returns {ChatService}
|
|
1751
|
+
*/
|
|
1752
|
+
chatService.registerModel = (name, capabilities = {}) => {
|
|
1753
|
+
if (!name || typeof name !== "string" || !name.trim()) throw new Error("[model-registry] 模型名称必须是非空字符串");
|
|
1754
|
+
const merged = {
|
|
1755
|
+
...DEFAULT_CAPABILITIES,
|
|
1756
|
+
...capabilities
|
|
1757
|
+
};
|
|
1758
|
+
this._registry.set(name.trim(), merged);
|
|
1759
|
+
if (chatService.config.model === name.trim()) this._syncCapabilities();
|
|
1760
|
+
return chatService;
|
|
1761
|
+
};
|
|
1762
|
+
/**
|
|
1763
|
+
* 列出所有已注册的模型名称
|
|
1764
|
+
* @returns {Array<string>}
|
|
1765
|
+
*/
|
|
1766
|
+
chatService.listModels = () => {
|
|
1767
|
+
return [...this._registry.keys()];
|
|
1768
|
+
};
|
|
1769
|
+
this._syncCapabilities();
|
|
1770
|
+
chatService.pipe({
|
|
1771
|
+
name: "model-registry-caps",
|
|
1772
|
+
phase: "beforeSend",
|
|
1773
|
+
run: ({ config }) => {
|
|
1774
|
+
if (!config.model) return;
|
|
1775
|
+
const caps = this._registry.get(config.model);
|
|
1776
|
+
if (caps) config.capabilities = { ...caps };
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
chatService.on("config-updated", ({ changes }) => {
|
|
1780
|
+
if ("model" in changes) this._syncCapabilities();
|
|
1781
|
+
if ("capabilities" in changes && !("model" in changes)) {
|
|
1782
|
+
const current = this._lookupCapabilities();
|
|
1783
|
+
if (current) chatService.config.capabilities = {
|
|
1784
|
+
...current,
|
|
1785
|
+
...changes.capabilities
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
},
|
|
1790
|
+
_lookupCapabilities() {
|
|
1791
|
+
const model = this.chat.config.model;
|
|
1792
|
+
if (!model) return null;
|
|
1793
|
+
return this._registry.get(model) || null;
|
|
1794
|
+
},
|
|
1795
|
+
_syncCapabilities() {
|
|
1796
|
+
const caps = this._lookupCapabilities();
|
|
1797
|
+
if (caps) this.chat.config.capabilities = { ...caps };
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
427
1801
|
//#endregion
|
|
428
|
-
export { ChatService, EventEmitter, MessageStore,
|
|
1802
|
+
export { APIError, ChatService, ConfigurationError, EventEmitter, MessageFormatter, MessageStore, NetworkError, ParsingError, Pipeline, SystemPromptStore, assembleMessages, assertAdapter, createModelRegistryPlugin, createOpenAIAdapter, createToolCallingPlugin, modelRegistryPlugin, openaiAdapter, toolCallingPlugin };
|
|
429
1803
|
|
|
430
1804
|
//# sourceMappingURL=my-ai-chat-framework.browser.es.js.map
|