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
|
@@ -140,6 +140,25 @@ var MessageStore = class {
|
|
|
140
140
|
return [];
|
|
141
141
|
}
|
|
142
142
|
/**
|
|
143
|
+
* 撤回到指定消息 id 的上一个用户消息,删除两者之间的所有消息
|
|
144
|
+
* 适用于重发
|
|
145
|
+
* @param {string} id — 目标消息 id
|
|
146
|
+
* @returns {Array} 被删除的消息列表
|
|
147
|
+
*/
|
|
148
|
+
undoToPreviousUser(id) {
|
|
149
|
+
let targetIndex = -1;
|
|
150
|
+
let previousUserIndex = -1;
|
|
151
|
+
for (let i = this._messages.length - 1; i >= 0; i--) {
|
|
152
|
+
if (this._messages[i].id === id) targetIndex = i;
|
|
153
|
+
if (targetIndex >= 0 && this._messages[i].role === "user") {
|
|
154
|
+
previousUserIndex = i;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (targetIndex >= 0 && previousUserIndex >= 0) return this._messages.splice(previousUserIndex + 1, targetIndex - previousUserIndex);
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
143
162
|
* 更新消息:按 id 查找并合并 changes,不新增消息
|
|
144
163
|
* @param {string} id — 消息 id
|
|
145
164
|
* @param {Object} changes — 要合并的字段
|
|
@@ -155,6 +174,69 @@ var MessageStore = class {
|
|
|
155
174
|
}
|
|
156
175
|
};
|
|
157
176
|
//#endregion
|
|
177
|
+
//#region src/core/Errors.js
|
|
178
|
+
/**
|
|
179
|
+
* 自定义错误类
|
|
180
|
+
* 用于区分不同类型的错误,方便用户通过 `error.name` 或 `instanceof` 处理
|
|
181
|
+
*/
|
|
182
|
+
var APIError = class extends Error {
|
|
183
|
+
/**
|
|
184
|
+
* @param {string} message - 错误消息(通常来自 API 响应)
|
|
185
|
+
* @param {number} statusCode - HTTP 状态码
|
|
186
|
+
* @param {any} originalError - 原始错误对象或相关信息
|
|
187
|
+
* @param {string} responseText - 原始响应文本(如果有)
|
|
188
|
+
*/
|
|
189
|
+
constructor(message, statusCode, originalError, responseText) {
|
|
190
|
+
super(message);
|
|
191
|
+
this.name = "APIError";
|
|
192
|
+
this.statusCode = statusCode;
|
|
193
|
+
this.originalError = originalError;
|
|
194
|
+
this.responseText = responseText;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
var NetworkError = class extends Error {
|
|
198
|
+
/**
|
|
199
|
+
* @param {string} message - 错误消息
|
|
200
|
+
* @param {any} originalError - 原始错误对象或相关信息
|
|
201
|
+
*/
|
|
202
|
+
constructor(message, originalError) {
|
|
203
|
+
super(message);
|
|
204
|
+
this.name = "NetworkError";
|
|
205
|
+
this.originalError = originalError;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
var ConfigurationError = class extends Error {
|
|
209
|
+
/**
|
|
210
|
+
* @param {string} message - 错误消息
|
|
211
|
+
*/
|
|
212
|
+
constructor(message) {
|
|
213
|
+
super(message);
|
|
214
|
+
this.name = "ConfigurationError";
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
var ValidationError = class extends ConfigurationError {
|
|
218
|
+
/**
|
|
219
|
+
* @param {string} message - 错误消息
|
|
220
|
+
*/
|
|
221
|
+
constructor(message) {
|
|
222
|
+
super(message);
|
|
223
|
+
this.name = "ValidationError";
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
var ParsingError = class extends Error {
|
|
227
|
+
/**
|
|
228
|
+
* @param {string} message - 错误消息
|
|
229
|
+
* @param {any} originalError - 原始错误对象或相关信息
|
|
230
|
+
* @param {string} responseText - 原始响应文本(如果有)
|
|
231
|
+
**/
|
|
232
|
+
constructor(message, originalError, responseText) {
|
|
233
|
+
super(message);
|
|
234
|
+
this.name = "ParsingError";
|
|
235
|
+
this.originalError = originalError;
|
|
236
|
+
this.responseText = responseText;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
//#endregion
|
|
158
240
|
//#region src/core/SystemPromptStore.js
|
|
159
241
|
/**
|
|
160
242
|
* SystemPromptStore — 系统提示词存储
|
|
@@ -182,7 +264,7 @@ var SystemPromptStore = class {
|
|
|
182
264
|
* @returns {Object} 添加的记录
|
|
183
265
|
*/
|
|
184
266
|
add(content, enabled = true) {
|
|
185
|
-
if (!content || typeof content !== "string" || !content.trim()) throw new
|
|
267
|
+
if (!content || typeof content !== "string" || !content.trim()) throw new ValidationError("[SystemPromptStore] content 必须是非空字符串");
|
|
186
268
|
const record = {
|
|
187
269
|
id: `sys_${Date.now()}_${++this._idCounter}`,
|
|
188
270
|
content: content.trim(),
|
|
@@ -198,7 +280,7 @@ var SystemPromptStore = class {
|
|
|
198
280
|
* @returns {Object} 被删除的记录
|
|
199
281
|
*/
|
|
200
282
|
remove(index) {
|
|
201
|
-
if (index < 0 || index >= this._prompts.length) throw new
|
|
283
|
+
if (index < 0 || index >= this._prompts.length) throw new ValidationError(`[SystemPromptStore] 索引越界: ${index}`);
|
|
202
284
|
return this._prompts.splice(index, 1)[0];
|
|
203
285
|
}
|
|
204
286
|
/**
|
|
@@ -207,7 +289,7 @@ var SystemPromptStore = class {
|
|
|
207
289
|
* @returns {boolean} 切换后的状态
|
|
208
290
|
*/
|
|
209
291
|
toggle(index) {
|
|
210
|
-
if (index < 0 || index >= this._prompts.length) throw new
|
|
292
|
+
if (index < 0 || index >= this._prompts.length) throw new ValidationError(`[SystemPromptStore] 索引越界: ${index}`);
|
|
211
293
|
this._prompts[index].enabled = !this._prompts[index].enabled;
|
|
212
294
|
return this._prompts[index].enabled;
|
|
213
295
|
}
|
|
@@ -217,8 +299,8 @@ var SystemPromptStore = class {
|
|
|
217
299
|
* @param {string} content
|
|
218
300
|
*/
|
|
219
301
|
update(index, content) {
|
|
220
|
-
if (index < 0 || index >= this._prompts.length) throw new
|
|
221
|
-
if (!content || typeof content !== "string" || !content.trim()) throw new
|
|
302
|
+
if (index < 0 || index >= this._prompts.length) throw new ValidationError(`[SystemPromptStore] 索引越界: ${index}`);
|
|
303
|
+
if (!content || typeof content !== "string" || !content.trim()) throw new ValidationError("[SystemPromptStore] content 必须是非空字符串");
|
|
222
304
|
this._prompts[index].content = content.trim();
|
|
223
305
|
this._prompts[index].timestamp = Date.now();
|
|
224
306
|
}
|
|
@@ -255,151 +337,337 @@ var SystemPromptStore = class {
|
|
|
255
337
|
}
|
|
256
338
|
};
|
|
257
339
|
//#endregion
|
|
258
|
-
//#region src/core/
|
|
340
|
+
//#region src/core/Pipeline.js
|
|
259
341
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
342
|
+
* Pipeline —— 极简顺序管道(v3.0 提案 · 阶段 1)
|
|
343
|
+
*
|
|
344
|
+
* 三个概念(对照 pipeline-demo.html):
|
|
345
|
+
* - 小车 ctx —— 本次请求的全部"状态",车间之间只通过它交接
|
|
346
|
+
* - 车间 stage —— { name, run(ctx) },只干一件事,不认别的车间
|
|
347
|
+
* - 调度 run() —— for + await,顺序执行(线性,无"回程";洋葱能力留给阶段 2 的 after 车间)
|
|
262
348
|
*/
|
|
263
|
-
var
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
* @param {number} statusCode - HTTP 状态码
|
|
267
|
-
* @param {any} originalError - 原始错误对象或相关信息
|
|
268
|
-
* @param {string} responseText - 原始响应文本(如果有)
|
|
269
|
-
*/
|
|
270
|
-
constructor(message, statusCode, originalError, responseText) {
|
|
271
|
-
super(message);
|
|
272
|
-
this.name = "APIError";
|
|
273
|
-
this.statusCode = statusCode;
|
|
274
|
-
this.originalError = originalError;
|
|
275
|
-
this.responseText = responseText;
|
|
349
|
+
var Pipeline = class {
|
|
350
|
+
constructor() {
|
|
351
|
+
this._stages = [];
|
|
276
352
|
}
|
|
277
|
-
};
|
|
278
|
-
var NetworkError = class extends Error {
|
|
279
353
|
/**
|
|
280
|
-
*
|
|
281
|
-
* @param {
|
|
354
|
+
* 注册一个车间(追加到末尾)。
|
|
355
|
+
* @param {{name: string, run: (ctx: object) => void | Promise<void>}} stage
|
|
356
|
+
* @returns {Pipeline} this,支持链式
|
|
282
357
|
*/
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
this.
|
|
286
|
-
this
|
|
358
|
+
register(stage) {
|
|
359
|
+
if (!stage || typeof stage.run !== "function") throw new TypeError("Pipeline.register: stage 需要 { name, run(ctx) },运行 run 必须是函数");
|
|
360
|
+
this._stages.push(stage);
|
|
361
|
+
return this;
|
|
287
362
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
*/
|
|
293
|
-
constructor(message) {
|
|
294
|
-
super(message);
|
|
295
|
-
this.name = "ConfigurationError";
|
|
363
|
+
/** 按名字移除车间(移除不存在的名字是安全的) */
|
|
364
|
+
unregister(name) {
|
|
365
|
+
this._stages = this._stages.filter((s) => s.name !== name);
|
|
366
|
+
return this;
|
|
296
367
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
super(message);
|
|
306
|
-
this.name = "ParsingError";
|
|
307
|
-
this.originalError = originalError;
|
|
308
|
-
this.responseText = responseText;
|
|
368
|
+
/** 列出当前车间名(调试用) */
|
|
369
|
+
names() {
|
|
370
|
+
return this._stages.map((s) => s.name);
|
|
371
|
+
}
|
|
372
|
+
/** 把小车开过所有车间;返回 ctx(车间可改 ctx 上的字段) */
|
|
373
|
+
async run(ctx) {
|
|
374
|
+
for (const stage of this._stages) await stage.run(ctx);
|
|
375
|
+
return ctx;
|
|
309
376
|
}
|
|
310
377
|
};
|
|
311
378
|
//#endregion
|
|
312
379
|
//#region src/core/ChatService.js
|
|
380
|
+
/**
|
|
381
|
+
* @typedef {Object} ChatAdapter 适配器协议(v3.0:写同级适配器只需实现这 4+1 个方法)
|
|
382
|
+
* @property {Function} buildRequest (messages, config, systemPrompts) => requestBody
|
|
383
|
+
* @property {Function} send (body, config, opts) => Promise<apiResponse>
|
|
384
|
+
* @property {Function} stream (body, config, onProgress, onDone, opts) => Promise<void>
|
|
385
|
+
* @property {Function} parseResponse (apiResponse) => assistantMessage
|
|
386
|
+
* @property {Function} [getRequestDefaults] () => 默认请求参数(可选)
|
|
387
|
+
* @property {Function} [install] (chatService, options) 插件式安装(可选)
|
|
388
|
+
*/
|
|
389
|
+
/**
|
|
390
|
+
* @typedef {Object} PipelineContext 管道小车 ctx(public pipe 车间可见/可改的全部字段)
|
|
391
|
+
* @property {Object} options 原始请求选项
|
|
392
|
+
* @property {*} userInput 用户输入(可为 undefined)
|
|
393
|
+
* @property {boolean} addUser 是否已新增/将新增用户消息
|
|
394
|
+
* @property {MessageStore} messages 消息列表(可读可改:push / unshift / update)
|
|
395
|
+
* @property {SystemPromptStore} systemPrompts 系统提示词(可读可改)
|
|
396
|
+
* @property {Object} config 合并后的请求级配置(prepareInput 后生效)
|
|
397
|
+
* @property {boolean} isStream 本次是否流式
|
|
398
|
+
* @property {Function|null} onProgress 流式进度回调
|
|
399
|
+
* @property {Function|null} onDone 流式完成回调
|
|
400
|
+
* @property {string|null} mergeToEntry 续写目标 id(null = 普通发送)
|
|
401
|
+
* @property {*} body 请求体(buildRequest 后产出)
|
|
402
|
+
* @property {*} result 最终结果(send 后产出)
|
|
403
|
+
*/
|
|
404
|
+
/**
|
|
405
|
+
* @typedef {Object} PipeStage 车间(v3.0 公开扩展单元)
|
|
406
|
+
* @property {string} name 车间名(唯一,不能与内部车间重名)
|
|
407
|
+
* @property {string} [phase] 'beforeSend'(默认)| 'afterSend'
|
|
408
|
+
* @property {Function} run (ctx: PipelineContext) => void | Promise<void>
|
|
409
|
+
*/
|
|
410
|
+
var SESSION_CONFIG_KEYS = [
|
|
411
|
+
"model",
|
|
412
|
+
"temperature",
|
|
413
|
+
"maxTokens",
|
|
414
|
+
"modelParams",
|
|
415
|
+
"system",
|
|
416
|
+
"retry"
|
|
417
|
+
];
|
|
418
|
+
var INTERNAL_STAGES = [
|
|
419
|
+
"prepareInput",
|
|
420
|
+
"beforeSend",
|
|
421
|
+
"autoContinue",
|
|
422
|
+
"buildRequest",
|
|
423
|
+
"send"
|
|
424
|
+
];
|
|
313
425
|
var ChatService = class extends EventEmitter {
|
|
314
426
|
constructor(config = {}) {
|
|
315
427
|
super();
|
|
316
428
|
this.config = { ...config };
|
|
317
|
-
|
|
429
|
+
if (config.store) {
|
|
430
|
+
if (typeof config.store.getAll !== "function" || typeof config.store.add !== "function") throw new ValidationError("store 必须实现 MessageStore 的接口(至少 getAll / add / update)");
|
|
431
|
+
this.messages = config.store;
|
|
432
|
+
} else this.messages = new MessageStore();
|
|
318
433
|
this.systemPrompts = new SystemPromptStore();
|
|
319
434
|
this._adapter = null;
|
|
320
|
-
this.
|
|
321
|
-
this.
|
|
322
|
-
this._hooks = new EventEmitter();
|
|
323
|
-
this._processResponse = null;
|
|
435
|
+
this._activeRequests = /* @__PURE__ */ new Map();
|
|
436
|
+
this._requestSeq = 0;
|
|
324
437
|
const model = this.config.model || this.config.modelParams?.model;
|
|
325
438
|
if (!model || typeof model !== "string" || !model.trim()) throw new ConfigurationError("缺少 model 配置");
|
|
326
|
-
|
|
439
|
+
this._assertValidConfig(this.config);
|
|
440
|
+
if (typeof config.system === "string" && config.system.trim()) this.systemPrompts.set(config.system);
|
|
441
|
+
if (config.adapter) this.setAdapter(config.adapter);
|
|
442
|
+
this._userStages = [];
|
|
443
|
+
this._stageOverrides = /* @__PURE__ */ new Map();
|
|
444
|
+
this._disabledStages = /* @__PURE__ */ new Set();
|
|
445
|
+
this._extraConfigKeys = /* @__PURE__ */ new Map();
|
|
446
|
+
this.plugins = {};
|
|
447
|
+
this._pipeline = this._buildPipeline();
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* 校验配置中的可校验字段(model 必填由构造器负责,这里只校验存在性)
|
|
451
|
+
* @throws {ConfigurationError}
|
|
452
|
+
*/
|
|
453
|
+
_assertValidConfig(cfg) {
|
|
454
|
+
const temperature = cfg.modelParams?.temperature ?? cfg.temperature;
|
|
327
455
|
if (temperature !== void 0 && (typeof temperature !== "number" || temperature < 0 || temperature > 2)) throw new ConfigurationError(`temperature 必须在 0-2 之间,当前值: ${temperature}`);
|
|
328
|
-
const maxTokens =
|
|
456
|
+
const maxTokens = cfg.modelParams?.maxTokens ?? cfg.maxTokens;
|
|
329
457
|
if (maxTokens !== void 0 && (typeof maxTokens !== "number" || maxTokens < 1 || !Number.isInteger(maxTokens))) throw new ConfigurationError(`maxTokens 必须为正整数,当前值: ${maxTokens}`);
|
|
330
|
-
|
|
458
|
+
const model = cfg.model ?? cfg.modelParams?.model;
|
|
459
|
+
if (model !== void 0 && (typeof model !== "string" || !model.trim())) throw new ConfigurationError("model 必须是非空字符串");
|
|
331
460
|
}
|
|
332
|
-
|
|
333
|
-
|
|
461
|
+
/**
|
|
462
|
+
* 安装插件。
|
|
463
|
+
* - 插件实现 `install(chat, options)`
|
|
464
|
+
* - 若 install 返回一个对象,会挂到 **`chat.plugins[plugin.name]`**(v4.0 命名空间,插件之间不会撞名)
|
|
465
|
+
* - 插件要提供卸载时,自己在返回值里带 `uninstall()`(或调用 `plugin.uninstall(chat)`)
|
|
466
|
+
* @returns {ChatService} this
|
|
467
|
+
*/
|
|
468
|
+
use(plugin, options = {}) {
|
|
469
|
+
const api = plugin.install(this, options);
|
|
470
|
+
if (plugin.name && api && typeof api === "object") this.plugins[plugin.name] = api;
|
|
334
471
|
return this;
|
|
335
472
|
}
|
|
336
|
-
|
|
337
|
-
|
|
473
|
+
/**
|
|
474
|
+
* 往请求管道里挂一个"车间"。
|
|
475
|
+
* - phase 'beforeSend'(默认):在内部 beforeRequest 钩子之后、构建请求体之前执行
|
|
476
|
+
* - phase 'afterSend' :在内部发送(流式/非流式)完成、结果落位之后执行(可改 ctx.result)
|
|
477
|
+
* 不注册任何车间 = 行为与版本 2.8.x 完全一致。
|
|
478
|
+
* @param {PipeStage} stage
|
|
479
|
+
* @returns {ChatService} this
|
|
480
|
+
* @throws {ConfigurationError} 参数非法 / 名字被占用 / 与内部车间重名
|
|
481
|
+
*/
|
|
482
|
+
pipe(stage) {
|
|
483
|
+
if (!stage || typeof stage !== "object" || typeof stage.run !== "function") throw new ConfigurationError("pipe: 需要 { name, phase?, run(ctx) },run 必须是函数");
|
|
484
|
+
if (typeof stage.name !== "string" || !stage.name.trim()) throw new ConfigurationError("pipe: 需要 name(车间名,非空字符串)");
|
|
485
|
+
if (INTERNAL_STAGES.includes(stage.name)) throw new ConfigurationError(`pipe: "${stage.name}" 是内部车间名,请换一个名字`);
|
|
486
|
+
if (this._userStages.some((s) => s.name === stage.name)) throw new ConfigurationError(`pipe: 车间 "${stage.name}" 已存在,请先 chat.unpipe("${stage.name}")`);
|
|
487
|
+
const phase = stage.phase === "afterSend" ? "afterSend" : "beforeSend";
|
|
488
|
+
this._userStages.push({
|
|
489
|
+
name: stage.name,
|
|
490
|
+
phase,
|
|
491
|
+
run: stage.run
|
|
492
|
+
});
|
|
493
|
+
this._rebuildPipeline();
|
|
494
|
+
return this;
|
|
338
495
|
}
|
|
339
|
-
|
|
340
|
-
|
|
496
|
+
/** 移除一个用户车间(移除不存在的名字是安全的)。@returns {ChatService} this */
|
|
497
|
+
unpipe(name) {
|
|
498
|
+
const before = this._userStages.length;
|
|
499
|
+
this._userStages = this._userStages.filter((s) => s.name !== name);
|
|
500
|
+
if (this._userStages.length !== before) this._rebuildPipeline();
|
|
501
|
+
return this;
|
|
341
502
|
}
|
|
342
|
-
|
|
343
|
-
|
|
503
|
+
/** 查看当前管道里所有车间名(调试/说明用,含内部车间) */
|
|
504
|
+
get pipelineStages() {
|
|
505
|
+
return this._pipeline.names();
|
|
344
506
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
507
|
+
/** 内部步骤名(顺序即执行顺序):prepareInput / beforeSend / autoContinue / buildRequest / send */
|
|
508
|
+
get internalStages() {
|
|
509
|
+
return [...INTERNAL_STAGES];
|
|
510
|
+
}
|
|
511
|
+
/** 内部步骤的默认实现表 */
|
|
512
|
+
_defaultStageRuns() {
|
|
513
|
+
return {
|
|
514
|
+
prepareInput: (ctx) => this._stagePrepareInput(ctx),
|
|
515
|
+
beforeSend: (ctx) => this._stageBeforeSend(ctx),
|
|
516
|
+
autoContinue: (ctx) => this._stageAutoContinue(ctx),
|
|
517
|
+
buildRequest: (ctx) => this._stageBuildRequest(ctx),
|
|
518
|
+
send: (ctx) => this._stageSend(ctx)
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* 用你自己的实现**顶替**某个内部步骤。框架不再执行该步的默认行为。
|
|
523
|
+
* 例:`chat.replaceStage('send', async (ctx) => { ctx.result = await 我的发送(ctx.body) })`
|
|
524
|
+
* 想"包一层"默认实现:先 `const base = chat.getStage('send')`,再在自己的函数里调用它。
|
|
525
|
+
* @param {string} name 步骤名(见 internalStages)
|
|
526
|
+
* @param {Function} run (ctx) => void | Promise<void>
|
|
527
|
+
* @throws {ConfigurationError} 步骤名未知 / run 不是函数
|
|
528
|
+
*/
|
|
529
|
+
replaceStage(name, run) {
|
|
530
|
+
if (!INTERNAL_STAGES.includes(name)) throw new ConfigurationError(`replaceStage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(", ")}`);
|
|
531
|
+
if (typeof run !== "function") throw new ConfigurationError("replaceStage: 第二个参数必须是函数 (ctx) => void | Promise<void>");
|
|
532
|
+
this._stageOverrides.set(name, run);
|
|
533
|
+
this._rebuildPipeline();
|
|
534
|
+
return this;
|
|
535
|
+
}
|
|
536
|
+
/** 撤销替换,恢复该步骤的默认实现(不影响 unstage 的关闭状态) */
|
|
537
|
+
restoreStage(name) {
|
|
538
|
+
if (!INTERNAL_STAGES.includes(name)) throw new ConfigurationError(`restoreStage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(", ")}`);
|
|
539
|
+
this._stageOverrides.delete(name);
|
|
540
|
+
this._rebuildPipeline();
|
|
541
|
+
return this;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* **关闭**某个内部步骤(从管道里移除,不再执行)。
|
|
545
|
+
* 例:`chat.unstage('autoContinue')` —— 彻底不要"自动续写检测"这一步。
|
|
546
|
+
* 警告:关闭 prepareInput / buildRequest / send 会让流程失去必要产物(如 ctx.body)。
|
|
547
|
+
*/
|
|
548
|
+
unstage(name) {
|
|
549
|
+
if (!INTERNAL_STAGES.includes(name)) throw new ConfigurationError(`unstage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(", ")}`);
|
|
550
|
+
this._disabledStages.add(name);
|
|
551
|
+
this._rebuildPipeline();
|
|
552
|
+
return this;
|
|
553
|
+
}
|
|
554
|
+
/** 重新启用被 unstage 关闭的步骤 */
|
|
555
|
+
restage(name) {
|
|
556
|
+
if (!INTERNAL_STAGES.includes(name)) throw new ConfigurationError(`restage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(", ")}`);
|
|
557
|
+
this._disabledStages.delete(name);
|
|
558
|
+
this._rebuildPipeline();
|
|
559
|
+
return this;
|
|
560
|
+
}
|
|
561
|
+
/** 取得某个内部步骤的默认实现(用于"包一层":做自己的事,再调用它) */
|
|
562
|
+
getStage(name) {
|
|
563
|
+
if (!INTERNAL_STAGES.includes(name)) throw new ConfigurationError(`getStage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(", ")}`);
|
|
564
|
+
return this._defaultStageRuns()[name];
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* 重建内部管道:内部步骤(可被 replaceStage 顶替 / 被 unstage 关闭)+ 用户车间按 phase 插入
|
|
568
|
+
* 顺序:prepareInput → beforeSend → [用户 beforeSend 车间] → autoContinue → buildRequest → send → [用户 afterSend 车间]
|
|
569
|
+
*/
|
|
570
|
+
_buildPipeline() {
|
|
571
|
+
const p = new Pipeline();
|
|
572
|
+
const defaults = this._defaultStageRuns();
|
|
573
|
+
const insertUsers = (phase) => {
|
|
574
|
+
for (const s of this._userStages) if (s.phase === phase) p.register({
|
|
575
|
+
name: s.name,
|
|
576
|
+
run: (ctx) => s.run(ctx)
|
|
353
577
|
});
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
578
|
+
};
|
|
579
|
+
for (const name of INTERNAL_STAGES) {
|
|
580
|
+
if (!this._disabledStages.has(name)) p.register({
|
|
581
|
+
name,
|
|
582
|
+
run: this._stageOverrides.get(name) || defaults[name]
|
|
358
583
|
});
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
this.messages.update(target.id, { prefix: void 0 });
|
|
362
|
-
throw err;
|
|
584
|
+
if (name === "beforeSend") insertUsers("beforeSend");
|
|
585
|
+
if (name === "send") insertUsers("afterSend");
|
|
363
586
|
}
|
|
587
|
+
return p;
|
|
364
588
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
589
|
+
_rebuildPipeline() {
|
|
590
|
+
this._pipeline = this._buildPipeline();
|
|
591
|
+
}
|
|
592
|
+
setAdapter(adapter) {
|
|
593
|
+
assertAdapter(adapter);
|
|
594
|
+
this._adapter = adapter;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* 中断请求(v4.0:按请求隔离)。
|
|
598
|
+
* - 不传参数:中断**所有**活跃请求(旧版只会中断"最后一个",多请求时会中断错的那个)
|
|
599
|
+
* - 传 requestId:只中断那一个(id 见 `chat.activeRequests`)
|
|
600
|
+
*/
|
|
601
|
+
abort(requestId) {
|
|
602
|
+
if (requestId) {
|
|
603
|
+
const controller = this._activeRequests.get(requestId);
|
|
604
|
+
if (controller) controller.abort();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
for (const controller of this._activeRequests.values()) controller.abort();
|
|
608
|
+
}
|
|
609
|
+
/** 是否有请求正在生成中 */
|
|
610
|
+
get isGenerating() {
|
|
611
|
+
return this._activeRequests.size > 0;
|
|
612
|
+
}
|
|
613
|
+
/** 当前活跃请求的 id 列表(调试 / 精确中断用) */
|
|
614
|
+
get activeRequests() {
|
|
615
|
+
return [...this._activeRequests.keys()];
|
|
616
|
+
}
|
|
617
|
+
/** 内部:登记一个请求,返回 { id, signal } */
|
|
618
|
+
_beginRequest() {
|
|
619
|
+
const id = "req_" + ++this._requestSeq;
|
|
620
|
+
const controller = new AbortController();
|
|
621
|
+
this._activeRequests.set(id, controller);
|
|
622
|
+
return {
|
|
623
|
+
id,
|
|
624
|
+
signal: controller.signal
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
/** 内部:注销一个请求 */
|
|
628
|
+
_endRequest(id) {
|
|
629
|
+
this._activeRequests.delete(id);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* 注册"运行时可改的配置字段"(v4.0)。
|
|
633
|
+
* 让插件自带配置(如 continuation 的 autoContinue)不必回头改核心白名单。
|
|
634
|
+
* @param {string[]|Object<string, Function>} keys 字段名数组,或 { 字段名: 校验器(value)=>boolean }
|
|
635
|
+
* @returns {ChatService} this
|
|
636
|
+
* @throws {ConfigurationError} 字段名非法 / 校验器不是函数
|
|
637
|
+
*/
|
|
638
|
+
registerConfigKeys(keys) {
|
|
639
|
+
const entries = Array.isArray(keys) ? keys.map((k) => [k, null]) : Object.entries(keys || {});
|
|
640
|
+
for (const [key, validator] of entries) {
|
|
641
|
+
if (typeof key !== "string" || !key.trim()) throw new ConfigurationError("registerConfigKeys: 字段名必须是非空字符串");
|
|
642
|
+
if (validator != null && typeof validator !== "function") throw new ConfigurationError(`registerConfigKeys: "${key}" 的校验器必须是函数`);
|
|
643
|
+
this._extraConfigKeys.set(key.trim(), validator || null);
|
|
394
644
|
}
|
|
645
|
+
return this;
|
|
646
|
+
}
|
|
647
|
+
/** 注销运行时可改字段(插件卸载时调用)@returns {ChatService} this */
|
|
648
|
+
unregisterConfigKeys(keys) {
|
|
649
|
+
for (const k of Array.isArray(keys) ? keys : [keys]) this._extraConfigKeys.delete(k);
|
|
650
|
+
return this;
|
|
395
651
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (last && (last.role === "assistant" || last._ephemeral)) return last;
|
|
400
|
-
throw new Error("最后一条消息不是 assistant,无法续写");
|
|
652
|
+
/** 当前允许在运行时修改的配置字段(内置基础字段 + 插件注册) */
|
|
653
|
+
get configKeys() {
|
|
654
|
+
return [...SESSION_CONFIG_KEYS, ...this._extraConfigKeys.keys()];
|
|
401
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* 运行时修改配置(仅"可改字段":内置基础字段 + 插件注册字段)。
|
|
658
|
+
*
|
|
659
|
+
* - 运输层(apiKey/baseUrl/headers):归适配器,要改就换适配器实例
|
|
660
|
+
* - 请求参数默认值:走适配器工厂 options 或请求级覆盖(chat.send(x, params))
|
|
661
|
+
* - 插件自有配置:插件用 `registerConfigKeys` 登记后才可改(工厂 options 仍是首选)
|
|
662
|
+
*
|
|
663
|
+
* 与构造器一样经过校验,不能注入非法值。
|
|
664
|
+
* @throws {ConfigurationError} 非白名单字段、非法值、或插件校验器拒绝
|
|
665
|
+
*/
|
|
402
666
|
updateConfig(partial) {
|
|
667
|
+
const allowed = this.configKeys;
|
|
668
|
+
for (const key of Object.keys(partial)) if (!allowed.includes(key)) throw new ConfigurationError(`updateConfig 不支持修改 "${key}"。允许的字段: ${allowed.join(", ")}`);
|
|
669
|
+
this._assertValidConfig(partial);
|
|
670
|
+
for (const [key, validator] of this._extraConfigKeys) if (validator && key in partial && validator(partial[key]) === false) throw new ConfigurationError(`updateConfig: 字段 "${key}" 的值不合法: ${JSON.stringify(partial[key])}`);
|
|
403
671
|
Object.assign(this.config, partial);
|
|
404
672
|
if (typeof partial.system === "string") this.systemPrompts.set(partial.system);
|
|
405
673
|
this.emit("config-updated", {
|
|
@@ -407,36 +675,73 @@ var ChatService = class extends EventEmitter {
|
|
|
407
675
|
timestamp: Date.now()
|
|
408
676
|
});
|
|
409
677
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
678
|
+
/**
|
|
679
|
+
* 合并请求参数(近者优先):
|
|
680
|
+
* 适配器默认(getRequestDefaults) ← 会话配置(this.config) ← 本次覆盖(params)
|
|
681
|
+
* modelParams 深层合并;平铺便捷键(temperature/maxTokens/reasoningEffort)自动折叠进 modelParams
|
|
682
|
+
*/
|
|
683
|
+
_mergeRequestConfig(params = {}) {
|
|
684
|
+
const adapterDefaults = this._adapter?.getRequestDefaults?.() || {};
|
|
685
|
+
const requestConfig = {
|
|
686
|
+
...adapterDefaults,
|
|
687
|
+
...this.config,
|
|
688
|
+
...params
|
|
689
|
+
};
|
|
690
|
+
const modelParams = {
|
|
691
|
+
...adapterDefaults.modelParams || {},
|
|
692
|
+
...this.config.modelParams || {},
|
|
693
|
+
...params.modelParams || {}
|
|
694
|
+
};
|
|
695
|
+
for (const key of [
|
|
696
|
+
"temperature",
|
|
697
|
+
"maxTokens",
|
|
698
|
+
"reasoningEffort"
|
|
699
|
+
]) {
|
|
700
|
+
const value = params[key] ?? this.config[key] ?? adapterDefaults[key];
|
|
701
|
+
if (value !== void 0) modelParams[key] = value;
|
|
427
702
|
}
|
|
428
|
-
|
|
429
|
-
|
|
703
|
+
requestConfig.modelParams = modelParams;
|
|
704
|
+
return requestConfig;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* 内部调度(v3.0 提案 · 阶段 1):造一辆小车 ctx,开过内部管道,返回结果。
|
|
708
|
+
* 行为与旧版 _request 完全一致,只是把固定线拆成了车间(见 _stage* 方法)。
|
|
709
|
+
*
|
|
710
|
+
* ctx 字段(车间可见):
|
|
711
|
+
* options —— 原始请求选项
|
|
712
|
+
* userInput —— 用户输入
|
|
713
|
+
* addUser —— 是否新增用户消息
|
|
714
|
+
* messages —— MessageStore(可读可改)
|
|
715
|
+
* systemPrompts —— SystemPromptStore
|
|
716
|
+
* config —— 合并后的请求级配置(prepareInput 车间产出)
|
|
717
|
+
* isStream —— 本次是否流式
|
|
718
|
+
* onProgress —— 流式进度回调(可为 null)
|
|
719
|
+
* onDone —— 流式完成回调(可为 null)
|
|
720
|
+
* mergeToEntry —— 续写目标 id(空 = 普通发送)
|
|
721
|
+
* body —— adapter 构建出的请求体(buildRequest 车间产出)
|
|
722
|
+
* result —— 最终结果(send 车间产出)
|
|
723
|
+
*/
|
|
724
|
+
async _request(options = {}) {
|
|
725
|
+
const ctx = {
|
|
726
|
+
options,
|
|
727
|
+
userInput: options.userInput,
|
|
728
|
+
addUser: options.addUser !== false,
|
|
729
|
+
messages: this.messages,
|
|
730
|
+
systemPrompts: this.systemPrompts,
|
|
731
|
+
config: null,
|
|
732
|
+
isStream: !!options.isStream,
|
|
733
|
+
onProgress: options.onProgress || null,
|
|
734
|
+
onDone: options.onDone || null,
|
|
735
|
+
mergeToEntry: options.mergeToEntry || null,
|
|
736
|
+
signal: options.signal || null,
|
|
737
|
+
body: null,
|
|
738
|
+
result: null
|
|
739
|
+
};
|
|
430
740
|
try {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
onProgress,
|
|
434
|
-
onDone,
|
|
435
|
-
mergeToEntry,
|
|
436
|
-
maxRetries: retryCfg.maxRetries ?? 0,
|
|
437
|
-
retryDelay: retryCfg.retryDelay ?? 1e3
|
|
438
|
-
});
|
|
741
|
+
await this._pipeline.run(ctx);
|
|
742
|
+
return ctx.result;
|
|
439
743
|
} catch (error) {
|
|
744
|
+
if (error.name === "AbortError") throw error;
|
|
440
745
|
this.emit("error", {
|
|
441
746
|
error,
|
|
442
747
|
timestamp: Date.now()
|
|
@@ -444,6 +749,47 @@ var ChatService = class extends EventEmitter {
|
|
|
444
749
|
throw error;
|
|
445
750
|
}
|
|
446
751
|
}
|
|
752
|
+
/** 车间 1:加用户消息 → 合并请求参数 → 广播 sending */
|
|
753
|
+
async _stagePrepareInput(ctx) {
|
|
754
|
+
this._addUserMessage(ctx.userInput, ctx.addUser);
|
|
755
|
+
ctx.config = this._mergeRequestConfig(ctx.options.params);
|
|
756
|
+
this.emit("sending", {
|
|
757
|
+
addUser: ctx.addUser,
|
|
758
|
+
userInput: ctx.userInput,
|
|
759
|
+
timestamp: Date.now()
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* 车间 2:发送前 —— **v4.0 起默认什么都不做**。
|
|
764
|
+
* 需要在这里做事的(注入、改写消息/配置):`chat.replaceStage('beforeSend', fn)`
|
|
765
|
+
* 或挂一个 `chat.pipe({ phase: 'beforeSend' })` 车间(在它之后执行)。
|
|
766
|
+
* 旧的 `_hooks.on('beforeRequest')` 兼容桥已于 v4.0 移除。
|
|
767
|
+
*/
|
|
768
|
+
async _stageBeforeSend(ctx) {}
|
|
769
|
+
/**
|
|
770
|
+
* 车间 3:自动续写检测 —— **v4.0 起默认什么都不做**。
|
|
771
|
+
* 续写是玩法:需要它的装上 continuation 插件,由插件 replaceStage('autoContinue', ...) 接管。
|
|
772
|
+
* 步骤名与位置保留,方便插件替换、也方便使用者 `unstage('autoContinue')` 明确移除。
|
|
773
|
+
*/
|
|
774
|
+
async _stageAutoContinue(ctx) {}
|
|
775
|
+
/** 车间 4:构建请求体 */
|
|
776
|
+
async _stageBuildRequest(ctx) {
|
|
777
|
+
ctx.body = this._adapter.buildRequest(ctx.messages.getAll(), ctx.config, ctx.systemPrompts.getEnabled());
|
|
778
|
+
}
|
|
779
|
+
/** 车间 5:发送(流式/非流式 + 重试 + 占位消息 + _processResponse 工具钩子) */
|
|
780
|
+
async _stageSend(ctx) {
|
|
781
|
+
const retryCfg = this.config.retry || {};
|
|
782
|
+
ctx.result = await this._withRetry(ctx.body, {
|
|
783
|
+
isStream: ctx.isStream,
|
|
784
|
+
onProgress: ctx.onProgress,
|
|
785
|
+
onDone: ctx.onDone,
|
|
786
|
+
mergeToEntry: ctx.mergeToEntry,
|
|
787
|
+
config: ctx.config,
|
|
788
|
+
maxRetries: retryCfg.maxRetries ?? 0,
|
|
789
|
+
retryDelay: retryCfg.retryDelay ?? 1e3,
|
|
790
|
+
signal: ctx.signal
|
|
791
|
+
});
|
|
792
|
+
}
|
|
447
793
|
_addUserMessage(userInput, addUser) {
|
|
448
794
|
if (addUser && userInput !== void 0) {
|
|
449
795
|
const msg = typeof userInput === "string" ? {
|
|
@@ -457,9 +803,9 @@ var ChatService = class extends EventEmitter {
|
|
|
457
803
|
}
|
|
458
804
|
}
|
|
459
805
|
/** retry 循环,占位消息只 push 一次 */
|
|
460
|
-
async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, maxRetries, retryDelay }) {
|
|
806
|
+
async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, config, maxRetries, retryDelay, signal }) {
|
|
461
807
|
let lastError = null;
|
|
462
|
-
const adapterOptions = { signal
|
|
808
|
+
const adapterOptions = { signal };
|
|
463
809
|
let placeholder = null;
|
|
464
810
|
let base = null;
|
|
465
811
|
if (isStream) {
|
|
@@ -487,54 +833,53 @@ var ChatService = class extends EventEmitter {
|
|
|
487
833
|
await new Promise((r) => setTimeout(r, retryDelay));
|
|
488
834
|
}
|
|
489
835
|
try {
|
|
490
|
-
this._isGenerating = true;
|
|
491
836
|
if (isStream) return await this._stream(body, adapterOptions, {
|
|
492
837
|
placeholder,
|
|
493
838
|
base,
|
|
494
839
|
mergeToEntry,
|
|
495
840
|
onProgress,
|
|
496
|
-
onDone
|
|
841
|
+
onDone,
|
|
842
|
+
config
|
|
497
843
|
});
|
|
498
844
|
else {
|
|
499
|
-
const resp = await this._adapter.send(body,
|
|
500
|
-
|
|
501
|
-
if (this._processResponse && !mergeToEntry) result = await this._processResponse(result, { isStream: false });
|
|
502
|
-
return result;
|
|
845
|
+
const resp = await this._adapter.send(body, config, adapterOptions);
|
|
846
|
+
return this._handleResult(this._adapter.parseResponse(resp), mergeToEntry);
|
|
503
847
|
}
|
|
504
848
|
} catch (error) {
|
|
505
849
|
lastError = error;
|
|
506
|
-
if (error.name === "AbortError" ||
|
|
507
|
-
this._isGenerating = false;
|
|
508
|
-
throw error;
|
|
509
|
-
}
|
|
850
|
+
if (error.name === "AbortError" || signal?.aborted) throw error;
|
|
510
851
|
if (error instanceof NetworkError && attempt < maxRetries) continue;
|
|
511
|
-
this._isGenerating = false;
|
|
512
852
|
throw error;
|
|
513
853
|
}
|
|
514
854
|
}
|
|
515
|
-
this._isGenerating = false;
|
|
516
855
|
throw lastError;
|
|
517
856
|
}
|
|
518
|
-
/**
|
|
519
|
-
|
|
857
|
+
/**
|
|
858
|
+
* 流式处理:ChatService 维护占位消息,adapter 只负责解析 SSE
|
|
859
|
+
*
|
|
860
|
+
* 参数说明:
|
|
861
|
+
* - placeholder:流式期间实时更新的目标消息(续写时 = target 本身,普通发送时 = 新建的空消息)
|
|
862
|
+
* - base:续写时保存的旧内容快照,用于和 API 新内容拼接
|
|
863
|
+
* - mergeToEntry:续写目标的 id,有值时走续写逻辑
|
|
864
|
+
* - onProgress:外部回调,每收到一个 chunk 触发
|
|
865
|
+
* - onDone:流结束回调
|
|
866
|
+
*/
|
|
867
|
+
async _stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone, config }) {
|
|
520
868
|
let finalMsg = null;
|
|
521
|
-
await this._adapter.stream(body,
|
|
522
|
-
placeholder.content = snap.content || "";
|
|
523
|
-
if (snap.reasoningContent) placeholder.reasoningContent = snap.reasoningContent;
|
|
869
|
+
await this._adapter.stream(body, config, (snap, delta) => {
|
|
870
|
+
placeholder.content = mergeToEntry && base ? base.content + (snap.content || "") : snap.content || "";
|
|
871
|
+
if (snap.reasoningContent) placeholder.reasoningContent = mergeToEntry && base ? base.reasoning + snap.reasoningContent : snap.reasoningContent;
|
|
524
872
|
if (snap.toolCalls) placeholder.toolCalls = [...snap.toolCalls];
|
|
525
873
|
this.emit("stream-progress", snap);
|
|
526
|
-
if (onProgress) onProgress(snap);
|
|
874
|
+
if (onProgress) onProgress(snap, delta);
|
|
527
875
|
}, async (final) => {
|
|
528
876
|
finalMsg = final;
|
|
529
|
-
if (this._processResponse && !mergeToEntry) finalMsg = await this._processResponse(finalMsg, {
|
|
530
|
-
isStream: true,
|
|
531
|
-
onProgress,
|
|
532
|
-
onDone
|
|
533
|
-
});
|
|
534
877
|
if (mergeToEntry && base) {
|
|
535
878
|
const changes = {
|
|
536
879
|
content: base.content + (final.content || ""),
|
|
537
|
-
_complete: true
|
|
880
|
+
_complete: true,
|
|
881
|
+
_ephemeral: false,
|
|
882
|
+
prefix: void 0
|
|
538
883
|
};
|
|
539
884
|
if (final.reasoningContent) changes.reasoningContent = base.reasoning + final.reasoningContent;
|
|
540
885
|
if (final.toolCalls) changes.toolCalls = final.toolCalls;
|
|
@@ -561,7 +906,9 @@ var ChatService = class extends EventEmitter {
|
|
|
561
906
|
}
|
|
562
907
|
const changes = {
|
|
563
908
|
content: (entry.content || "") + (assistantMsg.content || ""),
|
|
564
|
-
_complete: true
|
|
909
|
+
_complete: true,
|
|
910
|
+
_ephemeral: false,
|
|
911
|
+
prefix: void 0
|
|
565
912
|
};
|
|
566
913
|
if (assistantMsg.reasoningContent) changes.reasoningContent = (entry.reasoningContent || "") + assistantMsg.reasoningContent;
|
|
567
914
|
if (assistantMsg.toolCalls) changes.toolCalls = assistantMsg.toolCalls;
|
|
@@ -574,61 +921,148 @@ var ChatService = class extends EventEmitter {
|
|
|
574
921
|
this.emit("message", assistantMsg);
|
|
575
922
|
return assistantMsg;
|
|
576
923
|
}
|
|
577
|
-
|
|
578
|
-
|
|
924
|
+
/**
|
|
925
|
+
* 发送消息。
|
|
926
|
+
* @param {string|Object} userInput - 用户输入
|
|
927
|
+
* @param {Object} [params] - 请求级参数覆盖(model/temperature/modelParams 等,仅本次生效)
|
|
928
|
+
*/
|
|
929
|
+
async send(userInput, params) {
|
|
930
|
+
const { id, signal } = this._beginRequest();
|
|
579
931
|
try {
|
|
580
932
|
return await this._request({
|
|
581
933
|
userInput,
|
|
582
934
|
addUser: true,
|
|
583
|
-
isStream: false
|
|
935
|
+
isStream: false,
|
|
936
|
+
params,
|
|
937
|
+
signal
|
|
584
938
|
});
|
|
939
|
+
} catch (err) {
|
|
940
|
+
if (err.name === "AbortError") {
|
|
941
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
throw err;
|
|
585
945
|
} finally {
|
|
586
|
-
this.
|
|
587
|
-
this._isGenerating = false;
|
|
946
|
+
this._endRequest(id);
|
|
588
947
|
}
|
|
589
948
|
}
|
|
590
|
-
|
|
591
|
-
|
|
949
|
+
/**
|
|
950
|
+
* 流式发送。
|
|
951
|
+
* @param {string|Object} userInput - 用户输入
|
|
952
|
+
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
953
|
+
* @param {Function} [onProgress] - 流式进度回调
|
|
954
|
+
* @param {Function} [onDone] - 流式完成回调
|
|
955
|
+
*/
|
|
956
|
+
async stream(userInput, params, onProgress, onDone) {
|
|
957
|
+
if (typeof params === "function") {
|
|
958
|
+
onDone = onProgress;
|
|
959
|
+
onProgress = params;
|
|
960
|
+
params = void 0;
|
|
961
|
+
}
|
|
962
|
+
const { id, signal } = this._beginRequest();
|
|
592
963
|
try {
|
|
593
964
|
return await this._request({
|
|
594
965
|
userInput,
|
|
595
966
|
addUser: true,
|
|
596
967
|
isStream: true,
|
|
597
968
|
onProgress,
|
|
598
|
-
onDone
|
|
969
|
+
onDone,
|
|
970
|
+
params,
|
|
971
|
+
signal
|
|
599
972
|
});
|
|
973
|
+
} catch (err) {
|
|
974
|
+
if (err.name === "AbortError") {
|
|
975
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
throw err;
|
|
600
979
|
} finally {
|
|
601
|
-
this.
|
|
602
|
-
this._isGenerating = false;
|
|
980
|
+
this._endRequest(id);
|
|
603
981
|
}
|
|
604
982
|
}
|
|
605
|
-
|
|
606
|
-
|
|
983
|
+
/**
|
|
984
|
+
* 重发当前消息(不加用户消息)。
|
|
985
|
+
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
986
|
+
* @param {Object} [options] - { mergeToEntry } 可选:把结果合并进指定的已有消息(续写等场景)
|
|
987
|
+
*/
|
|
988
|
+
async sendExisting(params, options = {}) {
|
|
989
|
+
const mergeToEntry = options?.mergeToEntry || null;
|
|
990
|
+
const { id, signal } = this._beginRequest();
|
|
607
991
|
try {
|
|
608
992
|
return await this._request({
|
|
609
993
|
addUser: false,
|
|
610
|
-
isStream: false
|
|
994
|
+
isStream: false,
|
|
995
|
+
params,
|
|
996
|
+
mergeToEntry,
|
|
997
|
+
signal
|
|
611
998
|
});
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
if (err.name === "AbortError") {
|
|
1001
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
throw err;
|
|
612
1005
|
} finally {
|
|
613
|
-
this.
|
|
614
|
-
this._isGenerating = false;
|
|
1006
|
+
this._endRequest(id);
|
|
615
1007
|
}
|
|
616
1008
|
}
|
|
617
|
-
|
|
618
|
-
|
|
1009
|
+
/**
|
|
1010
|
+
* 流式重发(不加用户消息)。
|
|
1011
|
+
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
1012
|
+
* @param {Object} [options] - { mergeToEntry } 可选:把结果合并进指定的已有消息
|
|
1013
|
+
* @param {Function} [onProgress] - 流式进度回调
|
|
1014
|
+
* @param {Function} [onDone] - 流式完成回调
|
|
1015
|
+
*/
|
|
1016
|
+
async sendExistingStream(params, options, onProgress, onDone) {
|
|
1017
|
+
if (typeof params === "function") {
|
|
1018
|
+
onDone = options;
|
|
1019
|
+
onProgress = params;
|
|
1020
|
+
options = {};
|
|
1021
|
+
params = void 0;
|
|
1022
|
+
} else if (typeof options === "function") {
|
|
1023
|
+
onDone = onProgress;
|
|
1024
|
+
onProgress = options;
|
|
1025
|
+
options = {};
|
|
1026
|
+
}
|
|
1027
|
+
const mergeToEntry = options?.mergeToEntry || null;
|
|
1028
|
+
const { id, signal } = this._beginRequest();
|
|
619
1029
|
try {
|
|
620
1030
|
return await this._request({
|
|
621
1031
|
addUser: false,
|
|
622
1032
|
isStream: true,
|
|
623
1033
|
onProgress,
|
|
624
|
-
onDone
|
|
1034
|
+
onDone,
|
|
1035
|
+
params,
|
|
1036
|
+
mergeToEntry,
|
|
1037
|
+
signal
|
|
625
1038
|
});
|
|
1039
|
+
} catch (err) {
|
|
1040
|
+
if (err.name === "AbortError") {
|
|
1041
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
throw err;
|
|
626
1045
|
} finally {
|
|
627
|
-
this.
|
|
628
|
-
this._isGenerating = false;
|
|
1046
|
+
this._endRequest(id);
|
|
629
1047
|
}
|
|
630
1048
|
}
|
|
631
1049
|
};
|
|
1050
|
+
/**
|
|
1051
|
+
* 校验 adapter 是否满足协议(v3.0 · 阶段 3)。
|
|
1052
|
+
* 缺少必要方法时抛 ConfigurationError,并列出缺少的方法名——写"同级适配器"不再靠猜。
|
|
1053
|
+
* @param {*} adapter
|
|
1054
|
+
* @throws {ConfigurationError}
|
|
1055
|
+
*/
|
|
1056
|
+
function assertAdapter(adapter) {
|
|
1057
|
+
if (!adapter || typeof adapter !== "object") throw new ConfigurationError("setAdapter: adapter 必须是对象(如 openaiAdapter / createOpenAIAdapter() 实例)");
|
|
1058
|
+
const missing = [
|
|
1059
|
+
"buildRequest",
|
|
1060
|
+
"send",
|
|
1061
|
+
"stream",
|
|
1062
|
+
"parseResponse"
|
|
1063
|
+
].filter((k) => typeof adapter[k] !== "function");
|
|
1064
|
+
if (missing.length) throw new ConfigurationError(`setAdapter: adapter 缺少必要方法: ${missing.join(", ")}(协议见 ChatService.js 顶部的 @typedef ChatAdapter)`);
|
|
1065
|
+
}
|
|
632
1066
|
//#endregion
|
|
633
1067
|
//#region src/utils/url.js
|
|
634
1068
|
/**
|
|
@@ -660,26 +1094,23 @@ function isString(value) {
|
|
|
660
1094
|
//#endregion
|
|
661
1095
|
//#region src/utils/MessageFormatter.js
|
|
662
1096
|
/**
|
|
663
|
-
* MessageFormatter — 可注册的消息格式转换器
|
|
1097
|
+
* MessageFormatter — 可注册的消息格式转换器 + 可注册的过滤器链(v4.0)
|
|
664
1098
|
*
|
|
665
1099
|
* 职责:
|
|
666
|
-
* 1. 内置
|
|
1100
|
+
* 1. 内置 OpenAI 兼容格式的转换逻辑
|
|
667
1101
|
* 2. 支持 register(name, fn) 注册自定义格式(如 Anthropic、Gemini 等)
|
|
668
|
-
* 3.
|
|
669
|
-
*
|
|
1102
|
+
* 3. v4.0:消息"保留 / 丢弃"规则改为可注册的过滤器链(registerFilter)
|
|
1103
|
+
* —— 原来硬编码在格式里的 ephemeral / system / 空内容规则,现在都是过滤器;
|
|
1104
|
+
* 业务插件(如 continuation)可以同名覆盖内置过滤器,接管规则。
|
|
670
1105
|
*
|
|
671
1106
|
* 用法:
|
|
672
1107
|
* import { MessageFormatter } from 'my-ai-chat-framework';
|
|
673
1108
|
*
|
|
674
1109
|
* // 使用内置格式
|
|
675
|
-
* const msgs = MessageFormatter.format({
|
|
676
|
-
* messages, systemPrompts, capabilities, resolveImage
|
|
677
|
-
* }); // 默认 'openai'
|
|
1110
|
+
* const msgs = MessageFormatter.format({ messages, systemPrompts, capabilities, resolveImage });
|
|
678
1111
|
*
|
|
679
|
-
* //
|
|
680
|
-
* MessageFormatter.
|
|
681
|
-
* // 返回 Anthropic 格式的消息数组
|
|
682
|
-
* });
|
|
1112
|
+
* // 注册自己的过滤器(决定哪些消息进入请求)
|
|
1113
|
+
* MessageFormatter.registerFilter('my-rule', (msg, ctx) => msg.role === 'system' ? null : msg);
|
|
683
1114
|
*/
|
|
684
1115
|
var IS_URL = /^https?:\/\//i;
|
|
685
1116
|
function defaultResolveImage(imageId) {
|
|
@@ -708,7 +1139,23 @@ function buildMultimodalContent(textContent, images, resolveImage) {
|
|
|
708
1139
|
}
|
|
709
1140
|
return content;
|
|
710
1141
|
}
|
|
711
|
-
/**
|
|
1142
|
+
/** system 角色消息不进对话数组(system 由 systemPrompts 统一置顶) */
|
|
1143
|
+
function filterSystemDrop(msg) {
|
|
1144
|
+
if (msg.role === "system" && !msg._ephemeral) return null;
|
|
1145
|
+
return msg;
|
|
1146
|
+
}
|
|
1147
|
+
/** 空内容清理:无文本、无图片的消息丢弃(tool、带 toolCalls 的 assistant、prefix 空消息例外) */
|
|
1148
|
+
function filterEmptyDrop(msg) {
|
|
1149
|
+
if (msg.role === "system" || msg.role === "tool") return msg;
|
|
1150
|
+
if (msg.role === "assistant" && msg.toolCalls?.length) return msg;
|
|
1151
|
+
const hasText = msg.content && isString(msg.content) && msg.content.trim();
|
|
1152
|
+
const hasImages = Array.isArray(msg.images) && msg.images.length > 0;
|
|
1153
|
+
if (!hasText && !hasImages) {
|
|
1154
|
+
if (msg.role === "assistant" && msg.prefix) return msg;
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
return msg;
|
|
1158
|
+
}
|
|
712
1159
|
function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
|
|
713
1160
|
const rImg = typeof resolveImage === "function" ? resolveImage : defaultResolveImage;
|
|
714
1161
|
const result = [];
|
|
@@ -716,15 +1163,8 @@ function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
|
|
|
716
1163
|
role: "system",
|
|
717
1164
|
content: sp.content.trim()
|
|
718
1165
|
});
|
|
719
|
-
|
|
720
|
-
for (
|
|
721
|
-
else break;
|
|
722
|
-
for (let i = 0; i < messages.length; i++) {
|
|
723
|
-
const msg = messages[i];
|
|
724
|
-
if (msg._ephemeral) {
|
|
725
|
-
if (i < ephemEnd) continue;
|
|
726
|
-
}
|
|
727
|
-
if (msg.role === "system" && !msg._ephemeral) continue;
|
|
1166
|
+
const kept = MessageFormatter._runFilters(messages, capabilities || {});
|
|
1167
|
+
for (const msg of kept) {
|
|
728
1168
|
if (msg.role === "tool") {
|
|
729
1169
|
result.push({
|
|
730
1170
|
role: "tool",
|
|
@@ -760,7 +1200,7 @@ function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
|
|
|
760
1200
|
}
|
|
761
1201
|
const entry = { role: msg.role };
|
|
762
1202
|
if (hasImages) {
|
|
763
|
-
if (!capabilities?.vision) throw new
|
|
1203
|
+
if (!capabilities?.vision) throw new ValidationError("[MessageFormatter] 消息包含图片但模型不支持视觉(capabilities.vision=false)。请切换模型或移除图片。");
|
|
764
1204
|
entry.content = buildMultimodalContent(msg.content || "", msg.images, rImg);
|
|
765
1205
|
} else entry.content = msg.content;
|
|
766
1206
|
if (msg.prefix) entry.prefix = true;
|
|
@@ -770,22 +1210,71 @@ function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
|
|
|
770
1210
|
}
|
|
771
1211
|
var MessageFormatter = {
|
|
772
1212
|
_formats: new Map([["openai", toOpenAI]]),
|
|
1213
|
+
_filters: /* @__PURE__ */ new Map(),
|
|
1214
|
+
_filterOrder: 0,
|
|
773
1215
|
register(name, fn) {
|
|
774
|
-
if (!name || typeof name !== "string" || !name.trim()) throw new
|
|
775
|
-
if (typeof fn !== "function") throw new
|
|
1216
|
+
if (!name || typeof name !== "string" || !name.trim()) throw new ValidationError("[MessageFormatter] 格式名称必须是非空字符串");
|
|
1217
|
+
if (typeof fn !== "function") throw new ValidationError("[MessageFormatter] 转换函数必须是 function");
|
|
776
1218
|
this._formats.set(name.trim(), fn);
|
|
777
1219
|
},
|
|
778
1220
|
unregister(name) {
|
|
779
|
-
if (name === "openai") throw new
|
|
1221
|
+
if (name === "openai") throw new ValidationError("[MessageFormatter] 内置格式 \"openai\" 不可移除");
|
|
780
1222
|
this._formats.delete(name);
|
|
781
1223
|
},
|
|
782
1224
|
listFormats() {
|
|
783
1225
|
return [...this._formats.keys()];
|
|
784
1226
|
},
|
|
1227
|
+
registerFilter(name, fn, options = {}) {
|
|
1228
|
+
if (!name || typeof name !== "string" || !name.trim()) throw new ValidationError("[MessageFormatter] 过滤器名称必须是非空字符串");
|
|
1229
|
+
if (typeof fn !== "function") throw new ValidationError("[MessageFormatter] 过滤器必须是 function");
|
|
1230
|
+
const key = name.trim();
|
|
1231
|
+
const priority = typeof options.priority === "number" ? options.priority : 100;
|
|
1232
|
+
const prev = this._filters.get(key);
|
|
1233
|
+
this._filters.set(key, {
|
|
1234
|
+
name: key,
|
|
1235
|
+
fn,
|
|
1236
|
+
priority,
|
|
1237
|
+
order: ++this._filterOrder
|
|
1238
|
+
});
|
|
1239
|
+
return prev ? prev.fn : void 0;
|
|
1240
|
+
},
|
|
1241
|
+
unregisterFilter(name) {
|
|
1242
|
+
return this._filters.delete(name);
|
|
1243
|
+
},
|
|
1244
|
+
listFilters() {
|
|
1245
|
+
return [...this._filters.values()].sort((a, b) => a.priority - b.priority || a.order - b.order).map((f) => ({
|
|
1246
|
+
name: f.name,
|
|
1247
|
+
priority: f.priority
|
|
1248
|
+
}));
|
|
1249
|
+
},
|
|
1250
|
+
_runFilters(messages, capabilities = {}) {
|
|
1251
|
+
if (this._filters.size === 0) return [...messages];
|
|
1252
|
+
const list = [...this._filters.values()].sort((a, b) => a.priority - b.priority || a.order - b.order);
|
|
1253
|
+
const out = [];
|
|
1254
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1255
|
+
let msg = messages[i];
|
|
1256
|
+
const ctx = {
|
|
1257
|
+
index: i,
|
|
1258
|
+
total: messages.length,
|
|
1259
|
+
messages,
|
|
1260
|
+
capabilities
|
|
1261
|
+
};
|
|
1262
|
+
let dropped = false;
|
|
1263
|
+
for (const f of list) {
|
|
1264
|
+
msg = f.fn(msg, ctx);
|
|
1265
|
+
if (msg === null || msg === void 0) {
|
|
1266
|
+
dropped = true;
|
|
1267
|
+
break;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (!dropped) out.push(msg);
|
|
1271
|
+
}
|
|
1272
|
+
return out;
|
|
1273
|
+
},
|
|
785
1274
|
format(options = {}) {
|
|
786
1275
|
const formatName = options.format || "openai";
|
|
787
1276
|
const fn = this._formats.get(formatName);
|
|
788
|
-
if (!fn) throw new
|
|
1277
|
+
if (!fn) throw new ValidationError(`[MessageFormatter] 未知格式 "${formatName}"。可用格式: ${[...this._formats.keys()].join(", ")}`);
|
|
789
1278
|
return fn({
|
|
790
1279
|
messages: options.messages || [],
|
|
791
1280
|
systemPrompts: options.systemPrompts || [],
|
|
@@ -794,6 +1283,8 @@ var MessageFormatter = {
|
|
|
794
1283
|
});
|
|
795
1284
|
}
|
|
796
1285
|
};
|
|
1286
|
+
MessageFormatter.registerFilter("system-drop", filterSystemDrop, { priority: 900 });
|
|
1287
|
+
MessageFormatter.registerFilter("empty-drop", filterEmptyDrop, { priority: 910 });
|
|
797
1288
|
function assembleMessages(options = {}) {
|
|
798
1289
|
return MessageFormatter.format({
|
|
799
1290
|
...options,
|
|
@@ -806,17 +1297,48 @@ function assembleMessages(options = {}) {
|
|
|
806
1297
|
* OpenAI 兼容 API 适配器
|
|
807
1298
|
* 支持 DeepSeek 等完全兼容 OpenAI 接口的服务
|
|
808
1299
|
*/
|
|
1300
|
+
var TRANSPORT_KEYS = [
|
|
1301
|
+
"apiKey",
|
|
1302
|
+
"baseUrl",
|
|
1303
|
+
"apiUrl",
|
|
1304
|
+
"path",
|
|
1305
|
+
"headers"
|
|
1306
|
+
];
|
|
809
1307
|
var openaiAdapter = {
|
|
810
1308
|
name: "openai",
|
|
811
|
-
install(chatService) {
|
|
1309
|
+
install(chatService, options = {}) {
|
|
1310
|
+
if (this._transport) this._transport = {
|
|
1311
|
+
...this._transport,
|
|
1312
|
+
...options
|
|
1313
|
+
};
|
|
812
1314
|
chatService.setAdapter(this);
|
|
813
1315
|
},
|
|
1316
|
+
_resolveConfig(config) {
|
|
1317
|
+
const transport = this._transport || {};
|
|
1318
|
+
const resolved = {
|
|
1319
|
+
...config,
|
|
1320
|
+
...transport
|
|
1321
|
+
};
|
|
1322
|
+
if (transport.modelParams) resolved.modelParams = {
|
|
1323
|
+
...transport.modelParams,
|
|
1324
|
+
...config.modelParams || {}
|
|
1325
|
+
};
|
|
1326
|
+
return resolved;
|
|
1327
|
+
},
|
|
1328
|
+
getRequestDefaults() {
|
|
1329
|
+
if (!this._transport) return {};
|
|
1330
|
+
const defaults = {};
|
|
1331
|
+
for (const [key, value] of Object.entries(this._transport)) if (!TRANSPORT_KEYS.includes(key) && value !== void 0) defaults[key] = value;
|
|
1332
|
+
return defaults;
|
|
1333
|
+
},
|
|
814
1334
|
buildRequest(messages, config, systemPrompts = []) {
|
|
1335
|
+
config = this._resolveConfig(config);
|
|
815
1336
|
const model = config.model || config.modelParams?.model;
|
|
816
|
-
if (!model) throw new
|
|
817
|
-
const
|
|
818
|
-
const
|
|
819
|
-
const
|
|
1337
|
+
if (!model) throw new ConfigurationError("Missing required config: model (either at top level or in modelParams)");
|
|
1338
|
+
const mp = config.modelParams || {};
|
|
1339
|
+
const temperature = mp.temperature ?? config.temperature;
|
|
1340
|
+
const maxTokens = mp.maxTokens ?? config.maxTokens;
|
|
1341
|
+
const reasoningEffort = mp.reasoningEffort ?? config.reasoningEffort;
|
|
820
1342
|
const requestBody = {
|
|
821
1343
|
model,
|
|
822
1344
|
messages: MessageFormatter.format({
|
|
@@ -826,15 +1348,34 @@ var openaiAdapter = {
|
|
|
826
1348
|
resolveImage: config.resolveImage,
|
|
827
1349
|
format: config.messageFormat
|
|
828
1350
|
}),
|
|
829
|
-
temperature,
|
|
830
|
-
max_tokens: maxTokens,
|
|
831
1351
|
stream: false
|
|
832
1352
|
};
|
|
1353
|
+
if (temperature !== void 0) requestBody.temperature = temperature;
|
|
1354
|
+
if (maxTokens !== void 0) requestBody.max_tokens = maxTokens;
|
|
1355
|
+
if (mp.topP !== void 0) requestBody.top_p = mp.topP;
|
|
1356
|
+
if (mp.frequencyPenalty !== void 0) requestBody.frequency_penalty = mp.frequencyPenalty;
|
|
1357
|
+
if (mp.presencePenalty !== void 0) requestBody.presence_penalty = mp.presencePenalty;
|
|
1358
|
+
if (mp.stop !== void 0) requestBody.stop = mp.stop;
|
|
1359
|
+
if (mp.responseFormat !== void 0) requestBody.response_format = mp.responseFormat;
|
|
1360
|
+
if (mp.seed !== void 0) requestBody.seed = mp.seed;
|
|
833
1361
|
if (config.tools && Array.isArray(config.tools) && config.tools.length > 0) {
|
|
834
1362
|
requestBody.tools = config.tools;
|
|
835
1363
|
requestBody.tool_choice = "auto";
|
|
836
1364
|
}
|
|
837
|
-
if (reasoningEffort
|
|
1365
|
+
if (reasoningEffort !== void 0) requestBody.reasoning_effort = reasoningEffort;
|
|
1366
|
+
const consumedKeys = new Set([
|
|
1367
|
+
"model",
|
|
1368
|
+
"temperature",
|
|
1369
|
+
"maxTokens",
|
|
1370
|
+
"reasoningEffort",
|
|
1371
|
+
"topP",
|
|
1372
|
+
"frequencyPenalty",
|
|
1373
|
+
"presencePenalty",
|
|
1374
|
+
"stop",
|
|
1375
|
+
"responseFormat",
|
|
1376
|
+
"seed"
|
|
1377
|
+
]);
|
|
1378
|
+
for (const [key, value] of Object.entries(mp)) if (!consumedKeys.has(key) && value !== void 0) requestBody[key] = value;
|
|
838
1379
|
return requestBody;
|
|
839
1380
|
},
|
|
840
1381
|
_getUrl(config) {
|
|
@@ -847,6 +1388,7 @@ var openaiAdapter = {
|
|
|
847
1388
|
},
|
|
848
1389
|
async send(requestBody, config, options = {}) {
|
|
849
1390
|
try {
|
|
1391
|
+
config = this._resolveConfig(config);
|
|
850
1392
|
const url = this._getUrl(config);
|
|
851
1393
|
const headers = {
|
|
852
1394
|
"Content-Type": "application/json",
|
|
@@ -881,6 +1423,7 @@ var openaiAdapter = {
|
|
|
881
1423
|
return internal;
|
|
882
1424
|
},
|
|
883
1425
|
async stream(requestBody, config, onProgress, onDone, options = {}) {
|
|
1426
|
+
config = this._resolveConfig(config);
|
|
884
1427
|
const streamBody = {
|
|
885
1428
|
...requestBody,
|
|
886
1429
|
stream: true
|
|
@@ -915,22 +1458,37 @@ var openaiAdapter = {
|
|
|
915
1458
|
content: "",
|
|
916
1459
|
reasoningContent: ""
|
|
917
1460
|
};
|
|
1461
|
+
let reachedDone = false;
|
|
918
1462
|
while (true) {
|
|
919
1463
|
if (options.signal?.aborted) break;
|
|
920
1464
|
const { done, value } = await reader.read();
|
|
921
1465
|
if (done) break;
|
|
922
1466
|
buffer += decoder.decode(value, { stream: true });
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
1467
|
+
let sepMatch;
|
|
1468
|
+
const EVENT_SEP = /\r?\n\r?\n/;
|
|
1469
|
+
while ((sepMatch = EVENT_SEP.exec(buffer)) !== null) {
|
|
1470
|
+
const rawEvent = buffer.slice(0, sepMatch.index);
|
|
1471
|
+
buffer = buffer.slice(sepMatch.index + sepMatch[0].length);
|
|
1472
|
+
const dataPayload = rawEvent.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n");
|
|
1473
|
+
if (!dataPayload) continue;
|
|
1474
|
+
if (dataPayload.trim() === "[DONE]") {
|
|
1475
|
+
reachedDone = true;
|
|
1476
|
+
break;
|
|
1477
|
+
}
|
|
929
1478
|
try {
|
|
930
|
-
const delta = JSON.parse(
|
|
1479
|
+
const delta = JSON.parse(dataPayload).choices?.[0]?.delta;
|
|
931
1480
|
if (!delta) continue;
|
|
932
|
-
|
|
1481
|
+
const deltaInfo = {
|
|
1482
|
+
content: "",
|
|
1483
|
+
reasoningContent: "",
|
|
1484
|
+
toolCalls: null
|
|
1485
|
+
};
|
|
1486
|
+
if (delta.content) {
|
|
1487
|
+
accumulated.content += delta.content;
|
|
1488
|
+
deltaInfo.content = delta.content;
|
|
1489
|
+
}
|
|
933
1490
|
if (delta.tool_calls) {
|
|
1491
|
+
const newCalls = [];
|
|
934
1492
|
if (!accumulated.toolCalls) accumulated.toolCalls = [];
|
|
935
1493
|
for (const toolCallDelta of delta.tool_calls) {
|
|
936
1494
|
const index = toolCallDelta.index;
|
|
@@ -946,14 +1504,23 @@ var openaiAdapter = {
|
|
|
946
1504
|
if (toolCallDelta.type) accumulated.toolCalls[index].type = toolCallDelta.type;
|
|
947
1505
|
if (toolCallDelta.function?.name) accumulated.toolCalls[index].function.name += toolCallDelta.function.name;
|
|
948
1506
|
if (toolCallDelta.function?.arguments) accumulated.toolCalls[index].function.arguments += toolCallDelta.function.arguments;
|
|
1507
|
+
newCalls.push({
|
|
1508
|
+
index,
|
|
1509
|
+
delta: toolCallDelta
|
|
1510
|
+
});
|
|
949
1511
|
}
|
|
1512
|
+
deltaInfo.toolCalls = newCalls;
|
|
950
1513
|
}
|
|
951
|
-
if (delta.reasoning_content)
|
|
952
|
-
|
|
1514
|
+
if (delta.reasoning_content) {
|
|
1515
|
+
accumulated.reasoningContent += delta.reasoning_content;
|
|
1516
|
+
deltaInfo.reasoningContent = delta.reasoning_content;
|
|
1517
|
+
}
|
|
1518
|
+
onProgress({ ...accumulated }, deltaInfo);
|
|
953
1519
|
} catch (e) {
|
|
954
|
-
console.warn("stream parsing failed :", e,
|
|
1520
|
+
console.warn("stream parsing failed :", e, dataPayload);
|
|
955
1521
|
}
|
|
956
1522
|
}
|
|
1523
|
+
if (reachedDone) break;
|
|
957
1524
|
}
|
|
958
1525
|
onDone({
|
|
959
1526
|
role: "assistant",
|
|
@@ -974,165 +1541,246 @@ var openaiAdapter = {
|
|
|
974
1541
|
throw new APIError(errorMessage, response.status, null, errorText);
|
|
975
1542
|
}
|
|
976
1543
|
};
|
|
1544
|
+
/**
|
|
1545
|
+
* 创建 OpenAI 兼容适配器实例(工厂)。
|
|
1546
|
+
*
|
|
1547
|
+
* 与单例 openaiAdapter 的区别:每个实例自持一份运输配置(apiKey/baseUrl/apiUrl/path/headers),
|
|
1548
|
+
* 互不干扰——解决"同一适配器装到多个 ChatService 互相覆盖"的单例陷阱。
|
|
1549
|
+
*
|
|
1550
|
+
* 用法:
|
|
1551
|
+
* const adapter = createOpenAIAdapter({ apiKey, baseUrl, modelParams: { temperature: 0.8 } });
|
|
1552
|
+
* const chat = new ChatService({ adapter, model: 'deepseek-chat' });
|
|
1553
|
+
* chat.use(adapter); // 或直接 new ChatService({ adapter })
|
|
1554
|
+
*
|
|
1555
|
+
* 非运输键(model/modelParams/messageFormat/resolveImage/capabilities 等)会作为
|
|
1556
|
+
* "请求默认参数"供会话层合并,单次请求仍可覆盖。
|
|
1557
|
+
*/
|
|
1558
|
+
function createOpenAIAdapter(options = {}) {
|
|
1559
|
+
return {
|
|
1560
|
+
...openaiAdapter,
|
|
1561
|
+
_transport: { ...options }
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
977
1564
|
//#endregion
|
|
978
1565
|
//#region src/plugins/tool-calling.js
|
|
979
1566
|
/**
|
|
980
1567
|
* 工具调用插件
|
|
981
1568
|
* 功能:拦截助手消息中的 tool_calls,执行对应的工具,将结果作为 tool 消息加入对话,
|
|
982
1569
|
* 然后自动继续对话(通过 sendExisting / sendExistingStream),直到没有新的工具调用。
|
|
983
|
-
*
|
|
1570
|
+
*
|
|
984
1571
|
* 设计要点:
|
|
985
1572
|
* - 支持普通请求和流式请求(通过 isStream 标志区分)
|
|
986
1573
|
* - 支持多次工具调用循环(maxIterations 防止无限循环)
|
|
987
|
-
* -
|
|
1574
|
+
* - 并行执行工具
|
|
988
1575
|
* - 工具执行失败时,仍然返回错误信息给 AI,而不是中断整个流程
|
|
989
1576
|
* - 触发 tool-error 事件,方便用户监听工具执行异常
|
|
1577
|
+
*
|
|
1578
|
+
* 注意:请使用 createToolCallingPlugin() 工厂创建实例。
|
|
1579
|
+
* 默认导出的 toolCallingPlugin 是兼容用的模块级单例,装到多个 ChatService
|
|
1580
|
+
* 实例会互相覆盖(executor 表、工具定义、配置),新代码不要直接用单例。
|
|
1581
|
+
*/
|
|
1582
|
+
var toolCallingPlugin = createToolCallingPlugin();
|
|
1583
|
+
/**
|
|
1584
|
+
* 创建工具调用插件实例(工厂)。
|
|
1585
|
+
* @param {Object} [options] — { timeout?, maxIterations? }
|
|
1586
|
+
* - 两者**默认都不限制**:不传就是"循环到没有工具调用为止 / 不超时"
|
|
1587
|
+
* - 想限制才传(这是使用者的选择,不是框架替你做的决定)
|
|
990
1588
|
*/
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1589
|
+
function createToolCallingPlugin(options = {}) {
|
|
1590
|
+
return {
|
|
1591
|
+
name: "tool-calling",
|
|
1592
|
+
maxIterations: options.maxIterations ?? Infinity,
|
|
1593
|
+
_options: { ...options },
|
|
1594
|
+
_tools: /* @__PURE__ */ new Map(),
|
|
1595
|
+
_toolDefs: [],
|
|
1596
|
+
chatService: null,
|
|
1597
|
+
install(chatService, options = {}) {
|
|
1598
|
+
this._options = {
|
|
1599
|
+
...this._options,
|
|
1600
|
+
...options
|
|
1601
|
+
};
|
|
1602
|
+
this.chatService = chatService;
|
|
1603
|
+
chatService.pipe({
|
|
1604
|
+
name: "tool-calling-inject",
|
|
1605
|
+
phase: "beforeSend",
|
|
1606
|
+
run: ({ config }) => {
|
|
1607
|
+
if (this._toolDefs.length) config.tools = this._toolDefs;
|
|
1608
|
+
}
|
|
1010
1609
|
});
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1610
|
+
/**
|
|
1611
|
+
* 注册工具
|
|
1612
|
+
* @param {string} name - 工具名称(唯一标识)
|
|
1613
|
+
* @param {string} description - 工具描述(告诉 AI 何时调用)
|
|
1614
|
+
* @param {Function} executor - 异步执行函数,接收参数对象,返回结果(字符串或对象)
|
|
1615
|
+
* @param {Object} parameters - JSON Schema 参数定义(可选,默认为空对象)
|
|
1616
|
+
* @returns {ChatService} 返回 chatService 实例,支持链式调用
|
|
1617
|
+
*/
|
|
1618
|
+
const registerTool = (name, description, executor, parameters = {}) => {
|
|
1619
|
+
this._tools.set(name, {
|
|
1620
|
+
executor,
|
|
1621
|
+
description
|
|
1622
|
+
});
|
|
1623
|
+
const properties = {};
|
|
1624
|
+
const requiredKeys = [];
|
|
1625
|
+
for (const [key, def] of Object.entries(parameters || {})) {
|
|
1626
|
+
const { required: isRequired, ...schema } = def || {};
|
|
1627
|
+
properties[key] = schema;
|
|
1628
|
+
if (isRequired) requiredKeys.push(key);
|
|
1629
|
+
}
|
|
1630
|
+
const toolDefinition = {
|
|
1631
|
+
type: "function",
|
|
1632
|
+
function: {
|
|
1633
|
+
name,
|
|
1634
|
+
description,
|
|
1635
|
+
parameters: {
|
|
1636
|
+
type: "object",
|
|
1637
|
+
properties,
|
|
1638
|
+
required: requiredKeys
|
|
1639
|
+
}
|
|
1020
1640
|
}
|
|
1641
|
+
};
|
|
1642
|
+
if (!this._toolDefs.find((t) => t.function.name === name)) this._toolDefs.push(toolDefinition);
|
|
1643
|
+
return chatService;
|
|
1644
|
+
};
|
|
1645
|
+
chatService.pipe({
|
|
1646
|
+
name: "tool-calling-loop",
|
|
1647
|
+
phase: "afterSend",
|
|
1648
|
+
run: async (ctx) => {
|
|
1649
|
+
ctx.result = await this._handleWithTools(ctx.result, ctx.isStream, ctx.onProgress || void 0, ctx.onDone || void 0);
|
|
1021
1650
|
}
|
|
1651
|
+
});
|
|
1652
|
+
return {
|
|
1653
|
+
registerTool,
|
|
1654
|
+
tools: this._tools,
|
|
1655
|
+
uninstall: (c) => this.uninstall(c)
|
|
1022
1656
|
};
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const tool = self._tools.get(toolName);
|
|
1065
|
-
if (!tool) {
|
|
1066
|
-
chat.emit("tool-error", {
|
|
1067
|
-
toolName,
|
|
1068
|
-
error: /* @__PURE__ */ new Error(`Tool not registered: ${toolName}`),
|
|
1069
|
-
toolCallId: call.id,
|
|
1070
|
-
stage: "lookup",
|
|
1071
|
-
timestamp: Date.now()
|
|
1072
|
-
});
|
|
1073
|
-
return {
|
|
1074
|
-
tool_call_id: call.id,
|
|
1075
|
-
error: `工具 ${toolName} 未注册`,
|
|
1076
|
-
success: false
|
|
1077
|
-
};
|
|
1078
|
-
}
|
|
1079
|
-
try {
|
|
1080
|
-
const toolTimeout = chat.config.toolTimeout;
|
|
1081
|
-
let execPromise = tool.executor(args);
|
|
1082
|
-
if (toolTimeout && typeof toolTimeout === "number" && toolTimeout > 0) {
|
|
1083
|
-
const timeoutErr = /* @__PURE__ */ new Error(`工具 ${toolName} 执行超时 (${toolTimeout}ms)`);
|
|
1084
|
-
timeoutErr.name = "ToolTimeoutError";
|
|
1085
|
-
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(timeoutErr), toolTimeout));
|
|
1086
|
-
execPromise = Promise.race([execPromise, timeoutPromise]);
|
|
1657
|
+
},
|
|
1658
|
+
uninstall(chat) {
|
|
1659
|
+
if (!chat) return chat;
|
|
1660
|
+
chat.unpipe?.("tool-calling-inject");
|
|
1661
|
+
chat.unpipe?.("tool-calling-loop");
|
|
1662
|
+
if (chat.plugins && chat.plugins["tool-calling"]) delete chat.plugins["tool-calling"];
|
|
1663
|
+
return chat;
|
|
1664
|
+
},
|
|
1665
|
+
async _handleWithTools(initialResponse, isStream, onProgress, onDone) {
|
|
1666
|
+
const self = this;
|
|
1667
|
+
const chat = this.chatService;
|
|
1668
|
+
let iteration = 0;
|
|
1669
|
+
/**
|
|
1670
|
+
* 递归处理工具调用
|
|
1671
|
+
* @param {Object} initialResponse - 初始 AI 响应(可能是第一次请求的响应)
|
|
1672
|
+
* @returns {Promise<Object>} 最终 AI 响应(不含 tool_calls)
|
|
1673
|
+
*/
|
|
1674
|
+
async function processResponse(initialResponse) {
|
|
1675
|
+
let lastResponse = initialResponse;
|
|
1676
|
+
while (iteration < self.maxIterations) {
|
|
1677
|
+
const toolCalls = lastResponse?.toolCalls;
|
|
1678
|
+
if (!toolCalls || toolCalls.length === 0) break;
|
|
1679
|
+
iteration++;
|
|
1680
|
+
const toolResults = await Promise.all(toolCalls.map(async (call) => {
|
|
1681
|
+
const toolName = call.function?.name;
|
|
1682
|
+
let args;
|
|
1683
|
+
try {
|
|
1684
|
+
args = JSON.parse(call.function?.arguments || "{}");
|
|
1685
|
+
} catch (parseErr) {
|
|
1686
|
+
chat.emit("tool-error", {
|
|
1687
|
+
toolName: toolName || "unknown",
|
|
1688
|
+
error: parseErr,
|
|
1689
|
+
toolCallId: call.id,
|
|
1690
|
+
stage: "parse",
|
|
1691
|
+
timestamp: Date.now()
|
|
1692
|
+
});
|
|
1693
|
+
return {
|
|
1694
|
+
tool_call_id: call.id,
|
|
1695
|
+
error: `参数解析失败: ${parseErr.message}`,
|
|
1696
|
+
success: false
|
|
1697
|
+
};
|
|
1087
1698
|
}
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1699
|
+
const tool = self._tools.get(toolName);
|
|
1700
|
+
if (!tool) {
|
|
1701
|
+
chat.emit("tool-error", {
|
|
1702
|
+
toolName,
|
|
1703
|
+
error: /* @__PURE__ */ new Error(`Tool not registered: ${toolName}`),
|
|
1704
|
+
toolCallId: call.id,
|
|
1705
|
+
stage: "lookup",
|
|
1706
|
+
timestamp: Date.now()
|
|
1707
|
+
});
|
|
1708
|
+
return {
|
|
1709
|
+
tool_call_id: call.id,
|
|
1710
|
+
error: `工具 ${toolName} 未注册`,
|
|
1711
|
+
success: false
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
try {
|
|
1715
|
+
const toolTimeout = self._options.timeout;
|
|
1716
|
+
let result;
|
|
1717
|
+
if (typeof toolTimeout === "number" && toolTimeout > 0) {
|
|
1718
|
+
const controller = new AbortController();
|
|
1719
|
+
const timeoutErr = /* @__PURE__ */ new Error(`工具 ${toolName} 执行超时 (${toolTimeout}ms)`);
|
|
1720
|
+
timeoutErr.name = "ToolTimeoutError";
|
|
1721
|
+
let timer;
|
|
1722
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1723
|
+
timer = setTimeout(() => {
|
|
1724
|
+
controller.abort();
|
|
1725
|
+
reject(timeoutErr);
|
|
1726
|
+
}, toolTimeout);
|
|
1727
|
+
});
|
|
1728
|
+
const execPromise = Promise.resolve().then(() => tool.executor(args, { signal: controller.signal }));
|
|
1729
|
+
execPromise.catch(() => {});
|
|
1730
|
+
try {
|
|
1731
|
+
result = await Promise.race([execPromise, timeoutPromise]);
|
|
1732
|
+
} finally {
|
|
1733
|
+
clearTimeout(timer);
|
|
1734
|
+
}
|
|
1735
|
+
} else result = await tool.executor(args);
|
|
1736
|
+
chat.emit("tool-success", {
|
|
1737
|
+
toolName,
|
|
1738
|
+
result,
|
|
1739
|
+
toolCallId: call.id,
|
|
1740
|
+
timestamp: Date.now()
|
|
1741
|
+
});
|
|
1742
|
+
return {
|
|
1743
|
+
tool_call_id: call.id,
|
|
1744
|
+
content: typeof result === "string" ? result : JSON.stringify(result),
|
|
1745
|
+
success: true
|
|
1746
|
+
};
|
|
1747
|
+
} catch (err) {
|
|
1748
|
+
const stage = err.name === "ToolTimeoutError" ? "timeout" : "execute";
|
|
1749
|
+
chat.emit("tool-error", {
|
|
1750
|
+
toolName,
|
|
1751
|
+
error: err,
|
|
1752
|
+
toolCallId: call.id,
|
|
1753
|
+
stage,
|
|
1754
|
+
timestamp: Date.now()
|
|
1755
|
+
});
|
|
1756
|
+
return {
|
|
1757
|
+
tool_call_id: call.id,
|
|
1758
|
+
error: err.message,
|
|
1759
|
+
success: false
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
}));
|
|
1763
|
+
for (const tr of toolResults) {
|
|
1764
|
+
const content = tr.success ? tr.content : tr.error || "工具执行失败";
|
|
1765
|
+
chat.messages.addTool(content, tr.tool_call_id);
|
|
1114
1766
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
lastResponse = chunk;
|
|
1124
|
-
}, (final) => {
|
|
1125
|
-
lastResponse = final;
|
|
1126
|
-
resolve();
|
|
1767
|
+
if (isStream) await new Promise((resolve) => {
|
|
1768
|
+
chat.sendExistingStream((chunk) => {
|
|
1769
|
+
if (onProgress) onProgress(chunk);
|
|
1770
|
+
lastResponse = chunk;
|
|
1771
|
+
}, (final) => {
|
|
1772
|
+
lastResponse = final;
|
|
1773
|
+
resolve();
|
|
1774
|
+
});
|
|
1127
1775
|
});
|
|
1128
|
-
|
|
1129
|
-
|
|
1776
|
+
else lastResponse = await chat.sendExisting();
|
|
1777
|
+
}
|
|
1778
|
+
return lastResponse;
|
|
1130
1779
|
}
|
|
1131
|
-
return
|
|
1780
|
+
return await processResponse(initialResponse);
|
|
1132
1781
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
};
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1136
1784
|
//#endregion
|
|
1137
1785
|
//#region src/plugins/model-registry.js
|
|
1138
1786
|
/**
|
|
@@ -1239,56 +1887,225 @@ var DEFAULT_CAPABILITIES = {
|
|
|
1239
1887
|
reasoning: false,
|
|
1240
1888
|
vision: false
|
|
1241
1889
|
};
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1890
|
+
/**
|
|
1891
|
+
* 注意:请使用 createModelRegistryPlugin() 工厂创建实例。
|
|
1892
|
+
* 默认导出的 modelRegistryPlugin 是兼容用的模块级单例,装到多个 ChatService
|
|
1893
|
+
* 实例会互相覆盖(注册表、实例引用),新代码不要直接用单例。
|
|
1894
|
+
*/
|
|
1895
|
+
var modelRegistryPlugin = createModelRegistryPlugin();
|
|
1896
|
+
/**
|
|
1897
|
+
* 创建模型能力注册表插件实例(工厂)。
|
|
1898
|
+
* @param {Object} [options] — { models: { '模型名': capabilities } }(合并进内置表)
|
|
1899
|
+
*/
|
|
1900
|
+
function createModelRegistryPlugin(options = {}) {
|
|
1901
|
+
return {
|
|
1902
|
+
name: "model-registry",
|
|
1903
|
+
_options: { ...options },
|
|
1904
|
+
_registry: new Map([...Object.entries(BUILTIN_MODELS), ...Object.entries(options.models || {})]),
|
|
1905
|
+
chat: null,
|
|
1906
|
+
install(chatService, options = {}) {
|
|
1907
|
+
this._options = {
|
|
1908
|
+
...this._options,
|
|
1909
|
+
...options
|
|
1258
1910
|
};
|
|
1259
|
-
this.
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
chatService.on("config-updated", ({ changes }) => {
|
|
1272
|
-
if ("model" in changes) this._syncCapabilities();
|
|
1273
|
-
if ("capabilities" in changes && !("model" in changes)) {
|
|
1274
|
-
const current = this._lookupCapabilities();
|
|
1275
|
-
if (current) chatService.config.capabilities = {
|
|
1276
|
-
...current,
|
|
1277
|
-
...changes.capabilities
|
|
1911
|
+
this.chat = chatService;
|
|
1912
|
+
/**
|
|
1913
|
+
* 注册/覆盖一个模型的能力标签
|
|
1914
|
+
* @param {string} name — 模型名称
|
|
1915
|
+
* @param {Object} capabilities — 能力标签对象(部分字段即可,未提供的取默认值)
|
|
1916
|
+
* @returns {ChatService}
|
|
1917
|
+
*/
|
|
1918
|
+
const registerModel = (name, capabilities = {}) => {
|
|
1919
|
+
if (!name || typeof name !== "string" || !name.trim()) throw new ValidationError("[model-registry] 模型名称必须是非空字符串");
|
|
1920
|
+
const merged = {
|
|
1921
|
+
...DEFAULT_CAPABILITIES,
|
|
1922
|
+
...capabilities
|
|
1278
1923
|
};
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1924
|
+
this._registry.set(name.trim(), merged);
|
|
1925
|
+
if (chatService.config.model === name.trim()) this._syncCapabilities();
|
|
1926
|
+
return chatService;
|
|
1927
|
+
};
|
|
1928
|
+
/**
|
|
1929
|
+
* 列出所有已注册的模型名称
|
|
1930
|
+
* @returns {Array<string>}
|
|
1931
|
+
*/
|
|
1932
|
+
const listModels = () => {
|
|
1933
|
+
return [...this._registry.keys()];
|
|
1934
|
+
};
|
|
1935
|
+
this._syncCapabilities();
|
|
1936
|
+
chatService.pipe({
|
|
1937
|
+
name: "model-registry-caps",
|
|
1938
|
+
phase: "beforeSend",
|
|
1939
|
+
run: ({ config }) => {
|
|
1940
|
+
if (!config.model) return;
|
|
1941
|
+
const caps = this._registry.get(config.model);
|
|
1942
|
+
if (caps) config.capabilities = { ...caps };
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
chatService.on("config-updated", ({ changes }) => {
|
|
1946
|
+
if ("model" in changes) this._syncCapabilities();
|
|
1947
|
+
if ("capabilities" in changes && !("model" in changes)) {
|
|
1948
|
+
const current = this._lookupCapabilities();
|
|
1949
|
+
if (current) chatService.config.capabilities = {
|
|
1950
|
+
...current,
|
|
1951
|
+
...changes.capabilities
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
return {
|
|
1956
|
+
registerModel,
|
|
1957
|
+
listModels,
|
|
1958
|
+
uninstall: (c) => this.uninstall(c)
|
|
1959
|
+
};
|
|
1960
|
+
},
|
|
1961
|
+
uninstall(chat = this.chat) {
|
|
1962
|
+
if (!chat) return chat;
|
|
1963
|
+
chat.unpipe?.("model-registry-caps");
|
|
1964
|
+
if (chat.plugins && chat.plugins["model-registry"]) delete chat.plugins["model-registry"];
|
|
1965
|
+
return chat;
|
|
1966
|
+
},
|
|
1967
|
+
_lookupCapabilities() {
|
|
1968
|
+
const model = this.chat.config.model;
|
|
1969
|
+
if (!model) return null;
|
|
1970
|
+
return this._registry.get(model) || null;
|
|
1971
|
+
},
|
|
1972
|
+
_syncCapabilities() {
|
|
1973
|
+
const caps = this._lookupCapabilities();
|
|
1974
|
+
if (caps) this.chat.config.capabilities = { ...caps };
|
|
1975
|
+
}
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
//#endregion
|
|
1979
|
+
//#region src/plugins/continuation.js
|
|
1980
|
+
/**
|
|
1981
|
+
* continuation 插件(v4.0)
|
|
1982
|
+
*
|
|
1983
|
+
* 「续写 / 临时消息」这套玩法住在这里 —— 核心不认识它,装上才有:
|
|
1984
|
+
* 1. 续写检测(替换核心的 autoContinue 步骤):底部若有带 prefix 标记的消息,就把结果合并回它
|
|
1985
|
+
* 2. ephemeral 过滤器:临时消息只有"位于底部连续段"时才发给 API
|
|
1986
|
+
* 3. continueLast / continueLastStream 两个 API
|
|
1987
|
+
* 4. uninstall():把自己装的东西全部还原(步骤、过滤器、方法)
|
|
1988
|
+
*
|
|
1989
|
+
* 用法:
|
|
1990
|
+
* import { createContinuationPlugin } from 'my-ai-chat-framework';
|
|
1991
|
+
* chat.use(createContinuationPlugin({ autoContinue: true }));
|
|
1992
|
+
*
|
|
1993
|
+
* // 模拟思维链:注入一条临时引导,结果合并回上一条 assistant,不留痕
|
|
1994
|
+
* chat.messages.addOnceAssistant('(内心:他肯定又要熬夜)');
|
|
1995
|
+
* await chat.stream('在吗');
|
|
1996
|
+
*/
|
|
1997
|
+
/**
|
|
1998
|
+
* ephemeral 规则:只有底部连续的临时消息保留,其余丢弃(与 v3.x 核心内置行为一致)。
|
|
1999
|
+
* 导出以便使用者直接注册到 MessageFormatter(不使用插件时也能用这条规则)。
|
|
2000
|
+
*/
|
|
2001
|
+
function ephemeralFilterRule(msg, { index, messages }) {
|
|
2002
|
+
if (!msg._ephemeral) return msg;
|
|
2003
|
+
let start = messages.length;
|
|
2004
|
+
for (let i = messages.length - 1; i >= 0; i--) if (messages[i]._ephemeral) start = i;
|
|
2005
|
+
else break;
|
|
2006
|
+
return index >= start ? msg : null;
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* 找续写目标:最后一条必须是 assistant 或临时消息(否则抛错)。
|
|
2010
|
+
* @param {object} chat ChatService 实例
|
|
2011
|
+
*/
|
|
2012
|
+
function prepareContinue(chat) {
|
|
2013
|
+
const msgs = chat.messages.getAll();
|
|
2014
|
+
const last = msgs[msgs.length - 1];
|
|
2015
|
+
if (last && (last.role === "assistant" || last._ephemeral)) return last;
|
|
2016
|
+
throw new ValidationError("最后一条消息不是 assistant,无法续写");
|
|
2017
|
+
}
|
|
2018
|
+
/** 续写结束后的"转正" */
|
|
2019
|
+
function finalize(chat, targetId) {
|
|
2020
|
+
chat.messages.update(targetId, {
|
|
2021
|
+
_complete: true,
|
|
2022
|
+
prefix: void 0,
|
|
2023
|
+
_ephemeral: false
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* 创建续写插件实例(工厂)
|
|
2028
|
+
* @param {Object} [options] — { autoContinue?: boolean } 是否自动识别底部 prefix 消息并合并(默认 false)
|
|
2029
|
+
*/
|
|
2030
|
+
function createContinuationPlugin(options = {}) {
|
|
2031
|
+
return {
|
|
2032
|
+
name: "continuation",
|
|
2033
|
+
_options: {
|
|
2034
|
+
autoContinue: false,
|
|
2035
|
+
...options
|
|
2036
|
+
},
|
|
2037
|
+
_prevEphemeralFilter: void 0,
|
|
2038
|
+
_installedOn: null,
|
|
2039
|
+
install(chat) {
|
|
2040
|
+
this._installedOn = chat;
|
|
2041
|
+
if (typeof chat.registerConfigKeys === "function") chat.registerConfigKeys({ autoContinue: (v) => typeof v === "boolean" });
|
|
2042
|
+
chat.replaceStage("autoContinue", (ctx) => {
|
|
2043
|
+
if (!(chat.config.autoContinue ?? this._options.autoContinue)) return;
|
|
2044
|
+
if (ctx.mergeToEntry) return;
|
|
2045
|
+
const last = ctx.messages.getLast();
|
|
2046
|
+
if (last && last.prefix && (last.role === "assistant" || last._ephemeral)) ctx.mergeToEntry = last.id;
|
|
2047
|
+
});
|
|
2048
|
+
this._prevEphemeralFilter = MessageFormatter.registerFilter("ephemeral", ephemeralFilterRule, { priority: 100 });
|
|
2049
|
+
const continueLast = async () => {
|
|
2050
|
+
const target = prepareContinue(chat);
|
|
2051
|
+
chat.messages.update(target.id, { prefix: true });
|
|
2052
|
+
try {
|
|
2053
|
+
await chat.sendExisting(void 0, { mergeToEntry: target.id });
|
|
2054
|
+
finalize(chat, target.id);
|
|
2055
|
+
return target;
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
finalize(chat, target.id);
|
|
2058
|
+
if (err.name === "AbortError") {
|
|
2059
|
+
chat.emit("aborted", { timestamp: Date.now() });
|
|
2060
|
+
return target;
|
|
2061
|
+
}
|
|
2062
|
+
throw err;
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
const continueLastStream = async (onProgress, onDone) => {
|
|
2066
|
+
const target = prepareContinue(chat);
|
|
2067
|
+
chat.messages.update(target.id, { prefix: true });
|
|
2068
|
+
const baseLen = (target.content || "").length;
|
|
2069
|
+
try {
|
|
2070
|
+
await chat.sendExistingStream(void 0, { mergeToEntry: target.id }, (chunk) => {
|
|
2071
|
+
if (onProgress) onProgress({
|
|
2072
|
+
...chunk,
|
|
2073
|
+
content: (chunk.content || "").slice(baseLen)
|
|
2074
|
+
});
|
|
2075
|
+
}, (final) => {
|
|
2076
|
+
if (onDone) onDone(final);
|
|
2077
|
+
});
|
|
2078
|
+
finalize(chat, target.id);
|
|
2079
|
+
return target;
|
|
2080
|
+
} catch (err) {
|
|
2081
|
+
finalize(chat, target.id);
|
|
2082
|
+
if (err.name === "AbortError") {
|
|
2083
|
+
chat.emit("aborted", { timestamp: Date.now() });
|
|
2084
|
+
return target;
|
|
2085
|
+
}
|
|
2086
|
+
throw err;
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
return {
|
|
2090
|
+
continueLast,
|
|
2091
|
+
continueLastStream,
|
|
2092
|
+
uninstall: () => this.uninstall(chat)
|
|
2093
|
+
};
|
|
2094
|
+
},
|
|
2095
|
+
uninstall(chat = this._installedOn) {
|
|
2096
|
+
if (!chat) return chat;
|
|
2097
|
+
if (typeof chat.restoreStage === "function") chat.restoreStage("autoContinue");
|
|
2098
|
+
if (this._prevEphemeralFilter) MessageFormatter.registerFilter("ephemeral", this._prevEphemeralFilter, { priority: 100 });
|
|
2099
|
+
else MessageFormatter.unregisterFilter("ephemeral");
|
|
2100
|
+
if (chat.plugins && chat.plugins["continuation"]) delete chat.plugins["continuation"];
|
|
2101
|
+
if (typeof chat.unregisterConfigKeys === "function") chat.unregisterConfigKeys(["autoContinue"]);
|
|
2102
|
+
this._installedOn = null;
|
|
2103
|
+
return chat;
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
/** 兼容用单例(多实例场景请用工厂) */
|
|
2108
|
+
var continuationPlugin = createContinuationPlugin();
|
|
1292
2109
|
//#endregion
|
|
1293
2110
|
exports.APIError = APIError;
|
|
1294
2111
|
exports.ChatService = ChatService;
|
|
@@ -1298,8 +2115,16 @@ exports.MessageFormatter = MessageFormatter;
|
|
|
1298
2115
|
exports.MessageStore = MessageStore;
|
|
1299
2116
|
exports.NetworkError = NetworkError;
|
|
1300
2117
|
exports.ParsingError = ParsingError;
|
|
2118
|
+
exports.Pipeline = Pipeline;
|
|
1301
2119
|
exports.SystemPromptStore = SystemPromptStore;
|
|
2120
|
+
exports.ValidationError = ValidationError;
|
|
1302
2121
|
exports.assembleMessages = assembleMessages;
|
|
2122
|
+
exports.assertAdapter = assertAdapter;
|
|
2123
|
+
exports.continuationPlugin = continuationPlugin;
|
|
2124
|
+
exports.createContinuationPlugin = createContinuationPlugin;
|
|
2125
|
+
exports.createModelRegistryPlugin = createModelRegistryPlugin;
|
|
2126
|
+
exports.createOpenAIAdapter = createOpenAIAdapter;
|
|
2127
|
+
exports.createToolCallingPlugin = createToolCallingPlugin;
|
|
1303
2128
|
exports.modelRegistryPlugin = modelRegistryPlugin;
|
|
1304
2129
|
exports.openaiAdapter = openaiAdapter;
|
|
1305
2130
|
exports.toolCallingPlugin = toolCallingPlugin;
|