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.
@@ -173,6 +173,69 @@ var MessageStore = class {
173
173
  }
174
174
  };
175
175
  //#endregion
176
+ //#region src/core/Errors.js
177
+ /**
178
+ * 自定义错误类
179
+ * 用于区分不同类型的错误,方便用户通过 `error.name` 或 `instanceof` 处理
180
+ */
181
+ var APIError = class extends Error {
182
+ /**
183
+ * @param {string} message - 错误消息(通常来自 API 响应)
184
+ * @param {number} statusCode - HTTP 状态码
185
+ * @param {any} originalError - 原始错误对象或相关信息
186
+ * @param {string} responseText - 原始响应文本(如果有)
187
+ */
188
+ constructor(message, statusCode, originalError, responseText) {
189
+ super(message);
190
+ this.name = "APIError";
191
+ this.statusCode = statusCode;
192
+ this.originalError = originalError;
193
+ this.responseText = responseText;
194
+ }
195
+ };
196
+ var NetworkError = class extends Error {
197
+ /**
198
+ * @param {string} message - 错误消息
199
+ * @param {any} originalError - 原始错误对象或相关信息
200
+ */
201
+ constructor(message, originalError) {
202
+ super(message);
203
+ this.name = "NetworkError";
204
+ this.originalError = originalError;
205
+ }
206
+ };
207
+ var ConfigurationError = class extends Error {
208
+ /**
209
+ * @param {string} message - 错误消息
210
+ */
211
+ constructor(message) {
212
+ super(message);
213
+ this.name = "ConfigurationError";
214
+ }
215
+ };
216
+ var ValidationError = class extends ConfigurationError {
217
+ /**
218
+ * @param {string} message - 错误消息
219
+ */
220
+ constructor(message) {
221
+ super(message);
222
+ this.name = "ValidationError";
223
+ }
224
+ };
225
+ var ParsingError = class extends Error {
226
+ /**
227
+ * @param {string} message - 错误消息
228
+ * @param {any} originalError - 原始错误对象或相关信息
229
+ * @param {string} responseText - 原始响应文本(如果有)
230
+ **/
231
+ constructor(message, originalError, responseText) {
232
+ super(message);
233
+ this.name = "ParsingError";
234
+ this.originalError = originalError;
235
+ this.responseText = responseText;
236
+ }
237
+ };
238
+ //#endregion
176
239
  //#region src/core/SystemPromptStore.js
177
240
  /**
178
241
  * SystemPromptStore — 系统提示词存储
@@ -200,7 +263,7 @@ var SystemPromptStore = class {
200
263
  * @returns {Object} 添加的记录
201
264
  */
202
265
  add(content, enabled = true) {
203
- if (!content || typeof content !== "string" || !content.trim()) throw new Error("[SystemPromptStore] content 必须是非空字符串");
266
+ if (!content || typeof content !== "string" || !content.trim()) throw new ValidationError("[SystemPromptStore] content 必须是非空字符串");
204
267
  const record = {
205
268
  id: `sys_${Date.now()}_${++this._idCounter}`,
206
269
  content: content.trim(),
@@ -216,7 +279,7 @@ var SystemPromptStore = class {
216
279
  * @returns {Object} 被删除的记录
217
280
  */
218
281
  remove(index) {
219
- if (index < 0 || index >= this._prompts.length) throw new Error(`[SystemPromptStore] 索引越界: ${index}`);
282
+ if (index < 0 || index >= this._prompts.length) throw new ValidationError(`[SystemPromptStore] 索引越界: ${index}`);
220
283
  return this._prompts.splice(index, 1)[0];
221
284
  }
222
285
  /**
@@ -225,7 +288,7 @@ var SystemPromptStore = class {
225
288
  * @returns {boolean} 切换后的状态
226
289
  */
227
290
  toggle(index) {
228
- if (index < 0 || index >= this._prompts.length) throw new Error(`[SystemPromptStore] 索引越界: ${index}`);
291
+ if (index < 0 || index >= this._prompts.length) throw new ValidationError(`[SystemPromptStore] 索引越界: ${index}`);
229
292
  this._prompts[index].enabled = !this._prompts[index].enabled;
230
293
  return this._prompts[index].enabled;
231
294
  }
@@ -235,8 +298,8 @@ var SystemPromptStore = class {
235
298
  * @param {string} content
236
299
  */
237
300
  update(index, content) {
238
- if (index < 0 || index >= this._prompts.length) throw new Error(`[SystemPromptStore] 索引越界: ${index}`);
239
- if (!content || typeof content !== "string" || !content.trim()) throw new Error("[SystemPromptStore] content 必须是非空字符串");
301
+ if (index < 0 || index >= this._prompts.length) throw new ValidationError(`[SystemPromptStore] 索引越界: ${index}`);
302
+ if (!content || typeof content !== "string" || !content.trim()) throw new ValidationError("[SystemPromptStore] content 必须是非空字符串");
240
303
  this._prompts[index].content = content.trim();
241
304
  this._prompts[index].timestamp = Date.now();
242
305
  }
@@ -273,60 +336,6 @@ var SystemPromptStore = class {
273
336
  }
274
337
  };
275
338
  //#endregion
276
- //#region src/core/Errors.js
277
- /**
278
- * 自定义错误类
279
- * 用于区分不同类型的错误,方便用户通过 `error.name` 或 `instanceof` 处理
280
- */
281
- var APIError = class extends Error {
282
- /**
283
- * @param {string} message - 错误消息(通常来自 API 响应)
284
- * @param {number} statusCode - HTTP 状态码
285
- * @param {any} originalError - 原始错误对象或相关信息
286
- * @param {string} responseText - 原始响应文本(如果有)
287
- */
288
- constructor(message, statusCode, originalError, responseText) {
289
- super(message);
290
- this.name = "APIError";
291
- this.statusCode = statusCode;
292
- this.originalError = originalError;
293
- this.responseText = responseText;
294
- }
295
- };
296
- var NetworkError = class extends Error {
297
- /**
298
- * @param {string} message - 错误消息
299
- * @param {any} originalError - 原始错误对象或相关信息
300
- */
301
- constructor(message, originalError) {
302
- super(message);
303
- this.name = "NetworkError";
304
- this.originalError = originalError;
305
- }
306
- };
307
- var ConfigurationError = class extends Error {
308
- /**
309
- * @param {string} message - 错误消息
310
- */
311
- constructor(message) {
312
- super(message);
313
- this.name = "ConfigurationError";
314
- }
315
- };
316
- var ParsingError = class extends Error {
317
- /**
318
- * @param {string} message - 错误消息
319
- * @param {any} originalError - 原始错误对象或相关信息
320
- * @param {string} responseText - 原始响应文本(如果有)
321
- **/
322
- constructor(message, originalError, responseText) {
323
- super(message);
324
- this.name = "ParsingError";
325
- this.originalError = originalError;
326
- this.responseText = responseText;
327
- }
328
- };
329
- //#endregion
330
339
  //#region src/core/Pipeline.js
331
340
  /**
332
341
  * Pipeline —— 极简顺序管道(v3.0 提案 · 阶段 1)
@@ -403,26 +412,37 @@ var SESSION_CONFIG_KEYS = [
403
412
  "maxTokens",
404
413
  "modelParams",
405
414
  "system",
406
- "retry",
407
- "ephemeralContinue"
415
+ "retry"
416
+ ];
417
+ var INTERNAL_STAGES = [
418
+ "prepareInput",
419
+ "beforeSend",
420
+ "autoContinue",
421
+ "buildRequest",
422
+ "send"
408
423
  ];
409
424
  var ChatService = class extends EventEmitter {
410
425
  constructor(config = {}) {
411
426
  super();
412
427
  this.config = { ...config };
413
- this.messages = new MessageStore();
428
+ if (config.store) {
429
+ if (typeof config.store.getAll !== "function" || typeof config.store.add !== "function") throw new ValidationError("store 必须实现 MessageStore 的接口(至少 getAll / add / update)");
430
+ this.messages = config.store;
431
+ } else this.messages = new MessageStore();
414
432
  this.systemPrompts = new SystemPromptStore();
415
433
  this._adapter = null;
416
- this._abortController = null;
417
- this._isGenerating = false;
418
- this._hooks = new EventEmitter();
419
- this._processResponse = null;
434
+ this._activeRequests = /* @__PURE__ */ new Map();
435
+ this._requestSeq = 0;
420
436
  const model = this.config.model || this.config.modelParams?.model;
421
437
  if (!model || typeof model !== "string" || !model.trim()) throw new ConfigurationError("缺少 model 配置");
422
438
  this._assertValidConfig(this.config);
423
439
  if (typeof config.system === "string" && config.system.trim()) this.systemPrompts.set(config.system);
424
440
  if (config.adapter) this.setAdapter(config.adapter);
425
441
  this._userStages = [];
442
+ this._stageOverrides = /* @__PURE__ */ new Map();
443
+ this._disabledStages = /* @__PURE__ */ new Set();
444
+ this._extraConfigKeys = /* @__PURE__ */ new Map();
445
+ this.plugins = {};
426
446
  this._pipeline = this._buildPipeline();
427
447
  }
428
448
  /**
@@ -437,8 +457,16 @@ var ChatService = class extends EventEmitter {
437
457
  const model = cfg.model ?? cfg.modelParams?.model;
438
458
  if (model !== void 0 && (typeof model !== "string" || !model.trim())) throw new ConfigurationError("model 必须是非空字符串");
439
459
  }
460
+ /**
461
+ * 安装插件。
462
+ * - 插件实现 `install(chat, options)`
463
+ * - 若 install 返回一个对象,会挂到 **`chat.plugins[plugin.name]`**(v4.0 命名空间,插件之间不会撞名)
464
+ * - 插件要提供卸载时,自己在返回值里带 `uninstall()`(或调用 `plugin.uninstall(chat)`)
465
+ * @returns {ChatService} this
466
+ */
440
467
  use(plugin, options = {}) {
441
- plugin.install(this, options);
468
+ const api = plugin.install(this, options);
469
+ if (plugin.name && api && typeof api === "object") this.plugins[plugin.name] = api;
442
470
  return this;
443
471
  }
444
472
  /**
@@ -453,13 +481,7 @@ var ChatService = class extends EventEmitter {
453
481
  pipe(stage) {
454
482
  if (!stage || typeof stage !== "object" || typeof stage.run !== "function") throw new ConfigurationError("pipe: 需要 { name, phase?, run(ctx) },run 必须是函数");
455
483
  if (typeof stage.name !== "string" || !stage.name.trim()) throw new ConfigurationError("pipe: 需要 name(车间名,非空字符串)");
456
- if ([
457
- "prepareInput",
458
- "beforeSend",
459
- "autoContinue",
460
- "buildRequest",
461
- "send"
462
- ].includes(stage.name)) throw new ConfigurationError(`pipe: "${stage.name}" 是内部车间名,请换一个名字`);
484
+ if (INTERNAL_STAGES.includes(stage.name)) throw new ConfigurationError(`pipe: "${stage.name}" 是内部车间名,请换一个名字`);
463
485
  if (this._userStages.some((s) => s.name === stage.name)) throw new ConfigurationError(`pipe: 车间 "${stage.name}" 已存在,请先 chat.unpipe("${stage.name}")`);
464
486
  const phase = stage.phase === "afterSend" ? "afterSend" : "beforeSend";
465
487
  this._userStages.push({
@@ -481,37 +503,86 @@ var ChatService = class extends EventEmitter {
481
503
  get pipelineStages() {
482
504
  return this._pipeline.names();
483
505
  }
484
- /** 重建内部管道:内部 5 车间 + 用户车间按 phase 插入 */
506
+ /** 内部步骤名(顺序即执行顺序):prepareInput / beforeSend / autoContinue / buildRequest / send */
507
+ get internalStages() {
508
+ return [...INTERNAL_STAGES];
509
+ }
510
+ /** 内部步骤的默认实现表 */
511
+ _defaultStageRuns() {
512
+ return {
513
+ prepareInput: (ctx) => this._stagePrepareInput(ctx),
514
+ beforeSend: (ctx) => this._stageBeforeSend(ctx),
515
+ autoContinue: (ctx) => this._stageAutoContinue(ctx),
516
+ buildRequest: (ctx) => this._stageBuildRequest(ctx),
517
+ send: (ctx) => this._stageSend(ctx)
518
+ };
519
+ }
520
+ /**
521
+ * 用你自己的实现**顶替**某个内部步骤。框架不再执行该步的默认行为。
522
+ * 例:`chat.replaceStage('send', async (ctx) => { ctx.result = await 我的发送(ctx.body) })`
523
+ * 想"包一层"默认实现:先 `const base = chat.getStage('send')`,再在自己的函数里调用它。
524
+ * @param {string} name 步骤名(见 internalStages)
525
+ * @param {Function} run (ctx) => void | Promise<void>
526
+ * @throws {ConfigurationError} 步骤名未知 / run 不是函数
527
+ */
528
+ replaceStage(name, run) {
529
+ if (!INTERNAL_STAGES.includes(name)) throw new ConfigurationError(`replaceStage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(", ")}`);
530
+ if (typeof run !== "function") throw new ConfigurationError("replaceStage: 第二个参数必须是函数 (ctx) => void | Promise<void>");
531
+ this._stageOverrides.set(name, run);
532
+ this._rebuildPipeline();
533
+ return this;
534
+ }
535
+ /** 撤销替换,恢复该步骤的默认实现(不影响 unstage 的关闭状态) */
536
+ restoreStage(name) {
537
+ if (!INTERNAL_STAGES.includes(name)) throw new ConfigurationError(`restoreStage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(", ")}`);
538
+ this._stageOverrides.delete(name);
539
+ this._rebuildPipeline();
540
+ return this;
541
+ }
542
+ /**
543
+ * **关闭**某个内部步骤(从管道里移除,不再执行)。
544
+ * 例:`chat.unstage('autoContinue')` —— 彻底不要"自动续写检测"这一步。
545
+ * 警告:关闭 prepareInput / buildRequest / send 会让流程失去必要产物(如 ctx.body)。
546
+ */
547
+ unstage(name) {
548
+ if (!INTERNAL_STAGES.includes(name)) throw new ConfigurationError(`unstage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(", ")}`);
549
+ this._disabledStages.add(name);
550
+ this._rebuildPipeline();
551
+ return this;
552
+ }
553
+ /** 重新启用被 unstage 关闭的步骤 */
554
+ restage(name) {
555
+ if (!INTERNAL_STAGES.includes(name)) throw new ConfigurationError(`restage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(", ")}`);
556
+ this._disabledStages.delete(name);
557
+ this._rebuildPipeline();
558
+ return this;
559
+ }
560
+ /** 取得某个内部步骤的默认实现(用于"包一层":做自己的事,再调用它) */
561
+ getStage(name) {
562
+ if (!INTERNAL_STAGES.includes(name)) throw new ConfigurationError(`getStage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(", ")}`);
563
+ return this._defaultStageRuns()[name];
564
+ }
565
+ /**
566
+ * 重建内部管道:内部步骤(可被 replaceStage 顶替 / 被 unstage 关闭)+ 用户车间按 phase 插入
567
+ * 顺序:prepareInput → beforeSend → [用户 beforeSend 车间] → autoContinue → buildRequest → send → [用户 afterSend 车间]
568
+ */
485
569
  _buildPipeline() {
486
570
  const p = new Pipeline();
487
- p.register({
488
- name: "prepareInput",
489
- run: (ctx) => this._stagePrepareInput(ctx)
490
- });
491
- p.register({
492
- name: "beforeSend",
493
- run: (ctx) => this._stageBeforeSend(ctx)
494
- });
495
- for (const s of this._userStages) if (s.phase === "beforeSend") p.register({
496
- name: s.name,
497
- run: (ctx) => s.run(ctx)
498
- });
499
- p.register({
500
- name: "autoContinue",
501
- run: (ctx) => this._stageAutoContinue(ctx)
502
- });
503
- p.register({
504
- name: "buildRequest",
505
- run: (ctx) => this._stageBuildRequest(ctx)
506
- });
507
- p.register({
508
- name: "send",
509
- run: (ctx) => this._stageSend(ctx)
510
- });
511
- for (const s of this._userStages) if (s.phase === "afterSend") p.register({
512
- name: s.name,
513
- run: (ctx) => s.run(ctx)
514
- });
571
+ const defaults = this._defaultStageRuns();
572
+ const insertUsers = (phase) => {
573
+ for (const s of this._userStages) if (s.phase === phase) p.register({
574
+ name: s.name,
575
+ run: (ctx) => s.run(ctx)
576
+ });
577
+ };
578
+ for (const name of INTERNAL_STAGES) {
579
+ if (!this._disabledStages.has(name)) p.register({
580
+ name,
581
+ run: this._stageOverrides.get(name) || defaults[name]
582
+ });
583
+ if (name === "beforeSend") insertUsers("beforeSend");
584
+ if (name === "send") insertUsers("afterSend");
585
+ }
515
586
  return p;
516
587
  }
517
588
  _rebuildPipeline() {
@@ -521,98 +592,81 @@ var ChatService = class extends EventEmitter {
521
592
  assertAdapter(adapter);
522
593
  this._adapter = adapter;
523
594
  }
524
- abort() {
525
- if (this._abortController) this._abortController.abort();
595
+ /**
596
+ * 中断请求(v4.0:按请求隔离)。
597
+ * - 不传参数:中断**所有**活跃请求(旧版只会中断"最后一个",多请求时会中断错的那个)
598
+ * - 传 requestId:只中断那一个(id 见 `chat.activeRequests`)
599
+ */
600
+ abort(requestId) {
601
+ if (requestId) {
602
+ const controller = this._activeRequests.get(requestId);
603
+ if (controller) controller.abort();
604
+ return;
605
+ }
606
+ for (const controller of this._activeRequests.values()) controller.abort();
526
607
  }
608
+ /** 是否有请求正在生成中 */
527
609
  get isGenerating() {
528
- return this._isGenerating;
610
+ return this._activeRequests.size > 0;
611
+ }
612
+ /** 当前活跃请求的 id 列表(调试 / 精确中断用) */
613
+ get activeRequests() {
614
+ return [...this._activeRequests.keys()];
615
+ }
616
+ /** 内部:登记一个请求,返回 { id, signal } */
617
+ _beginRequest() {
618
+ const id = "req_" + ++this._requestSeq;
619
+ const controller = new AbortController();
620
+ this._activeRequests.set(id, controller);
621
+ return {
622
+ id,
623
+ signal: controller.signal
624
+ };
529
625
  }
530
- async continueLast() {
531
- const target = this._prepareContinue();
532
- this.messages.update(target.id, { prefix: true });
533
- try {
534
- await this._request({
535
- addUser: false,
536
- isStream: false,
537
- mergeToEntry: target.id
538
- });
539
- this.messages.update(target.id, {
540
- _complete: true,
541
- prefix: void 0,
542
- _ephemeral: false
543
- });
544
- return target;
545
- } catch (err) {
546
- this.messages.update(target.id, {
547
- _complete: true,
548
- prefix: void 0,
549
- _ephemeral: false
550
- });
551
- if (err.name === "AbortError") {
552
- this.emit("aborted", { timestamp: Date.now() });
553
- return target;
554
- }
555
- throw err;
556
- }
626
+ /** 内部:注销一个请求 */
627
+ _endRequest(id) {
628
+ this._activeRequests.delete(id);
557
629
  }
558
- async continueLastStream(onProgress, onDone) {
559
- const target = this._prepareContinue();
560
- this.messages.update(target.id, { prefix: true });
561
- const baseLen = (target.content || "").length;
562
- try {
563
- await this._request({
564
- addUser: false,
565
- isStream: true,
566
- mergeToEntry: target.id,
567
- onProgress: (chunk) => {
568
- if (onProgress) onProgress({
569
- ...chunk,
570
- content: (chunk.content || "").slice(baseLen)
571
- });
572
- },
573
- onDone: (final) => {
574
- if (onDone) onDone(final);
575
- }
576
- });
577
- this.messages.update(target.id, {
578
- _complete: true,
579
- prefix: void 0,
580
- _ephemeral: false
581
- });
582
- return target;
583
- } catch (err) {
584
- this.messages.update(target.id, {
585
- _complete: true,
586
- prefix: void 0,
587
- _ephemeral: false
588
- });
589
- if (err.name === "AbortError") {
590
- this.emit("aborted", { timestamp: Date.now() });
591
- return target;
592
- }
593
- throw err;
630
+ /**
631
+ * 注册"运行时可改的配置字段"(v4.0)。
632
+ * 让插件自带配置(如 continuation 的 autoContinue)不必回头改核心白名单。
633
+ * @param {string[]|Object<string, Function>} keys 字段名数组,或 { 字段名: 校验器(value)=>boolean }
634
+ * @returns {ChatService} this
635
+ * @throws {ConfigurationError} 字段名非法 / 校验器不是函数
636
+ */
637
+ registerConfigKeys(keys) {
638
+ const entries = Array.isArray(keys) ? keys.map((k) => [k, null]) : Object.entries(keys || {});
639
+ for (const [key, validator] of entries) {
640
+ if (typeof key !== "string" || !key.trim()) throw new ConfigurationError("registerConfigKeys: 字段名必须是非空字符串");
641
+ if (validator != null && typeof validator !== "function") throw new ConfigurationError(`registerConfigKeys: "${key}" 的校验器必须是函数`);
642
+ this._extraConfigKeys.set(key.trim(), validator || null);
594
643
  }
644
+ return this;
595
645
  }
596
- _prepareContinue() {
597
- const msgs = this.messages.getAll();
598
- const last = msgs[msgs.length - 1];
599
- if (last && (last.role === "assistant" || last._ephemeral)) return last;
600
- throw new Error("最后一条消息不是 assistant,无法续写");
646
+ /** 注销运行时可改字段(插件卸载时调用)@returns {ChatService} this */
647
+ unregisterConfigKeys(keys) {
648
+ for (const k of Array.isArray(keys) ? keys : [keys]) this._extraConfigKeys.delete(k);
649
+ return this;
650
+ }
651
+ /** 当前允许在运行时修改的配置字段(内置基础字段 + 插件注册) */
652
+ get configKeys() {
653
+ return [...SESSION_CONFIG_KEYS, ...this._extraConfigKeys.keys()];
601
654
  }
602
655
  /**
603
- * 运行时修改配置(仅会话层字段)。
656
+ * 运行时修改配置(仅"可改字段":内置基础字段 + 插件注册字段)。
604
657
  *
605
- * 白名单:model / temperature / maxTokens / modelParams / system / retry / ephemeralContinue。
606
658
  * - 运输层(apiKey/baseUrl/headers):归适配器,要改就换适配器实例
607
659
  * - 请求参数默认值:走适配器工厂 options 或请求级覆盖(chat.send(x, params))
608
- * - 能力表:归 modelRegistry 插件
660
+ * - 插件自有配置:插件用 `registerConfigKeys` 登记后才可改(工厂 options 仍是首选)
609
661
  *
610
662
  * 与构造器一样经过校验,不能注入非法值。
611
- * @throws {ConfigurationError} 非白名单字段或非法值
663
+ * @throws {ConfigurationError} 非白名单字段、非法值、或插件校验器拒绝
612
664
  */
613
665
  updateConfig(partial) {
614
- for (const key of Object.keys(partial)) if (!SESSION_CONFIG_KEYS.includes(key)) throw new ConfigurationError(`updateConfig 不支持修改 "${key}"。允许的会话层字段: ${SESSION_CONFIG_KEYS.join(", ")}`);
666
+ const allowed = this.configKeys;
667
+ for (const key of Object.keys(partial)) if (!allowed.includes(key)) throw new ConfigurationError(`updateConfig 不支持修改 "${key}"。允许的字段: ${allowed.join(", ")}`);
615
668
  this._assertValidConfig(partial);
669
+ 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])}`);
616
670
  Object.assign(this.config, partial);
617
671
  if (typeof partial.system === "string") this.systemPrompts.set(partial.system);
618
672
  this.emit("config-updated", {
@@ -678,6 +732,7 @@ var ChatService = class extends EventEmitter {
678
732
  onProgress: options.onProgress || null,
679
733
  onDone: options.onDone || null,
680
734
  mergeToEntry: options.mergeToEntry || null,
735
+ signal: options.signal || null,
681
736
  body: null,
682
737
  result: null
683
738
  };
@@ -703,22 +758,19 @@ var ChatService = class extends EventEmitter {
703
758
  timestamp: Date.now()
704
759
  });
705
760
  }
706
- /** 车间 2:发送前钩子(兼容桥:事件钩子照发;阶段 4 会迁到 beforeSend 公开位置) */
707
- async _stageBeforeSend(ctx) {
708
- await this._hooks.emitAsync("beforeRequest", {
709
- messages: ctx.messages,
710
- config: ctx.config,
711
- options: ctx.options
712
- });
713
- }
714
- /** 车间 3:自动续写检测(ephemeralContinue 时,底部若有 prefix 消息则续写) */
715
- async _stageAutoContinue(ctx) {
716
- if (this.config.ephemeralContinue && !ctx.mergeToEntry) {
717
- const msgs = ctx.messages.getAll();
718
- const last = msgs[msgs.length - 1];
719
- if (last && last.prefix && (last.role === "assistant" || last._ephemeral)) ctx.mergeToEntry = last.id;
720
- }
721
- }
761
+ /**
762
+ * 车间 2:发送前 —— **v4.0 起默认什么都不做**。
763
+ * 需要在这里做事的(注入、改写消息/配置):`chat.replaceStage('beforeSend', fn)`
764
+ * 或挂一个 `chat.pipe({ phase: 'beforeSend' })` 车间(在它之后执行)。
765
+ * 旧的 `_hooks.on('beforeRequest')` 兼容桥已于 v4.0 移除。
766
+ */
767
+ async _stageBeforeSend(ctx) {}
768
+ /**
769
+ * 车间 3:自动续写检测 —— **v4.0 起默认什么都不做**。
770
+ * 续写是玩法:需要它的装上 continuation 插件,由插件 replaceStage('autoContinue', ...) 接管。
771
+ * 步骤名与位置保留,方便插件替换、也方便使用者 `unstage('autoContinue')` 明确移除。
772
+ */
773
+ async _stageAutoContinue(ctx) {}
722
774
  /** 车间 4:构建请求体 */
723
775
  async _stageBuildRequest(ctx) {
724
776
  ctx.body = this._adapter.buildRequest(ctx.messages.getAll(), ctx.config, ctx.systemPrompts.getEnabled());
@@ -733,7 +785,8 @@ var ChatService = class extends EventEmitter {
733
785
  mergeToEntry: ctx.mergeToEntry,
734
786
  config: ctx.config,
735
787
  maxRetries: retryCfg.maxRetries ?? 0,
736
- retryDelay: retryCfg.retryDelay ?? 1e3
788
+ retryDelay: retryCfg.retryDelay ?? 1e3,
789
+ signal: ctx.signal
737
790
  });
738
791
  }
739
792
  _addUserMessage(userInput, addUser) {
@@ -749,9 +802,9 @@ var ChatService = class extends EventEmitter {
749
802
  }
750
803
  }
751
804
  /** retry 循环,占位消息只 push 一次 */
752
- async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, config, maxRetries, retryDelay }) {
805
+ async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, config, maxRetries, retryDelay, signal }) {
753
806
  let lastError = null;
754
- const adapterOptions = { signal: this._abortController?.signal };
807
+ const adapterOptions = { signal };
755
808
  let placeholder = null;
756
809
  let base = null;
757
810
  if (isStream) {
@@ -779,7 +832,6 @@ var ChatService = class extends EventEmitter {
779
832
  await new Promise((r) => setTimeout(r, retryDelay));
780
833
  }
781
834
  try {
782
- this._isGenerating = true;
783
835
  if (isStream) return await this._stream(body, adapterOptions, {
784
836
  placeholder,
785
837
  base,
@@ -790,22 +842,15 @@ var ChatService = class extends EventEmitter {
790
842
  });
791
843
  else {
792
844
  const resp = await this._adapter.send(body, config, adapterOptions);
793
- let result = this._handleResult(this._adapter.parseResponse(resp), mergeToEntry);
794
- if (this._processResponse && !mergeToEntry) result = await this._processResponse(result, { isStream: false });
795
- return result;
845
+ return this._handleResult(this._adapter.parseResponse(resp), mergeToEntry);
796
846
  }
797
847
  } catch (error) {
798
848
  lastError = error;
799
- if (error.name === "AbortError" || this._abortController?.signal.aborted) {
800
- this._isGenerating = false;
801
- throw error;
802
- }
849
+ if (error.name === "AbortError" || signal?.aborted) throw error;
803
850
  if (error instanceof NetworkError && attempt < maxRetries) continue;
804
- this._isGenerating = false;
805
851
  throw error;
806
852
  }
807
853
  }
808
- this._isGenerating = false;
809
854
  throw lastError;
810
855
  }
811
856
  /**
@@ -820,19 +865,14 @@ var ChatService = class extends EventEmitter {
820
865
  */
821
866
  async _stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone, config }) {
822
867
  let finalMsg = null;
823
- await this._adapter.stream(body, config, (snap) => {
868
+ await this._adapter.stream(body, config, (snap, delta) => {
824
869
  placeholder.content = mergeToEntry && base ? base.content + (snap.content || "") : snap.content || "";
825
870
  if (snap.reasoningContent) placeholder.reasoningContent = mergeToEntry && base ? base.reasoning + snap.reasoningContent : snap.reasoningContent;
826
871
  if (snap.toolCalls) placeholder.toolCalls = [...snap.toolCalls];
827
872
  this.emit("stream-progress", snap);
828
- if (onProgress) onProgress(snap);
873
+ if (onProgress) onProgress(snap, delta);
829
874
  }, async (final) => {
830
875
  finalMsg = final;
831
- if (this._processResponse && !mergeToEntry) finalMsg = await this._processResponse(finalMsg, {
832
- isStream: true,
833
- onProgress,
834
- onDone
835
- });
836
876
  if (mergeToEntry && base) {
837
877
  const changes = {
838
878
  content: base.content + (final.content || ""),
@@ -886,13 +926,14 @@ var ChatService = class extends EventEmitter {
886
926
  * @param {Object} [params] - 请求级参数覆盖(model/temperature/modelParams 等,仅本次生效)
887
927
  */
888
928
  async send(userInput, params) {
889
- this._abortController = new AbortController();
929
+ const { id, signal } = this._beginRequest();
890
930
  try {
891
931
  return await this._request({
892
932
  userInput,
893
933
  addUser: true,
894
934
  isStream: false,
895
- params
935
+ params,
936
+ signal
896
937
  });
897
938
  } catch (err) {
898
939
  if (err.name === "AbortError") {
@@ -901,8 +942,7 @@ var ChatService = class extends EventEmitter {
901
942
  }
902
943
  throw err;
903
944
  } finally {
904
- this._abortController = null;
905
- this._isGenerating = false;
945
+ this._endRequest(id);
906
946
  }
907
947
  }
908
948
  /**
@@ -918,7 +958,7 @@ var ChatService = class extends EventEmitter {
918
958
  onProgress = params;
919
959
  params = void 0;
920
960
  }
921
- this._abortController = new AbortController();
961
+ const { id, signal } = this._beginRequest();
922
962
  try {
923
963
  return await this._request({
924
964
  userInput,
@@ -926,7 +966,8 @@ var ChatService = class extends EventEmitter {
926
966
  isStream: true,
927
967
  onProgress,
928
968
  onDone,
929
- params
969
+ params,
970
+ signal
930
971
  });
931
972
  } catch (err) {
932
973
  if (err.name === "AbortError") {
@@ -935,21 +976,24 @@ var ChatService = class extends EventEmitter {
935
976
  }
936
977
  throw err;
937
978
  } finally {
938
- this._abortController = null;
939
- this._isGenerating = false;
979
+ this._endRequest(id);
940
980
  }
941
981
  }
942
982
  /**
943
983
  * 重发当前消息(不加用户消息)。
944
984
  * @param {Object} [params] - 请求级参数覆盖(仅本次生效)
985
+ * @param {Object} [options] - { mergeToEntry } 可选:把结果合并进指定的已有消息(续写等场景)
945
986
  */
946
- async sendExisting(params) {
947
- this._abortController = new AbortController();
987
+ async sendExisting(params, options = {}) {
988
+ const mergeToEntry = options?.mergeToEntry || null;
989
+ const { id, signal } = this._beginRequest();
948
990
  try {
949
991
  return await this._request({
950
992
  addUser: false,
951
993
  isStream: false,
952
- params
994
+ params,
995
+ mergeToEntry,
996
+ signal
953
997
  });
954
998
  } catch (err) {
955
999
  if (err.name === "AbortError") {
@@ -958,30 +1002,38 @@ var ChatService = class extends EventEmitter {
958
1002
  }
959
1003
  throw err;
960
1004
  } finally {
961
- this._abortController = null;
962
- this._isGenerating = false;
1005
+ this._endRequest(id);
963
1006
  }
964
1007
  }
965
1008
  /**
966
1009
  * 流式重发(不加用户消息)。
967
1010
  * @param {Object} [params] - 请求级参数覆盖(仅本次生效)
1011
+ * @param {Object} [options] - { mergeToEntry } 可选:把结果合并进指定的已有消息
968
1012
  * @param {Function} [onProgress] - 流式进度回调
969
1013
  * @param {Function} [onDone] - 流式完成回调
970
1014
  */
971
- async sendExistingStream(params, onProgress, onDone) {
1015
+ async sendExistingStream(params, options, onProgress, onDone) {
972
1016
  if (typeof params === "function") {
973
- onDone = onProgress;
1017
+ onDone = options;
974
1018
  onProgress = params;
1019
+ options = {};
975
1020
  params = void 0;
1021
+ } else if (typeof options === "function") {
1022
+ onDone = onProgress;
1023
+ onProgress = options;
1024
+ options = {};
976
1025
  }
977
- this._abortController = new AbortController();
1026
+ const mergeToEntry = options?.mergeToEntry || null;
1027
+ const { id, signal } = this._beginRequest();
978
1028
  try {
979
1029
  return await this._request({
980
1030
  addUser: false,
981
1031
  isStream: true,
982
1032
  onProgress,
983
1033
  onDone,
984
- params
1034
+ params,
1035
+ mergeToEntry,
1036
+ signal
985
1037
  });
986
1038
  } catch (err) {
987
1039
  if (err.name === "AbortError") {
@@ -990,8 +1042,7 @@ var ChatService = class extends EventEmitter {
990
1042
  }
991
1043
  throw err;
992
1044
  } finally {
993
- this._abortController = null;
994
- this._isGenerating = false;
1045
+ this._endRequest(id);
995
1046
  }
996
1047
  }
997
1048
  };
@@ -1042,26 +1093,23 @@ function isString(value) {
1042
1093
  //#endregion
1043
1094
  //#region src/utils/MessageFormatter.js
1044
1095
  /**
1045
- * MessageFormatter — 可注册的消息格式转换器
1096
+ * MessageFormatter — 可注册的消息格式转换器 + 可注册的过滤器链(v4.0)
1046
1097
  *
1047
1098
  * 职责:
1048
- * 1. 内置 OpenA I兼容格式的转换逻辑
1099
+ * 1. 内置 OpenAI 兼容格式的转换逻辑
1049
1100
  * 2. 支持 register(name, fn) 注册自定义格式(如 Anthropic、Gemini 等)
1050
- * 3. 统一处理 capabilities 过滤(reasoning、vision 等)
1051
- * 4. 所有适配器通过此工具获取 API 消息数组,消除重复代码
1101
+ * 3. v4.0:消息"保留 / 丢弃"规则改为可注册的过滤器链(registerFilter)
1102
+ * —— 原来硬编码在格式里的 ephemeral / system / 空内容规则,现在都是过滤器;
1103
+ * 业务插件(如 continuation)可以同名覆盖内置过滤器,接管规则。
1052
1104
  *
1053
1105
  * 用法:
1054
1106
  * import { MessageFormatter } from 'my-ai-chat-framework';
1055
1107
  *
1056
1108
  * // 使用内置格式
1057
- * const msgs = MessageFormatter.format({
1058
- * messages, systemPrompts, capabilities, resolveImage
1059
- * }); // 默认 'openai'
1109
+ * const msgs = MessageFormatter.format({ messages, systemPrompts, capabilities, resolveImage });
1060
1110
  *
1061
- * // 注册自定义格式
1062
- * MessageFormatter.register('anthropic', ({ messages, systemPrompts, capabilities }) => {
1063
- * // 返回 Anthropic 格式的消息数组
1064
- * });
1111
+ * // 注册自己的过滤器(决定哪些消息进入请求)
1112
+ * MessageFormatter.registerFilter('my-rule', (msg, ctx) => msg.role === 'system' ? null : msg);
1065
1113
  */
1066
1114
  var IS_URL = /^https?:\/\//i;
1067
1115
  function defaultResolveImage(imageId) {
@@ -1090,7 +1138,23 @@ function buildMultimodalContent(textContent, images, resolveImage) {
1090
1138
  }
1091
1139
  return content;
1092
1140
  }
1093
- /** OpenAI 兼容格式 */
1141
+ /** system 角色消息不进对话数组(system 由 systemPrompts 统一置顶) */
1142
+ function filterSystemDrop(msg) {
1143
+ if (msg.role === "system" && !msg._ephemeral) return null;
1144
+ return msg;
1145
+ }
1146
+ /** 空内容清理:无文本、无图片的消息丢弃(tool、带 toolCalls 的 assistant、prefix 空消息例外) */
1147
+ function filterEmptyDrop(msg) {
1148
+ if (msg.role === "system" || msg.role === "tool") return msg;
1149
+ if (msg.role === "assistant" && msg.toolCalls?.length) return msg;
1150
+ const hasText = msg.content && isString(msg.content) && msg.content.trim();
1151
+ const hasImages = Array.isArray(msg.images) && msg.images.length > 0;
1152
+ if (!hasText && !hasImages) {
1153
+ if (msg.role === "assistant" && msg.prefix) return msg;
1154
+ return null;
1155
+ }
1156
+ return msg;
1157
+ }
1094
1158
  function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
1095
1159
  const rImg = typeof resolveImage === "function" ? resolveImage : defaultResolveImage;
1096
1160
  const result = [];
@@ -1098,15 +1162,8 @@ function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
1098
1162
  role: "system",
1099
1163
  content: sp.content.trim()
1100
1164
  });
1101
- let ephemEnd = messages.length;
1102
- for (let i = messages.length - 1; i >= 0; i--) if (messages[i]._ephemeral) ephemEnd = i;
1103
- else break;
1104
- for (let i = 0; i < messages.length; i++) {
1105
- const msg = messages[i];
1106
- if (msg._ephemeral) {
1107
- if (i < ephemEnd) continue;
1108
- }
1109
- if (msg.role === "system" && !msg._ephemeral) continue;
1165
+ const kept = MessageFormatter._runFilters(messages, capabilities || {});
1166
+ for (const msg of kept) {
1110
1167
  if (msg.role === "tool") {
1111
1168
  result.push({
1112
1169
  role: "tool",
@@ -1142,7 +1199,7 @@ function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
1142
1199
  }
1143
1200
  const entry = { role: msg.role };
1144
1201
  if (hasImages) {
1145
- if (!capabilities?.vision) throw new Error("[MessageFormatter] 消息包含图片但模型不支持视觉(capabilities.vision=false)。请切换模型或移除图片。");
1202
+ if (!capabilities?.vision) throw new ValidationError("[MessageFormatter] 消息包含图片但模型不支持视觉(capabilities.vision=false)。请切换模型或移除图片。");
1146
1203
  entry.content = buildMultimodalContent(msg.content || "", msg.images, rImg);
1147
1204
  } else entry.content = msg.content;
1148
1205
  if (msg.prefix) entry.prefix = true;
@@ -1152,22 +1209,71 @@ function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
1152
1209
  }
1153
1210
  var MessageFormatter = {
1154
1211
  _formats: new Map([["openai", toOpenAI]]),
1212
+ _filters: /* @__PURE__ */ new Map(),
1213
+ _filterOrder: 0,
1155
1214
  register(name, fn) {
1156
- if (!name || typeof name !== "string" || !name.trim()) throw new Error("[MessageFormatter] 格式名称必须是非空字符串");
1157
- if (typeof fn !== "function") throw new Error("[MessageFormatter] 转换函数必须是 function");
1215
+ if (!name || typeof name !== "string" || !name.trim()) throw new ValidationError("[MessageFormatter] 格式名称必须是非空字符串");
1216
+ if (typeof fn !== "function") throw new ValidationError("[MessageFormatter] 转换函数必须是 function");
1158
1217
  this._formats.set(name.trim(), fn);
1159
1218
  },
1160
1219
  unregister(name) {
1161
- if (name === "openai") throw new Error("[MessageFormatter] 内置格式 \"openai\" 不可移除");
1220
+ if (name === "openai") throw new ValidationError("[MessageFormatter] 内置格式 \"openai\" 不可移除");
1162
1221
  this._formats.delete(name);
1163
1222
  },
1164
1223
  listFormats() {
1165
1224
  return [...this._formats.keys()];
1166
1225
  },
1226
+ registerFilter(name, fn, options = {}) {
1227
+ if (!name || typeof name !== "string" || !name.trim()) throw new ValidationError("[MessageFormatter] 过滤器名称必须是非空字符串");
1228
+ if (typeof fn !== "function") throw new ValidationError("[MessageFormatter] 过滤器必须是 function");
1229
+ const key = name.trim();
1230
+ const priority = typeof options.priority === "number" ? options.priority : 100;
1231
+ const prev = this._filters.get(key);
1232
+ this._filters.set(key, {
1233
+ name: key,
1234
+ fn,
1235
+ priority,
1236
+ order: ++this._filterOrder
1237
+ });
1238
+ return prev ? prev.fn : void 0;
1239
+ },
1240
+ unregisterFilter(name) {
1241
+ return this._filters.delete(name);
1242
+ },
1243
+ listFilters() {
1244
+ return [...this._filters.values()].sort((a, b) => a.priority - b.priority || a.order - b.order).map((f) => ({
1245
+ name: f.name,
1246
+ priority: f.priority
1247
+ }));
1248
+ },
1249
+ _runFilters(messages, capabilities = {}) {
1250
+ if (this._filters.size === 0) return [...messages];
1251
+ const list = [...this._filters.values()].sort((a, b) => a.priority - b.priority || a.order - b.order);
1252
+ const out = [];
1253
+ for (let i = 0; i < messages.length; i++) {
1254
+ let msg = messages[i];
1255
+ const ctx = {
1256
+ index: i,
1257
+ total: messages.length,
1258
+ messages,
1259
+ capabilities
1260
+ };
1261
+ let dropped = false;
1262
+ for (const f of list) {
1263
+ msg = f.fn(msg, ctx);
1264
+ if (msg === null || msg === void 0) {
1265
+ dropped = true;
1266
+ break;
1267
+ }
1268
+ }
1269
+ if (!dropped) out.push(msg);
1270
+ }
1271
+ return out;
1272
+ },
1167
1273
  format(options = {}) {
1168
1274
  const formatName = options.format || "openai";
1169
1275
  const fn = this._formats.get(formatName);
1170
- if (!fn) throw new Error(`[MessageFormatter] 未知格式 "${formatName}"。可用格式: ${[...this._formats.keys()].join(", ")}`);
1276
+ if (!fn) throw new ValidationError(`[MessageFormatter] 未知格式 "${formatName}"。可用格式: ${[...this._formats.keys()].join(", ")}`);
1171
1277
  return fn({
1172
1278
  messages: options.messages || [],
1173
1279
  systemPrompts: options.systemPrompts || [],
@@ -1176,6 +1282,8 @@ var MessageFormatter = {
1176
1282
  });
1177
1283
  }
1178
1284
  };
1285
+ MessageFormatter.registerFilter("system-drop", filterSystemDrop, { priority: 900 });
1286
+ MessageFormatter.registerFilter("empty-drop", filterEmptyDrop, { priority: 910 });
1179
1287
  function assembleMessages(options = {}) {
1180
1288
  return MessageFormatter.format({
1181
1289
  ...options,
@@ -1225,10 +1333,10 @@ var openaiAdapter = {
1225
1333
  buildRequest(messages, config, systemPrompts = []) {
1226
1334
  config = this._resolveConfig(config);
1227
1335
  const model = config.model || config.modelParams?.model;
1228
- if (!model) throw new Error("Missing required config: model (either at top level or in modelParams)");
1336
+ if (!model) throw new ConfigurationError("Missing required config: model (either at top level or in modelParams)");
1229
1337
  const mp = config.modelParams || {};
1230
- const temperature = mp.temperature ?? config.temperature ?? .7;
1231
- const maxTokens = mp.maxTokens ?? config.maxTokens ?? 2e3;
1338
+ const temperature = mp.temperature ?? config.temperature;
1339
+ const maxTokens = mp.maxTokens ?? config.maxTokens;
1232
1340
  const reasoningEffort = mp.reasoningEffort ?? config.reasoningEffort;
1233
1341
  const requestBody = {
1234
1342
  model,
@@ -1239,10 +1347,10 @@ var openaiAdapter = {
1239
1347
  resolveImage: config.resolveImage,
1240
1348
  format: config.messageFormat
1241
1349
  }),
1242
- temperature,
1243
- max_tokens: maxTokens,
1244
1350
  stream: false
1245
1351
  };
1352
+ if (temperature !== void 0) requestBody.temperature = temperature;
1353
+ if (maxTokens !== void 0) requestBody.max_tokens = maxTokens;
1246
1354
  if (mp.topP !== void 0) requestBody.top_p = mp.topP;
1247
1355
  if (mp.frequencyPenalty !== void 0) requestBody.frequency_penalty = mp.frequencyPenalty;
1248
1356
  if (mp.presencePenalty !== void 0) requestBody.presence_penalty = mp.presencePenalty;
@@ -1349,22 +1457,37 @@ var openaiAdapter = {
1349
1457
  content: "",
1350
1458
  reasoningContent: ""
1351
1459
  };
1460
+ let reachedDone = false;
1352
1461
  while (true) {
1353
1462
  if (options.signal?.aborted) break;
1354
1463
  const { done, value } = await reader.read();
1355
1464
  if (done) break;
1356
1465
  buffer += decoder.decode(value, { stream: true });
1357
- const lines = buffer.split(/\n\n/);
1358
- buffer = lines.pop();
1359
- for (const line of lines) {
1360
- const dataLine = line.replace(/^data: /, "").trim();
1361
- if (!dataLine) continue;
1362
- if (dataLine === "[DONE]") break;
1466
+ let sepMatch;
1467
+ const EVENT_SEP = /\r?\n\r?\n/;
1468
+ while ((sepMatch = EVENT_SEP.exec(buffer)) !== null) {
1469
+ const rawEvent = buffer.slice(0, sepMatch.index);
1470
+ buffer = buffer.slice(sepMatch.index + sepMatch[0].length);
1471
+ const dataPayload = rawEvent.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n");
1472
+ if (!dataPayload) continue;
1473
+ if (dataPayload.trim() === "[DONE]") {
1474
+ reachedDone = true;
1475
+ break;
1476
+ }
1363
1477
  try {
1364
- const delta = JSON.parse(dataLine).choices?.[0]?.delta;
1478
+ const delta = JSON.parse(dataPayload).choices?.[0]?.delta;
1365
1479
  if (!delta) continue;
1366
- if (delta.content) accumulated.content += delta.content;
1480
+ const deltaInfo = {
1481
+ content: "",
1482
+ reasoningContent: "",
1483
+ toolCalls: null
1484
+ };
1485
+ if (delta.content) {
1486
+ accumulated.content += delta.content;
1487
+ deltaInfo.content = delta.content;
1488
+ }
1367
1489
  if (delta.tool_calls) {
1490
+ const newCalls = [];
1368
1491
  if (!accumulated.toolCalls) accumulated.toolCalls = [];
1369
1492
  for (const toolCallDelta of delta.tool_calls) {
1370
1493
  const index = toolCallDelta.index;
@@ -1380,14 +1503,23 @@ var openaiAdapter = {
1380
1503
  if (toolCallDelta.type) accumulated.toolCalls[index].type = toolCallDelta.type;
1381
1504
  if (toolCallDelta.function?.name) accumulated.toolCalls[index].function.name += toolCallDelta.function.name;
1382
1505
  if (toolCallDelta.function?.arguments) accumulated.toolCalls[index].function.arguments += toolCallDelta.function.arguments;
1506
+ newCalls.push({
1507
+ index,
1508
+ delta: toolCallDelta
1509
+ });
1383
1510
  }
1511
+ deltaInfo.toolCalls = newCalls;
1512
+ }
1513
+ if (delta.reasoning_content) {
1514
+ accumulated.reasoningContent += delta.reasoning_content;
1515
+ deltaInfo.reasoningContent = delta.reasoning_content;
1384
1516
  }
1385
- if (delta.reasoning_content) accumulated.reasoningContent += delta.reasoning_content;
1386
- onProgress({ ...accumulated });
1517
+ onProgress({ ...accumulated }, deltaInfo);
1387
1518
  } catch (e) {
1388
- console.warn("stream parsing failed :", e, dataLine);
1519
+ console.warn("stream parsing failed :", e, dataPayload);
1389
1520
  }
1390
1521
  }
1522
+ if (reachedDone) break;
1391
1523
  }
1392
1524
  onDone({
1393
1525
  role: "assistant",
@@ -1449,12 +1581,14 @@ function createOpenAIAdapter(options = {}) {
1449
1581
  var toolCallingPlugin = createToolCallingPlugin();
1450
1582
  /**
1451
1583
  * 创建工具调用插件实例(工厂)。
1452
- * @param {Object} [options] — { timeout, maxIterations }
1584
+ * @param {Object} [options] — { timeout?, maxIterations? }
1585
+ * - 两者**默认都不限制**:不传就是"循环到没有工具调用为止 / 不超时"
1586
+ * - 想限制才传(这是使用者的选择,不是框架替你做的决定)
1453
1587
  */
1454
1588
  function createToolCallingPlugin(options = {}) {
1455
1589
  return {
1456
1590
  name: "tool-calling",
1457
- maxIterations: options.maxIterations || 5,
1591
+ maxIterations: options.maxIterations ?? Infinity,
1458
1592
  _options: { ...options },
1459
1593
  _tools: /* @__PURE__ */ new Map(),
1460
1594
  _toolDefs: [],
@@ -1480,11 +1614,18 @@ function createToolCallingPlugin(options = {}) {
1480
1614
  * @param {Object} parameters - JSON Schema 参数定义(可选,默认为空对象)
1481
1615
  * @returns {ChatService} 返回 chatService 实例,支持链式调用
1482
1616
  */
1483
- chatService.registerTool = (name, description, executor, parameters = {}) => {
1617
+ const registerTool = (name, description, executor, parameters = {}) => {
1484
1618
  this._tools.set(name, {
1485
1619
  executor,
1486
1620
  description
1487
1621
  });
1622
+ const properties = {};
1623
+ const requiredKeys = [];
1624
+ for (const [key, def] of Object.entries(parameters || {})) {
1625
+ const { required: isRequired, ...schema } = def || {};
1626
+ properties[key] = schema;
1627
+ if (isRequired) requiredKeys.push(key);
1628
+ }
1488
1629
  const toolDefinition = {
1489
1630
  type: "function",
1490
1631
  function: {
@@ -1492,8 +1633,8 @@ function createToolCallingPlugin(options = {}) {
1492
1633
  description,
1493
1634
  parameters: {
1494
1635
  type: "object",
1495
- properties: parameters,
1496
- required: Object.keys(parameters).filter((key) => parameters[key]?.required)
1636
+ properties,
1637
+ required: requiredKeys
1497
1638
  }
1498
1639
  }
1499
1640
  };
@@ -1507,6 +1648,18 @@ function createToolCallingPlugin(options = {}) {
1507
1648
  ctx.result = await this._handleWithTools(ctx.result, ctx.isStream, ctx.onProgress || void 0, ctx.onDone || void 0);
1508
1649
  }
1509
1650
  });
1651
+ return {
1652
+ registerTool,
1653
+ tools: this._tools,
1654
+ uninstall: (c) => this.uninstall(c)
1655
+ };
1656
+ },
1657
+ uninstall(chat) {
1658
+ if (!chat) return chat;
1659
+ chat.unpipe?.("tool-calling-inject");
1660
+ chat.unpipe?.("tool-calling-loop");
1661
+ if (chat.plugins && chat.plugins["tool-calling"]) delete chat.plugins["tool-calling"];
1662
+ return chat;
1510
1663
  },
1511
1664
  async _handleWithTools(initialResponse, isStream, onProgress, onDone) {
1512
1665
  const self = this;
@@ -1558,15 +1711,27 @@ function createToolCallingPlugin(options = {}) {
1558
1711
  };
1559
1712
  }
1560
1713
  try {
1561
- const toolTimeout = self._options.timeout ?? chat.config.toolTimeout;
1562
- let execPromise = tool.executor(args);
1563
- if (toolTimeout && typeof toolTimeout === "number" && toolTimeout > 0) {
1714
+ const toolTimeout = self._options.timeout;
1715
+ let result;
1716
+ if (typeof toolTimeout === "number" && toolTimeout > 0) {
1717
+ const controller = new AbortController();
1564
1718
  const timeoutErr = /* @__PURE__ */ new Error(`工具 ${toolName} 执行超时 (${toolTimeout}ms)`);
1565
1719
  timeoutErr.name = "ToolTimeoutError";
1566
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(timeoutErr), toolTimeout));
1567
- execPromise = Promise.race([execPromise, timeoutPromise]);
1568
- }
1569
- const result = await execPromise;
1720
+ let timer;
1721
+ const timeoutPromise = new Promise((_, reject) => {
1722
+ timer = setTimeout(() => {
1723
+ controller.abort();
1724
+ reject(timeoutErr);
1725
+ }, toolTimeout);
1726
+ });
1727
+ const execPromise = Promise.resolve().then(() => tool.executor(args, { signal: controller.signal }));
1728
+ execPromise.catch(() => {});
1729
+ try {
1730
+ result = await Promise.race([execPromise, timeoutPromise]);
1731
+ } finally {
1732
+ clearTimeout(timer);
1733
+ }
1734
+ } else result = await tool.executor(args);
1570
1735
  chat.emit("tool-success", {
1571
1736
  toolName,
1572
1737
  result,
@@ -1749,8 +1914,8 @@ function createModelRegistryPlugin(options = {}) {
1749
1914
  * @param {Object} capabilities — 能力标签对象(部分字段即可,未提供的取默认值)
1750
1915
  * @returns {ChatService}
1751
1916
  */
1752
- chatService.registerModel = (name, capabilities = {}) => {
1753
- if (!name || typeof name !== "string" || !name.trim()) throw new Error("[model-registry] 模型名称必须是非空字符串");
1917
+ const registerModel = (name, capabilities = {}) => {
1918
+ if (!name || typeof name !== "string" || !name.trim()) throw new ValidationError("[model-registry] 模型名称必须是非空字符串");
1754
1919
  const merged = {
1755
1920
  ...DEFAULT_CAPABILITIES,
1756
1921
  ...capabilities
@@ -1763,7 +1928,7 @@ function createModelRegistryPlugin(options = {}) {
1763
1928
  * 列出所有已注册的模型名称
1764
1929
  * @returns {Array<string>}
1765
1930
  */
1766
- chatService.listModels = () => {
1931
+ const listModels = () => {
1767
1932
  return [...this._registry.keys()];
1768
1933
  };
1769
1934
  this._syncCapabilities();
@@ -1786,6 +1951,17 @@ function createModelRegistryPlugin(options = {}) {
1786
1951
  };
1787
1952
  }
1788
1953
  });
1954
+ return {
1955
+ registerModel,
1956
+ listModels,
1957
+ uninstall: (c) => this.uninstall(c)
1958
+ };
1959
+ },
1960
+ uninstall(chat = this.chat) {
1961
+ if (!chat) return chat;
1962
+ chat.unpipe?.("model-registry-caps");
1963
+ if (chat.plugins && chat.plugins["model-registry"]) delete chat.plugins["model-registry"];
1964
+ return chat;
1789
1965
  },
1790
1966
  _lookupCapabilities() {
1791
1967
  const model = this.chat.config.model;
@@ -1799,6 +1975,137 @@ function createModelRegistryPlugin(options = {}) {
1799
1975
  };
1800
1976
  }
1801
1977
  //#endregion
1802
- export { APIError, ChatService, ConfigurationError, EventEmitter, MessageFormatter, MessageStore, NetworkError, ParsingError, Pipeline, SystemPromptStore, assembleMessages, assertAdapter, createModelRegistryPlugin, createOpenAIAdapter, createToolCallingPlugin, modelRegistryPlugin, openaiAdapter, toolCallingPlugin };
1978
+ //#region src/plugins/continuation.js
1979
+ /**
1980
+ * continuation 插件(v4.0)
1981
+ *
1982
+ * 「续写 / 临时消息」这套玩法住在这里 —— 核心不认识它,装上才有:
1983
+ * 1. 续写检测(替换核心的 autoContinue 步骤):底部若有带 prefix 标记的消息,就把结果合并回它
1984
+ * 2. ephemeral 过滤器:临时消息只有"位于底部连续段"时才发给 API
1985
+ * 3. continueLast / continueLastStream 两个 API
1986
+ * 4. uninstall():把自己装的东西全部还原(步骤、过滤器、方法)
1987
+ *
1988
+ * 用法:
1989
+ * import { createContinuationPlugin } from 'my-ai-chat-framework';
1990
+ * chat.use(createContinuationPlugin({ autoContinue: true }));
1991
+ *
1992
+ * // 模拟思维链:注入一条临时引导,结果合并回上一条 assistant,不留痕
1993
+ * chat.messages.addOnceAssistant('(内心:他肯定又要熬夜)');
1994
+ * await chat.stream('在吗');
1995
+ */
1996
+ /**
1997
+ * ephemeral 规则:只有底部连续的临时消息保留,其余丢弃(与 v3.x 核心内置行为一致)。
1998
+ * 导出以便使用者直接注册到 MessageFormatter(不使用插件时也能用这条规则)。
1999
+ */
2000
+ function ephemeralFilterRule(msg, { index, messages }) {
2001
+ if (!msg._ephemeral) return msg;
2002
+ let start = messages.length;
2003
+ for (let i = messages.length - 1; i >= 0; i--) if (messages[i]._ephemeral) start = i;
2004
+ else break;
2005
+ return index >= start ? msg : null;
2006
+ }
2007
+ /**
2008
+ * 找续写目标:最后一条必须是 assistant 或临时消息(否则抛错)。
2009
+ * @param {object} chat ChatService 实例
2010
+ */
2011
+ function prepareContinue(chat) {
2012
+ const msgs = chat.messages.getAll();
2013
+ const last = msgs[msgs.length - 1];
2014
+ if (last && (last.role === "assistant" || last._ephemeral)) return last;
2015
+ throw new ValidationError("最后一条消息不是 assistant,无法续写");
2016
+ }
2017
+ /** 续写结束后的"转正" */
2018
+ function finalize(chat, targetId) {
2019
+ chat.messages.update(targetId, {
2020
+ _complete: true,
2021
+ prefix: void 0,
2022
+ _ephemeral: false
2023
+ });
2024
+ }
2025
+ /**
2026
+ * 创建续写插件实例(工厂)
2027
+ * @param {Object} [options] — { autoContinue?: boolean } 是否自动识别底部 prefix 消息并合并(默认 false)
2028
+ */
2029
+ function createContinuationPlugin(options = {}) {
2030
+ return {
2031
+ name: "continuation",
2032
+ _options: {
2033
+ autoContinue: false,
2034
+ ...options
2035
+ },
2036
+ _prevEphemeralFilter: void 0,
2037
+ _installedOn: null,
2038
+ install(chat) {
2039
+ this._installedOn = chat;
2040
+ if (typeof chat.registerConfigKeys === "function") chat.registerConfigKeys({ autoContinue: (v) => typeof v === "boolean" });
2041
+ chat.replaceStage("autoContinue", (ctx) => {
2042
+ if (!(chat.config.autoContinue ?? this._options.autoContinue)) return;
2043
+ if (ctx.mergeToEntry) return;
2044
+ const last = ctx.messages.getLast();
2045
+ if (last && last.prefix && (last.role === "assistant" || last._ephemeral)) ctx.mergeToEntry = last.id;
2046
+ });
2047
+ this._prevEphemeralFilter = MessageFormatter.registerFilter("ephemeral", ephemeralFilterRule, { priority: 100 });
2048
+ const continueLast = async () => {
2049
+ const target = prepareContinue(chat);
2050
+ chat.messages.update(target.id, { prefix: true });
2051
+ try {
2052
+ await chat.sendExisting(void 0, { mergeToEntry: target.id });
2053
+ finalize(chat, target.id);
2054
+ return target;
2055
+ } catch (err) {
2056
+ finalize(chat, target.id);
2057
+ if (err.name === "AbortError") {
2058
+ chat.emit("aborted", { timestamp: Date.now() });
2059
+ return target;
2060
+ }
2061
+ throw err;
2062
+ }
2063
+ };
2064
+ const continueLastStream = async (onProgress, onDone) => {
2065
+ const target = prepareContinue(chat);
2066
+ chat.messages.update(target.id, { prefix: true });
2067
+ const baseLen = (target.content || "").length;
2068
+ try {
2069
+ await chat.sendExistingStream(void 0, { mergeToEntry: target.id }, (chunk) => {
2070
+ if (onProgress) onProgress({
2071
+ ...chunk,
2072
+ content: (chunk.content || "").slice(baseLen)
2073
+ });
2074
+ }, (final) => {
2075
+ if (onDone) onDone(final);
2076
+ });
2077
+ finalize(chat, target.id);
2078
+ return target;
2079
+ } catch (err) {
2080
+ finalize(chat, target.id);
2081
+ if (err.name === "AbortError") {
2082
+ chat.emit("aborted", { timestamp: Date.now() });
2083
+ return target;
2084
+ }
2085
+ throw err;
2086
+ }
2087
+ };
2088
+ return {
2089
+ continueLast,
2090
+ continueLastStream,
2091
+ uninstall: () => this.uninstall(chat)
2092
+ };
2093
+ },
2094
+ uninstall(chat = this._installedOn) {
2095
+ if (!chat) return chat;
2096
+ if (typeof chat.restoreStage === "function") chat.restoreStage("autoContinue");
2097
+ if (this._prevEphemeralFilter) MessageFormatter.registerFilter("ephemeral", this._prevEphemeralFilter, { priority: 100 });
2098
+ else MessageFormatter.unregisterFilter("ephemeral");
2099
+ if (chat.plugins && chat.plugins["continuation"]) delete chat.plugins["continuation"];
2100
+ if (typeof chat.unregisterConfigKeys === "function") chat.unregisterConfigKeys(["autoContinue"]);
2101
+ this._installedOn = null;
2102
+ return chat;
2103
+ }
2104
+ };
2105
+ }
2106
+ /** 兼容用单例(多实例场景请用工厂) */
2107
+ var continuationPlugin = createContinuationPlugin();
2108
+ //#endregion
2109
+ export { APIError, ChatService, ConfigurationError, EventEmitter, MessageFormatter, MessageStore, NetworkError, ParsingError, Pipeline, SystemPromptStore, ValidationError, assembleMessages, assertAdapter, continuationPlugin, createContinuationPlugin, createModelRegistryPlugin, createOpenAIAdapter, createToolCallingPlugin, modelRegistryPlugin, openaiAdapter, toolCallingPlugin };
1803
2110
 
1804
2111
  //# sourceMappingURL=my-ai-chat-framework.browser.es.js.map