my-ai-chat-framework 3.0.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 +52 -0
- package/README.md +3 -3
- package/README_ZH.md +22 -5
- package/dist/my-ai-chat-framework.browser.es.js +599 -292
- package/dist/my-ai-chat-framework.browser.es.js.map +1 -1
- package/dist/my-ai-chat-framework.browser.umd.js +601 -291
- package/dist/my-ai-chat-framework.browser.umd.js.map +1 -1
- package/dist/my-ai-chat-framework.node.cjs.js +601 -291
- package/dist/my-ai-chat-framework.node.cjs.js.map +1 -1
- package/docs/DEVELOPER.md +100 -10
- package/docs/README.md +33 -0
- package/package.json +1 -1
- package/src/adapters/openai.js +44 -18
- package/src/core/ChatService.js +250 -141
- package/src/core/Errors.js +13 -0
- package/src/core/SystemPromptStore.js +7 -5
- package/src/index.js +2 -1
- package/src/plugins/continuation.js +156 -0
- package/src/plugins/model-registry.js +16 -3
- package/src/plugins/tool-calling.js +46 -18
- package/src/utils/MessageFormatter.js +138 -46
|
@@ -174,6 +174,69 @@ var MessageStore = class {
|
|
|
174
174
|
}
|
|
175
175
|
};
|
|
176
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
|
|
177
240
|
//#region src/core/SystemPromptStore.js
|
|
178
241
|
/**
|
|
179
242
|
* SystemPromptStore — 系统提示词存储
|
|
@@ -201,7 +264,7 @@ var SystemPromptStore = class {
|
|
|
201
264
|
* @returns {Object} 添加的记录
|
|
202
265
|
*/
|
|
203
266
|
add(content, enabled = true) {
|
|
204
|
-
if (!content || typeof content !== "string" || !content.trim()) throw new
|
|
267
|
+
if (!content || typeof content !== "string" || !content.trim()) throw new ValidationError("[SystemPromptStore] content 必须是非空字符串");
|
|
205
268
|
const record = {
|
|
206
269
|
id: `sys_${Date.now()}_${++this._idCounter}`,
|
|
207
270
|
content: content.trim(),
|
|
@@ -217,7 +280,7 @@ var SystemPromptStore = class {
|
|
|
217
280
|
* @returns {Object} 被删除的记录
|
|
218
281
|
*/
|
|
219
282
|
remove(index) {
|
|
220
|
-
if (index < 0 || index >= this._prompts.length) throw new
|
|
283
|
+
if (index < 0 || index >= this._prompts.length) throw new ValidationError(`[SystemPromptStore] 索引越界: ${index}`);
|
|
221
284
|
return this._prompts.splice(index, 1)[0];
|
|
222
285
|
}
|
|
223
286
|
/**
|
|
@@ -226,7 +289,7 @@ var SystemPromptStore = class {
|
|
|
226
289
|
* @returns {boolean} 切换后的状态
|
|
227
290
|
*/
|
|
228
291
|
toggle(index) {
|
|
229
|
-
if (index < 0 || index >= this._prompts.length) throw new
|
|
292
|
+
if (index < 0 || index >= this._prompts.length) throw new ValidationError(`[SystemPromptStore] 索引越界: ${index}`);
|
|
230
293
|
this._prompts[index].enabled = !this._prompts[index].enabled;
|
|
231
294
|
return this._prompts[index].enabled;
|
|
232
295
|
}
|
|
@@ -236,8 +299,8 @@ var SystemPromptStore = class {
|
|
|
236
299
|
* @param {string} content
|
|
237
300
|
*/
|
|
238
301
|
update(index, content) {
|
|
239
|
-
if (index < 0 || index >= this._prompts.length) throw new
|
|
240
|
-
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 必须是非空字符串");
|
|
241
304
|
this._prompts[index].content = content.trim();
|
|
242
305
|
this._prompts[index].timestamp = Date.now();
|
|
243
306
|
}
|
|
@@ -274,60 +337,6 @@ var SystemPromptStore = class {
|
|
|
274
337
|
}
|
|
275
338
|
};
|
|
276
339
|
//#endregion
|
|
277
|
-
//#region src/core/Errors.js
|
|
278
|
-
/**
|
|
279
|
-
* 自定义错误类
|
|
280
|
-
* 用于区分不同类型的错误,方便用户通过 `error.name` 或 `instanceof` 处理
|
|
281
|
-
*/
|
|
282
|
-
var APIError = class extends Error {
|
|
283
|
-
/**
|
|
284
|
-
* @param {string} message - 错误消息(通常来自 API 响应)
|
|
285
|
-
* @param {number} statusCode - HTTP 状态码
|
|
286
|
-
* @param {any} originalError - 原始错误对象或相关信息
|
|
287
|
-
* @param {string} responseText - 原始响应文本(如果有)
|
|
288
|
-
*/
|
|
289
|
-
constructor(message, statusCode, originalError, responseText) {
|
|
290
|
-
super(message);
|
|
291
|
-
this.name = "APIError";
|
|
292
|
-
this.statusCode = statusCode;
|
|
293
|
-
this.originalError = originalError;
|
|
294
|
-
this.responseText = responseText;
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
var NetworkError = class extends Error {
|
|
298
|
-
/**
|
|
299
|
-
* @param {string} message - 错误消息
|
|
300
|
-
* @param {any} originalError - 原始错误对象或相关信息
|
|
301
|
-
*/
|
|
302
|
-
constructor(message, originalError) {
|
|
303
|
-
super(message);
|
|
304
|
-
this.name = "NetworkError";
|
|
305
|
-
this.originalError = originalError;
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
var ConfigurationError = class extends Error {
|
|
309
|
-
/**
|
|
310
|
-
* @param {string} message - 错误消息
|
|
311
|
-
*/
|
|
312
|
-
constructor(message) {
|
|
313
|
-
super(message);
|
|
314
|
-
this.name = "ConfigurationError";
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
var ParsingError = class extends Error {
|
|
318
|
-
/**
|
|
319
|
-
* @param {string} message - 错误消息
|
|
320
|
-
* @param {any} originalError - 原始错误对象或相关信息
|
|
321
|
-
* @param {string} responseText - 原始响应文本(如果有)
|
|
322
|
-
**/
|
|
323
|
-
constructor(message, originalError, responseText) {
|
|
324
|
-
super(message);
|
|
325
|
-
this.name = "ParsingError";
|
|
326
|
-
this.originalError = originalError;
|
|
327
|
-
this.responseText = responseText;
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
//#endregion
|
|
331
340
|
//#region src/core/Pipeline.js
|
|
332
341
|
/**
|
|
333
342
|
* Pipeline —— 极简顺序管道(v3.0 提案 · 阶段 1)
|
|
@@ -404,26 +413,37 @@ var SESSION_CONFIG_KEYS = [
|
|
|
404
413
|
"maxTokens",
|
|
405
414
|
"modelParams",
|
|
406
415
|
"system",
|
|
407
|
-
"retry"
|
|
408
|
-
|
|
416
|
+
"retry"
|
|
417
|
+
];
|
|
418
|
+
var INTERNAL_STAGES = [
|
|
419
|
+
"prepareInput",
|
|
420
|
+
"beforeSend",
|
|
421
|
+
"autoContinue",
|
|
422
|
+
"buildRequest",
|
|
423
|
+
"send"
|
|
409
424
|
];
|
|
410
425
|
var ChatService = class extends EventEmitter {
|
|
411
426
|
constructor(config = {}) {
|
|
412
427
|
super();
|
|
413
428
|
this.config = { ...config };
|
|
414
|
-
|
|
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();
|
|
415
433
|
this.systemPrompts = new SystemPromptStore();
|
|
416
434
|
this._adapter = null;
|
|
417
|
-
this.
|
|
418
|
-
this.
|
|
419
|
-
this._hooks = new EventEmitter();
|
|
420
|
-
this._processResponse = null;
|
|
435
|
+
this._activeRequests = /* @__PURE__ */ new Map();
|
|
436
|
+
this._requestSeq = 0;
|
|
421
437
|
const model = this.config.model || this.config.modelParams?.model;
|
|
422
438
|
if (!model || typeof model !== "string" || !model.trim()) throw new ConfigurationError("缺少 model 配置");
|
|
423
439
|
this._assertValidConfig(this.config);
|
|
424
440
|
if (typeof config.system === "string" && config.system.trim()) this.systemPrompts.set(config.system);
|
|
425
441
|
if (config.adapter) this.setAdapter(config.adapter);
|
|
426
442
|
this._userStages = [];
|
|
443
|
+
this._stageOverrides = /* @__PURE__ */ new Map();
|
|
444
|
+
this._disabledStages = /* @__PURE__ */ new Set();
|
|
445
|
+
this._extraConfigKeys = /* @__PURE__ */ new Map();
|
|
446
|
+
this.plugins = {};
|
|
427
447
|
this._pipeline = this._buildPipeline();
|
|
428
448
|
}
|
|
429
449
|
/**
|
|
@@ -438,8 +458,16 @@ var ChatService = class extends EventEmitter {
|
|
|
438
458
|
const model = cfg.model ?? cfg.modelParams?.model;
|
|
439
459
|
if (model !== void 0 && (typeof model !== "string" || !model.trim())) throw new ConfigurationError("model 必须是非空字符串");
|
|
440
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* 安装插件。
|
|
463
|
+
* - 插件实现 `install(chat, options)`
|
|
464
|
+
* - 若 install 返回一个对象,会挂到 **`chat.plugins[plugin.name]`**(v4.0 命名空间,插件之间不会撞名)
|
|
465
|
+
* - 插件要提供卸载时,自己在返回值里带 `uninstall()`(或调用 `plugin.uninstall(chat)`)
|
|
466
|
+
* @returns {ChatService} this
|
|
467
|
+
*/
|
|
441
468
|
use(plugin, options = {}) {
|
|
442
|
-
plugin.install(this, options);
|
|
469
|
+
const api = plugin.install(this, options);
|
|
470
|
+
if (plugin.name && api && typeof api === "object") this.plugins[plugin.name] = api;
|
|
443
471
|
return this;
|
|
444
472
|
}
|
|
445
473
|
/**
|
|
@@ -454,13 +482,7 @@ var ChatService = class extends EventEmitter {
|
|
|
454
482
|
pipe(stage) {
|
|
455
483
|
if (!stage || typeof stage !== "object" || typeof stage.run !== "function") throw new ConfigurationError("pipe: 需要 { name, phase?, run(ctx) },run 必须是函数");
|
|
456
484
|
if (typeof stage.name !== "string" || !stage.name.trim()) throw new ConfigurationError("pipe: 需要 name(车间名,非空字符串)");
|
|
457
|
-
if (
|
|
458
|
-
"prepareInput",
|
|
459
|
-
"beforeSend",
|
|
460
|
-
"autoContinue",
|
|
461
|
-
"buildRequest",
|
|
462
|
-
"send"
|
|
463
|
-
].includes(stage.name)) throw new ConfigurationError(`pipe: "${stage.name}" 是内部车间名,请换一个名字`);
|
|
485
|
+
if (INTERNAL_STAGES.includes(stage.name)) throw new ConfigurationError(`pipe: "${stage.name}" 是内部车间名,请换一个名字`);
|
|
464
486
|
if (this._userStages.some((s) => s.name === stage.name)) throw new ConfigurationError(`pipe: 车间 "${stage.name}" 已存在,请先 chat.unpipe("${stage.name}")`);
|
|
465
487
|
const phase = stage.phase === "afterSend" ? "afterSend" : "beforeSend";
|
|
466
488
|
this._userStages.push({
|
|
@@ -482,37 +504,86 @@ var ChatService = class extends EventEmitter {
|
|
|
482
504
|
get pipelineStages() {
|
|
483
505
|
return this._pipeline.names();
|
|
484
506
|
}
|
|
485
|
-
/**
|
|
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
|
+
*/
|
|
486
570
|
_buildPipeline() {
|
|
487
571
|
const p = new Pipeline();
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
name
|
|
502
|
-
|
|
503
|
-
});
|
|
504
|
-
p.register({
|
|
505
|
-
name: "buildRequest",
|
|
506
|
-
run: (ctx) => this._stageBuildRequest(ctx)
|
|
507
|
-
});
|
|
508
|
-
p.register({
|
|
509
|
-
name: "send",
|
|
510
|
-
run: (ctx) => this._stageSend(ctx)
|
|
511
|
-
});
|
|
512
|
-
for (const s of this._userStages) if (s.phase === "afterSend") p.register({
|
|
513
|
-
name: s.name,
|
|
514
|
-
run: (ctx) => s.run(ctx)
|
|
515
|
-
});
|
|
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)
|
|
577
|
+
});
|
|
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]
|
|
583
|
+
});
|
|
584
|
+
if (name === "beforeSend") insertUsers("beforeSend");
|
|
585
|
+
if (name === "send") insertUsers("afterSend");
|
|
586
|
+
}
|
|
516
587
|
return p;
|
|
517
588
|
}
|
|
518
589
|
_rebuildPipeline() {
|
|
@@ -522,98 +593,81 @@ var ChatService = class extends EventEmitter {
|
|
|
522
593
|
assertAdapter(adapter);
|
|
523
594
|
this._adapter = adapter;
|
|
524
595
|
}
|
|
525
|
-
|
|
526
|
-
|
|
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();
|
|
527
608
|
}
|
|
609
|
+
/** 是否有请求正在生成中 */
|
|
528
610
|
get isGenerating() {
|
|
529
|
-
return this.
|
|
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
|
+
};
|
|
530
626
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
this.
|
|
534
|
-
try {
|
|
535
|
-
await this._request({
|
|
536
|
-
addUser: false,
|
|
537
|
-
isStream: false,
|
|
538
|
-
mergeToEntry: target.id
|
|
539
|
-
});
|
|
540
|
-
this.messages.update(target.id, {
|
|
541
|
-
_complete: true,
|
|
542
|
-
prefix: void 0,
|
|
543
|
-
_ephemeral: false
|
|
544
|
-
});
|
|
545
|
-
return target;
|
|
546
|
-
} catch (err) {
|
|
547
|
-
this.messages.update(target.id, {
|
|
548
|
-
_complete: true,
|
|
549
|
-
prefix: void 0,
|
|
550
|
-
_ephemeral: false
|
|
551
|
-
});
|
|
552
|
-
if (err.name === "AbortError") {
|
|
553
|
-
this.emit("aborted", { timestamp: Date.now() });
|
|
554
|
-
return target;
|
|
555
|
-
}
|
|
556
|
-
throw err;
|
|
557
|
-
}
|
|
627
|
+
/** 内部:注销一个请求 */
|
|
628
|
+
_endRequest(id) {
|
|
629
|
+
this._activeRequests.delete(id);
|
|
558
630
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
});
|
|
573
|
-
},
|
|
574
|
-
onDone: (final) => {
|
|
575
|
-
if (onDone) onDone(final);
|
|
576
|
-
}
|
|
577
|
-
});
|
|
578
|
-
this.messages.update(target.id, {
|
|
579
|
-
_complete: true,
|
|
580
|
-
prefix: void 0,
|
|
581
|
-
_ephemeral: false
|
|
582
|
-
});
|
|
583
|
-
return target;
|
|
584
|
-
} catch (err) {
|
|
585
|
-
this.messages.update(target.id, {
|
|
586
|
-
_complete: true,
|
|
587
|
-
prefix: void 0,
|
|
588
|
-
_ephemeral: false
|
|
589
|
-
});
|
|
590
|
-
if (err.name === "AbortError") {
|
|
591
|
-
this.emit("aborted", { timestamp: Date.now() });
|
|
592
|
-
return target;
|
|
593
|
-
}
|
|
594
|
-
throw err;
|
|
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);
|
|
595
644
|
}
|
|
645
|
+
return this;
|
|
596
646
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
647
|
+
/** 注销运行时可改字段(插件卸载时调用)@returns {ChatService} this */
|
|
648
|
+
unregisterConfigKeys(keys) {
|
|
649
|
+
for (const k of Array.isArray(keys) ? keys : [keys]) this._extraConfigKeys.delete(k);
|
|
650
|
+
return this;
|
|
651
|
+
}
|
|
652
|
+
/** 当前允许在运行时修改的配置字段(内置基础字段 + 插件注册) */
|
|
653
|
+
get configKeys() {
|
|
654
|
+
return [...SESSION_CONFIG_KEYS, ...this._extraConfigKeys.keys()];
|
|
602
655
|
}
|
|
603
656
|
/**
|
|
604
|
-
*
|
|
657
|
+
* 运行时修改配置(仅"可改字段":内置基础字段 + 插件注册字段)。
|
|
605
658
|
*
|
|
606
|
-
* 白名单:model / temperature / maxTokens / modelParams / system / retry / ephemeralContinue。
|
|
607
659
|
* - 运输层(apiKey/baseUrl/headers):归适配器,要改就换适配器实例
|
|
608
660
|
* - 请求参数默认值:走适配器工厂 options 或请求级覆盖(chat.send(x, params))
|
|
609
|
-
* -
|
|
661
|
+
* - 插件自有配置:插件用 `registerConfigKeys` 登记后才可改(工厂 options 仍是首选)
|
|
610
662
|
*
|
|
611
663
|
* 与构造器一样经过校验,不能注入非法值。
|
|
612
|
-
* @throws {ConfigurationError}
|
|
664
|
+
* @throws {ConfigurationError} 非白名单字段、非法值、或插件校验器拒绝
|
|
613
665
|
*/
|
|
614
666
|
updateConfig(partial) {
|
|
615
|
-
|
|
667
|
+
const allowed = this.configKeys;
|
|
668
|
+
for (const key of Object.keys(partial)) if (!allowed.includes(key)) throw new ConfigurationError(`updateConfig 不支持修改 "${key}"。允许的字段: ${allowed.join(", ")}`);
|
|
616
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])}`);
|
|
617
671
|
Object.assign(this.config, partial);
|
|
618
672
|
if (typeof partial.system === "string") this.systemPrompts.set(partial.system);
|
|
619
673
|
this.emit("config-updated", {
|
|
@@ -679,6 +733,7 @@ var ChatService = class extends EventEmitter {
|
|
|
679
733
|
onProgress: options.onProgress || null,
|
|
680
734
|
onDone: options.onDone || null,
|
|
681
735
|
mergeToEntry: options.mergeToEntry || null,
|
|
736
|
+
signal: options.signal || null,
|
|
682
737
|
body: null,
|
|
683
738
|
result: null
|
|
684
739
|
};
|
|
@@ -704,22 +759,19 @@ var ChatService = class extends EventEmitter {
|
|
|
704
759
|
timestamp: Date.now()
|
|
705
760
|
});
|
|
706
761
|
}
|
|
707
|
-
/**
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
if (last && last.prefix && (last.role === "assistant" || last._ephemeral)) ctx.mergeToEntry = last.id;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
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) {}
|
|
723
775
|
/** 车间 4:构建请求体 */
|
|
724
776
|
async _stageBuildRequest(ctx) {
|
|
725
777
|
ctx.body = this._adapter.buildRequest(ctx.messages.getAll(), ctx.config, ctx.systemPrompts.getEnabled());
|
|
@@ -734,7 +786,8 @@ var ChatService = class extends EventEmitter {
|
|
|
734
786
|
mergeToEntry: ctx.mergeToEntry,
|
|
735
787
|
config: ctx.config,
|
|
736
788
|
maxRetries: retryCfg.maxRetries ?? 0,
|
|
737
|
-
retryDelay: retryCfg.retryDelay ?? 1e3
|
|
789
|
+
retryDelay: retryCfg.retryDelay ?? 1e3,
|
|
790
|
+
signal: ctx.signal
|
|
738
791
|
});
|
|
739
792
|
}
|
|
740
793
|
_addUserMessage(userInput, addUser) {
|
|
@@ -750,9 +803,9 @@ var ChatService = class extends EventEmitter {
|
|
|
750
803
|
}
|
|
751
804
|
}
|
|
752
805
|
/** retry 循环,占位消息只 push 一次 */
|
|
753
|
-
async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, config, maxRetries, retryDelay }) {
|
|
806
|
+
async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, config, maxRetries, retryDelay, signal }) {
|
|
754
807
|
let lastError = null;
|
|
755
|
-
const adapterOptions = { signal
|
|
808
|
+
const adapterOptions = { signal };
|
|
756
809
|
let placeholder = null;
|
|
757
810
|
let base = null;
|
|
758
811
|
if (isStream) {
|
|
@@ -780,7 +833,6 @@ var ChatService = class extends EventEmitter {
|
|
|
780
833
|
await new Promise((r) => setTimeout(r, retryDelay));
|
|
781
834
|
}
|
|
782
835
|
try {
|
|
783
|
-
this._isGenerating = true;
|
|
784
836
|
if (isStream) return await this._stream(body, adapterOptions, {
|
|
785
837
|
placeholder,
|
|
786
838
|
base,
|
|
@@ -791,22 +843,15 @@ var ChatService = class extends EventEmitter {
|
|
|
791
843
|
});
|
|
792
844
|
else {
|
|
793
845
|
const resp = await this._adapter.send(body, config, adapterOptions);
|
|
794
|
-
|
|
795
|
-
if (this._processResponse && !mergeToEntry) result = await this._processResponse(result, { isStream: false });
|
|
796
|
-
return result;
|
|
846
|
+
return this._handleResult(this._adapter.parseResponse(resp), mergeToEntry);
|
|
797
847
|
}
|
|
798
848
|
} catch (error) {
|
|
799
849
|
lastError = error;
|
|
800
|
-
if (error.name === "AbortError" ||
|
|
801
|
-
this._isGenerating = false;
|
|
802
|
-
throw error;
|
|
803
|
-
}
|
|
850
|
+
if (error.name === "AbortError" || signal?.aborted) throw error;
|
|
804
851
|
if (error instanceof NetworkError && attempt < maxRetries) continue;
|
|
805
|
-
this._isGenerating = false;
|
|
806
852
|
throw error;
|
|
807
853
|
}
|
|
808
854
|
}
|
|
809
|
-
this._isGenerating = false;
|
|
810
855
|
throw lastError;
|
|
811
856
|
}
|
|
812
857
|
/**
|
|
@@ -821,19 +866,14 @@ var ChatService = class extends EventEmitter {
|
|
|
821
866
|
*/
|
|
822
867
|
async _stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone, config }) {
|
|
823
868
|
let finalMsg = null;
|
|
824
|
-
await this._adapter.stream(body, config, (snap) => {
|
|
869
|
+
await this._adapter.stream(body, config, (snap, delta) => {
|
|
825
870
|
placeholder.content = mergeToEntry && base ? base.content + (snap.content || "") : snap.content || "";
|
|
826
871
|
if (snap.reasoningContent) placeholder.reasoningContent = mergeToEntry && base ? base.reasoning + snap.reasoningContent : snap.reasoningContent;
|
|
827
872
|
if (snap.toolCalls) placeholder.toolCalls = [...snap.toolCalls];
|
|
828
873
|
this.emit("stream-progress", snap);
|
|
829
|
-
if (onProgress) onProgress(snap);
|
|
874
|
+
if (onProgress) onProgress(snap, delta);
|
|
830
875
|
}, async (final) => {
|
|
831
876
|
finalMsg = final;
|
|
832
|
-
if (this._processResponse && !mergeToEntry) finalMsg = await this._processResponse(finalMsg, {
|
|
833
|
-
isStream: true,
|
|
834
|
-
onProgress,
|
|
835
|
-
onDone
|
|
836
|
-
});
|
|
837
877
|
if (mergeToEntry && base) {
|
|
838
878
|
const changes = {
|
|
839
879
|
content: base.content + (final.content || ""),
|
|
@@ -887,13 +927,14 @@ var ChatService = class extends EventEmitter {
|
|
|
887
927
|
* @param {Object} [params] - 请求级参数覆盖(model/temperature/modelParams 等,仅本次生效)
|
|
888
928
|
*/
|
|
889
929
|
async send(userInput, params) {
|
|
890
|
-
|
|
930
|
+
const { id, signal } = this._beginRequest();
|
|
891
931
|
try {
|
|
892
932
|
return await this._request({
|
|
893
933
|
userInput,
|
|
894
934
|
addUser: true,
|
|
895
935
|
isStream: false,
|
|
896
|
-
params
|
|
936
|
+
params,
|
|
937
|
+
signal
|
|
897
938
|
});
|
|
898
939
|
} catch (err) {
|
|
899
940
|
if (err.name === "AbortError") {
|
|
@@ -902,8 +943,7 @@ var ChatService = class extends EventEmitter {
|
|
|
902
943
|
}
|
|
903
944
|
throw err;
|
|
904
945
|
} finally {
|
|
905
|
-
this.
|
|
906
|
-
this._isGenerating = false;
|
|
946
|
+
this._endRequest(id);
|
|
907
947
|
}
|
|
908
948
|
}
|
|
909
949
|
/**
|
|
@@ -919,7 +959,7 @@ var ChatService = class extends EventEmitter {
|
|
|
919
959
|
onProgress = params;
|
|
920
960
|
params = void 0;
|
|
921
961
|
}
|
|
922
|
-
|
|
962
|
+
const { id, signal } = this._beginRequest();
|
|
923
963
|
try {
|
|
924
964
|
return await this._request({
|
|
925
965
|
userInput,
|
|
@@ -927,7 +967,8 @@ var ChatService = class extends EventEmitter {
|
|
|
927
967
|
isStream: true,
|
|
928
968
|
onProgress,
|
|
929
969
|
onDone,
|
|
930
|
-
params
|
|
970
|
+
params,
|
|
971
|
+
signal
|
|
931
972
|
});
|
|
932
973
|
} catch (err) {
|
|
933
974
|
if (err.name === "AbortError") {
|
|
@@ -936,21 +977,24 @@ var ChatService = class extends EventEmitter {
|
|
|
936
977
|
}
|
|
937
978
|
throw err;
|
|
938
979
|
} finally {
|
|
939
|
-
this.
|
|
940
|
-
this._isGenerating = false;
|
|
980
|
+
this._endRequest(id);
|
|
941
981
|
}
|
|
942
982
|
}
|
|
943
983
|
/**
|
|
944
984
|
* 重发当前消息(不加用户消息)。
|
|
945
985
|
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
986
|
+
* @param {Object} [options] - { mergeToEntry } 可选:把结果合并进指定的已有消息(续写等场景)
|
|
946
987
|
*/
|
|
947
|
-
async sendExisting(params) {
|
|
948
|
-
|
|
988
|
+
async sendExisting(params, options = {}) {
|
|
989
|
+
const mergeToEntry = options?.mergeToEntry || null;
|
|
990
|
+
const { id, signal } = this._beginRequest();
|
|
949
991
|
try {
|
|
950
992
|
return await this._request({
|
|
951
993
|
addUser: false,
|
|
952
994
|
isStream: false,
|
|
953
|
-
params
|
|
995
|
+
params,
|
|
996
|
+
mergeToEntry,
|
|
997
|
+
signal
|
|
954
998
|
});
|
|
955
999
|
} catch (err) {
|
|
956
1000
|
if (err.name === "AbortError") {
|
|
@@ -959,30 +1003,38 @@ var ChatService = class extends EventEmitter {
|
|
|
959
1003
|
}
|
|
960
1004
|
throw err;
|
|
961
1005
|
} finally {
|
|
962
|
-
this.
|
|
963
|
-
this._isGenerating = false;
|
|
1006
|
+
this._endRequest(id);
|
|
964
1007
|
}
|
|
965
1008
|
}
|
|
966
1009
|
/**
|
|
967
1010
|
* 流式重发(不加用户消息)。
|
|
968
1011
|
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
1012
|
+
* @param {Object} [options] - { mergeToEntry } 可选:把结果合并进指定的已有消息
|
|
969
1013
|
* @param {Function} [onProgress] - 流式进度回调
|
|
970
1014
|
* @param {Function} [onDone] - 流式完成回调
|
|
971
1015
|
*/
|
|
972
|
-
async sendExistingStream(params, onProgress, onDone) {
|
|
1016
|
+
async sendExistingStream(params, options, onProgress, onDone) {
|
|
973
1017
|
if (typeof params === "function") {
|
|
974
|
-
onDone =
|
|
1018
|
+
onDone = options;
|
|
975
1019
|
onProgress = params;
|
|
1020
|
+
options = {};
|
|
976
1021
|
params = void 0;
|
|
1022
|
+
} else if (typeof options === "function") {
|
|
1023
|
+
onDone = onProgress;
|
|
1024
|
+
onProgress = options;
|
|
1025
|
+
options = {};
|
|
977
1026
|
}
|
|
978
|
-
|
|
1027
|
+
const mergeToEntry = options?.mergeToEntry || null;
|
|
1028
|
+
const { id, signal } = this._beginRequest();
|
|
979
1029
|
try {
|
|
980
1030
|
return await this._request({
|
|
981
1031
|
addUser: false,
|
|
982
1032
|
isStream: true,
|
|
983
1033
|
onProgress,
|
|
984
1034
|
onDone,
|
|
985
|
-
params
|
|
1035
|
+
params,
|
|
1036
|
+
mergeToEntry,
|
|
1037
|
+
signal
|
|
986
1038
|
});
|
|
987
1039
|
} catch (err) {
|
|
988
1040
|
if (err.name === "AbortError") {
|
|
@@ -991,8 +1043,7 @@ var ChatService = class extends EventEmitter {
|
|
|
991
1043
|
}
|
|
992
1044
|
throw err;
|
|
993
1045
|
} finally {
|
|
994
|
-
this.
|
|
995
|
-
this._isGenerating = false;
|
|
1046
|
+
this._endRequest(id);
|
|
996
1047
|
}
|
|
997
1048
|
}
|
|
998
1049
|
};
|
|
@@ -1043,26 +1094,23 @@ function isString(value) {
|
|
|
1043
1094
|
//#endregion
|
|
1044
1095
|
//#region src/utils/MessageFormatter.js
|
|
1045
1096
|
/**
|
|
1046
|
-
* MessageFormatter — 可注册的消息格式转换器
|
|
1097
|
+
* MessageFormatter — 可注册的消息格式转换器 + 可注册的过滤器链(v4.0)
|
|
1047
1098
|
*
|
|
1048
1099
|
* 职责:
|
|
1049
|
-
* 1. 内置
|
|
1100
|
+
* 1. 内置 OpenAI 兼容格式的转换逻辑
|
|
1050
1101
|
* 2. 支持 register(name, fn) 注册自定义格式(如 Anthropic、Gemini 等)
|
|
1051
|
-
* 3.
|
|
1052
|
-
*
|
|
1102
|
+
* 3. v4.0:消息"保留 / 丢弃"规则改为可注册的过滤器链(registerFilter)
|
|
1103
|
+
* —— 原来硬编码在格式里的 ephemeral / system / 空内容规则,现在都是过滤器;
|
|
1104
|
+
* 业务插件(如 continuation)可以同名覆盖内置过滤器,接管规则。
|
|
1053
1105
|
*
|
|
1054
1106
|
* 用法:
|
|
1055
1107
|
* import { MessageFormatter } from 'my-ai-chat-framework';
|
|
1056
1108
|
*
|
|
1057
1109
|
* // 使用内置格式
|
|
1058
|
-
* const msgs = MessageFormatter.format({
|
|
1059
|
-
* messages, systemPrompts, capabilities, resolveImage
|
|
1060
|
-
* }); // 默认 'openai'
|
|
1110
|
+
* const msgs = MessageFormatter.format({ messages, systemPrompts, capabilities, resolveImage });
|
|
1061
1111
|
*
|
|
1062
|
-
* //
|
|
1063
|
-
* MessageFormatter.
|
|
1064
|
-
* // 返回 Anthropic 格式的消息数组
|
|
1065
|
-
* });
|
|
1112
|
+
* // 注册自己的过滤器(决定哪些消息进入请求)
|
|
1113
|
+
* MessageFormatter.registerFilter('my-rule', (msg, ctx) => msg.role === 'system' ? null : msg);
|
|
1066
1114
|
*/
|
|
1067
1115
|
var IS_URL = /^https?:\/\//i;
|
|
1068
1116
|
function defaultResolveImage(imageId) {
|
|
@@ -1091,7 +1139,23 @@ function buildMultimodalContent(textContent, images, resolveImage) {
|
|
|
1091
1139
|
}
|
|
1092
1140
|
return content;
|
|
1093
1141
|
}
|
|
1094
|
-
/**
|
|
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
|
+
}
|
|
1095
1159
|
function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
|
|
1096
1160
|
const rImg = typeof resolveImage === "function" ? resolveImage : defaultResolveImage;
|
|
1097
1161
|
const result = [];
|
|
@@ -1099,15 +1163,8 @@ function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
|
|
|
1099
1163
|
role: "system",
|
|
1100
1164
|
content: sp.content.trim()
|
|
1101
1165
|
});
|
|
1102
|
-
|
|
1103
|
-
for (
|
|
1104
|
-
else break;
|
|
1105
|
-
for (let i = 0; i < messages.length; i++) {
|
|
1106
|
-
const msg = messages[i];
|
|
1107
|
-
if (msg._ephemeral) {
|
|
1108
|
-
if (i < ephemEnd) continue;
|
|
1109
|
-
}
|
|
1110
|
-
if (msg.role === "system" && !msg._ephemeral) continue;
|
|
1166
|
+
const kept = MessageFormatter._runFilters(messages, capabilities || {});
|
|
1167
|
+
for (const msg of kept) {
|
|
1111
1168
|
if (msg.role === "tool") {
|
|
1112
1169
|
result.push({
|
|
1113
1170
|
role: "tool",
|
|
@@ -1143,7 +1200,7 @@ function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
|
|
|
1143
1200
|
}
|
|
1144
1201
|
const entry = { role: msg.role };
|
|
1145
1202
|
if (hasImages) {
|
|
1146
|
-
if (!capabilities?.vision) throw new
|
|
1203
|
+
if (!capabilities?.vision) throw new ValidationError("[MessageFormatter] 消息包含图片但模型不支持视觉(capabilities.vision=false)。请切换模型或移除图片。");
|
|
1147
1204
|
entry.content = buildMultimodalContent(msg.content || "", msg.images, rImg);
|
|
1148
1205
|
} else entry.content = msg.content;
|
|
1149
1206
|
if (msg.prefix) entry.prefix = true;
|
|
@@ -1153,22 +1210,71 @@ function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
|
|
|
1153
1210
|
}
|
|
1154
1211
|
var MessageFormatter = {
|
|
1155
1212
|
_formats: new Map([["openai", toOpenAI]]),
|
|
1213
|
+
_filters: /* @__PURE__ */ new Map(),
|
|
1214
|
+
_filterOrder: 0,
|
|
1156
1215
|
register(name, fn) {
|
|
1157
|
-
if (!name || typeof name !== "string" || !name.trim()) throw new
|
|
1158
|
-
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");
|
|
1159
1218
|
this._formats.set(name.trim(), fn);
|
|
1160
1219
|
},
|
|
1161
1220
|
unregister(name) {
|
|
1162
|
-
if (name === "openai") throw new
|
|
1221
|
+
if (name === "openai") throw new ValidationError("[MessageFormatter] 内置格式 \"openai\" 不可移除");
|
|
1163
1222
|
this._formats.delete(name);
|
|
1164
1223
|
},
|
|
1165
1224
|
listFormats() {
|
|
1166
1225
|
return [...this._formats.keys()];
|
|
1167
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
|
+
},
|
|
1168
1274
|
format(options = {}) {
|
|
1169
1275
|
const formatName = options.format || "openai";
|
|
1170
1276
|
const fn = this._formats.get(formatName);
|
|
1171
|
-
if (!fn) throw new
|
|
1277
|
+
if (!fn) throw new ValidationError(`[MessageFormatter] 未知格式 "${formatName}"。可用格式: ${[...this._formats.keys()].join(", ")}`);
|
|
1172
1278
|
return fn({
|
|
1173
1279
|
messages: options.messages || [],
|
|
1174
1280
|
systemPrompts: options.systemPrompts || [],
|
|
@@ -1177,6 +1283,8 @@ var MessageFormatter = {
|
|
|
1177
1283
|
});
|
|
1178
1284
|
}
|
|
1179
1285
|
};
|
|
1286
|
+
MessageFormatter.registerFilter("system-drop", filterSystemDrop, { priority: 900 });
|
|
1287
|
+
MessageFormatter.registerFilter("empty-drop", filterEmptyDrop, { priority: 910 });
|
|
1180
1288
|
function assembleMessages(options = {}) {
|
|
1181
1289
|
return MessageFormatter.format({
|
|
1182
1290
|
...options,
|
|
@@ -1226,10 +1334,10 @@ var openaiAdapter = {
|
|
|
1226
1334
|
buildRequest(messages, config, systemPrompts = []) {
|
|
1227
1335
|
config = this._resolveConfig(config);
|
|
1228
1336
|
const model = config.model || config.modelParams?.model;
|
|
1229
|
-
if (!model) throw new
|
|
1337
|
+
if (!model) throw new ConfigurationError("Missing required config: model (either at top level or in modelParams)");
|
|
1230
1338
|
const mp = config.modelParams || {};
|
|
1231
|
-
const temperature = mp.temperature ?? config.temperature
|
|
1232
|
-
const maxTokens = mp.maxTokens ?? config.maxTokens
|
|
1339
|
+
const temperature = mp.temperature ?? config.temperature;
|
|
1340
|
+
const maxTokens = mp.maxTokens ?? config.maxTokens;
|
|
1233
1341
|
const reasoningEffort = mp.reasoningEffort ?? config.reasoningEffort;
|
|
1234
1342
|
const requestBody = {
|
|
1235
1343
|
model,
|
|
@@ -1240,10 +1348,10 @@ var openaiAdapter = {
|
|
|
1240
1348
|
resolveImage: config.resolveImage,
|
|
1241
1349
|
format: config.messageFormat
|
|
1242
1350
|
}),
|
|
1243
|
-
temperature,
|
|
1244
|
-
max_tokens: maxTokens,
|
|
1245
1351
|
stream: false
|
|
1246
1352
|
};
|
|
1353
|
+
if (temperature !== void 0) requestBody.temperature = temperature;
|
|
1354
|
+
if (maxTokens !== void 0) requestBody.max_tokens = maxTokens;
|
|
1247
1355
|
if (mp.topP !== void 0) requestBody.top_p = mp.topP;
|
|
1248
1356
|
if (mp.frequencyPenalty !== void 0) requestBody.frequency_penalty = mp.frequencyPenalty;
|
|
1249
1357
|
if (mp.presencePenalty !== void 0) requestBody.presence_penalty = mp.presencePenalty;
|
|
@@ -1350,22 +1458,37 @@ var openaiAdapter = {
|
|
|
1350
1458
|
content: "",
|
|
1351
1459
|
reasoningContent: ""
|
|
1352
1460
|
};
|
|
1461
|
+
let reachedDone = false;
|
|
1353
1462
|
while (true) {
|
|
1354
1463
|
if (options.signal?.aborted) break;
|
|
1355
1464
|
const { done, value } = await reader.read();
|
|
1356
1465
|
if (done) break;
|
|
1357
1466
|
buffer += decoder.decode(value, { stream: true });
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
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
|
+
}
|
|
1364
1478
|
try {
|
|
1365
|
-
const delta = JSON.parse(
|
|
1479
|
+
const delta = JSON.parse(dataPayload).choices?.[0]?.delta;
|
|
1366
1480
|
if (!delta) continue;
|
|
1367
|
-
|
|
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
|
+
}
|
|
1368
1490
|
if (delta.tool_calls) {
|
|
1491
|
+
const newCalls = [];
|
|
1369
1492
|
if (!accumulated.toolCalls) accumulated.toolCalls = [];
|
|
1370
1493
|
for (const toolCallDelta of delta.tool_calls) {
|
|
1371
1494
|
const index = toolCallDelta.index;
|
|
@@ -1381,14 +1504,23 @@ var openaiAdapter = {
|
|
|
1381
1504
|
if (toolCallDelta.type) accumulated.toolCalls[index].type = toolCallDelta.type;
|
|
1382
1505
|
if (toolCallDelta.function?.name) accumulated.toolCalls[index].function.name += toolCallDelta.function.name;
|
|
1383
1506
|
if (toolCallDelta.function?.arguments) accumulated.toolCalls[index].function.arguments += toolCallDelta.function.arguments;
|
|
1507
|
+
newCalls.push({
|
|
1508
|
+
index,
|
|
1509
|
+
delta: toolCallDelta
|
|
1510
|
+
});
|
|
1384
1511
|
}
|
|
1512
|
+
deltaInfo.toolCalls = newCalls;
|
|
1513
|
+
}
|
|
1514
|
+
if (delta.reasoning_content) {
|
|
1515
|
+
accumulated.reasoningContent += delta.reasoning_content;
|
|
1516
|
+
deltaInfo.reasoningContent = delta.reasoning_content;
|
|
1385
1517
|
}
|
|
1386
|
-
|
|
1387
|
-
onProgress({ ...accumulated });
|
|
1518
|
+
onProgress({ ...accumulated }, deltaInfo);
|
|
1388
1519
|
} catch (e) {
|
|
1389
|
-
console.warn("stream parsing failed :", e,
|
|
1520
|
+
console.warn("stream parsing failed :", e, dataPayload);
|
|
1390
1521
|
}
|
|
1391
1522
|
}
|
|
1523
|
+
if (reachedDone) break;
|
|
1392
1524
|
}
|
|
1393
1525
|
onDone({
|
|
1394
1526
|
role: "assistant",
|
|
@@ -1450,12 +1582,14 @@ function createOpenAIAdapter(options = {}) {
|
|
|
1450
1582
|
var toolCallingPlugin = createToolCallingPlugin();
|
|
1451
1583
|
/**
|
|
1452
1584
|
* 创建工具调用插件实例(工厂)。
|
|
1453
|
-
* @param {Object} [options] — { timeout
|
|
1585
|
+
* @param {Object} [options] — { timeout?, maxIterations? }
|
|
1586
|
+
* - 两者**默认都不限制**:不传就是"循环到没有工具调用为止 / 不超时"
|
|
1587
|
+
* - 想限制才传(这是使用者的选择,不是框架替你做的决定)
|
|
1454
1588
|
*/
|
|
1455
1589
|
function createToolCallingPlugin(options = {}) {
|
|
1456
1590
|
return {
|
|
1457
1591
|
name: "tool-calling",
|
|
1458
|
-
maxIterations: options.maxIterations
|
|
1592
|
+
maxIterations: options.maxIterations ?? Infinity,
|
|
1459
1593
|
_options: { ...options },
|
|
1460
1594
|
_tools: /* @__PURE__ */ new Map(),
|
|
1461
1595
|
_toolDefs: [],
|
|
@@ -1481,11 +1615,18 @@ function createToolCallingPlugin(options = {}) {
|
|
|
1481
1615
|
* @param {Object} parameters - JSON Schema 参数定义(可选,默认为空对象)
|
|
1482
1616
|
* @returns {ChatService} 返回 chatService 实例,支持链式调用
|
|
1483
1617
|
*/
|
|
1484
|
-
|
|
1618
|
+
const registerTool = (name, description, executor, parameters = {}) => {
|
|
1485
1619
|
this._tools.set(name, {
|
|
1486
1620
|
executor,
|
|
1487
1621
|
description
|
|
1488
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
|
+
}
|
|
1489
1630
|
const toolDefinition = {
|
|
1490
1631
|
type: "function",
|
|
1491
1632
|
function: {
|
|
@@ -1493,8 +1634,8 @@ function createToolCallingPlugin(options = {}) {
|
|
|
1493
1634
|
description,
|
|
1494
1635
|
parameters: {
|
|
1495
1636
|
type: "object",
|
|
1496
|
-
properties
|
|
1497
|
-
required:
|
|
1637
|
+
properties,
|
|
1638
|
+
required: requiredKeys
|
|
1498
1639
|
}
|
|
1499
1640
|
}
|
|
1500
1641
|
};
|
|
@@ -1508,6 +1649,18 @@ function createToolCallingPlugin(options = {}) {
|
|
|
1508
1649
|
ctx.result = await this._handleWithTools(ctx.result, ctx.isStream, ctx.onProgress || void 0, ctx.onDone || void 0);
|
|
1509
1650
|
}
|
|
1510
1651
|
});
|
|
1652
|
+
return {
|
|
1653
|
+
registerTool,
|
|
1654
|
+
tools: this._tools,
|
|
1655
|
+
uninstall: (c) => this.uninstall(c)
|
|
1656
|
+
};
|
|
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;
|
|
1511
1664
|
},
|
|
1512
1665
|
async _handleWithTools(initialResponse, isStream, onProgress, onDone) {
|
|
1513
1666
|
const self = this;
|
|
@@ -1559,15 +1712,27 @@ function createToolCallingPlugin(options = {}) {
|
|
|
1559
1712
|
};
|
|
1560
1713
|
}
|
|
1561
1714
|
try {
|
|
1562
|
-
const toolTimeout = self._options.timeout
|
|
1563
|
-
let
|
|
1564
|
-
if (
|
|
1715
|
+
const toolTimeout = self._options.timeout;
|
|
1716
|
+
let result;
|
|
1717
|
+
if (typeof toolTimeout === "number" && toolTimeout > 0) {
|
|
1718
|
+
const controller = new AbortController();
|
|
1565
1719
|
const timeoutErr = /* @__PURE__ */ new Error(`工具 ${toolName} 执行超时 (${toolTimeout}ms)`);
|
|
1566
1720
|
timeoutErr.name = "ToolTimeoutError";
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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);
|
|
1571
1736
|
chat.emit("tool-success", {
|
|
1572
1737
|
toolName,
|
|
1573
1738
|
result,
|
|
@@ -1750,8 +1915,8 @@ function createModelRegistryPlugin(options = {}) {
|
|
|
1750
1915
|
* @param {Object} capabilities — 能力标签对象(部分字段即可,未提供的取默认值)
|
|
1751
1916
|
* @returns {ChatService}
|
|
1752
1917
|
*/
|
|
1753
|
-
|
|
1754
|
-
if (!name || typeof name !== "string" || !name.trim()) throw new
|
|
1918
|
+
const registerModel = (name, capabilities = {}) => {
|
|
1919
|
+
if (!name || typeof name !== "string" || !name.trim()) throw new ValidationError("[model-registry] 模型名称必须是非空字符串");
|
|
1755
1920
|
const merged = {
|
|
1756
1921
|
...DEFAULT_CAPABILITIES,
|
|
1757
1922
|
...capabilities
|
|
@@ -1764,7 +1929,7 @@ function createModelRegistryPlugin(options = {}) {
|
|
|
1764
1929
|
* 列出所有已注册的模型名称
|
|
1765
1930
|
* @returns {Array<string>}
|
|
1766
1931
|
*/
|
|
1767
|
-
|
|
1932
|
+
const listModels = () => {
|
|
1768
1933
|
return [...this._registry.keys()];
|
|
1769
1934
|
};
|
|
1770
1935
|
this._syncCapabilities();
|
|
@@ -1787,6 +1952,17 @@ function createModelRegistryPlugin(options = {}) {
|
|
|
1787
1952
|
};
|
|
1788
1953
|
}
|
|
1789
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;
|
|
1790
1966
|
},
|
|
1791
1967
|
_lookupCapabilities() {
|
|
1792
1968
|
const model = this.chat.config.model;
|
|
@@ -1800,6 +1976,137 @@ function createModelRegistryPlugin(options = {}) {
|
|
|
1800
1976
|
};
|
|
1801
1977
|
}
|
|
1802
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();
|
|
2109
|
+
//#endregion
|
|
1803
2110
|
exports.APIError = APIError;
|
|
1804
2111
|
exports.ChatService = ChatService;
|
|
1805
2112
|
exports.ConfigurationError = ConfigurationError;
|
|
@@ -1810,8 +2117,11 @@ exports.NetworkError = NetworkError;
|
|
|
1810
2117
|
exports.ParsingError = ParsingError;
|
|
1811
2118
|
exports.Pipeline = Pipeline;
|
|
1812
2119
|
exports.SystemPromptStore = SystemPromptStore;
|
|
2120
|
+
exports.ValidationError = ValidationError;
|
|
1813
2121
|
exports.assembleMessages = assembleMessages;
|
|
1814
2122
|
exports.assertAdapter = assertAdapter;
|
|
2123
|
+
exports.continuationPlugin = continuationPlugin;
|
|
2124
|
+
exports.createContinuationPlugin = createContinuationPlugin;
|
|
1815
2125
|
exports.createModelRegistryPlugin = createModelRegistryPlugin;
|
|
1816
2126
|
exports.createOpenAIAdapter = createOpenAIAdapter;
|
|
1817
2127
|
exports.createToolCallingPlugin = createToolCallingPlugin;
|