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.
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from './EventEmitter.js';
2
2
  import { MessageStore } from './MessageStore.js';
3
3
  import { SystemPromptStore } from './SystemPromptStore.js';
4
- import { ConfigurationError, NetworkError } from './Errors.js';
4
+ import { ConfigurationError, NetworkError, ValidationError } from './Errors.js';
5
5
  import { Pipeline } from './Pipeline.js';
6
6
 
7
7
  /**
@@ -38,19 +38,29 @@ import { Pipeline } from './Pipeline.js';
38
38
  */
39
39
 
40
40
  // updateConfig 白名单:会话层字段(运输层/请求默认参数归适配器,能力表归插件)
41
- const SESSION_CONFIG_KEYS = ['model', 'temperature', 'maxTokens', 'modelParams', 'system', 'retry', 'ephemeralContinue'];
41
+ const SESSION_CONFIG_KEYS = ['model', 'temperature', 'maxTokens', 'modelParams', 'system', 'retry'];
42
+
43
+ // 内部车间四步 + 发送(顺序即执行顺序);v3.1 起每一步都可以被 replaceStage 顶替、被 unstage 关闭
44
+ const INTERNAL_STAGES = ['prepareInput', 'beforeSend', 'autoContinue', 'buildRequest', 'send'];
42
45
 
43
46
  export class ChatService extends EventEmitter {
44
47
  constructor(config = {}) {
45
48
  super();
46
49
  this.config = { ...config };
47
- this.messages = new MessageStore();
50
+ // v4.0(B3):消息存储可注入 —— 想换成自定义 store(如将来支持对话树)不用改核心
51
+ if (config.store) {
52
+ if (typeof config.store.getAll !== 'function' || typeof config.store.add !== 'function') {
53
+ throw new ValidationError('store 必须实现 MessageStore 的接口(至少 getAll / add / update)');
54
+ }
55
+ this.messages = config.store;
56
+ } else {
57
+ this.messages = new MessageStore();
58
+ }
48
59
  this.systemPrompts = new SystemPromptStore();
49
60
  this._adapter = null;
50
- this._abortController = null;
51
- this._isGenerating = false;
52
- this._hooks = new EventEmitter();
53
- this._processResponse = null;
61
+ // v4.0(B1):每个请求各自持有 AbortController —— 多请求互不干扰(工具循环里再发请求也安全)
62
+ this._activeRequests = new Map(); // requestId → AbortController
63
+ this._requestSeq = 0;
54
64
 
55
65
  const model = this.config.model || this.config.modelParams?.model;
56
66
  if (!model || typeof model !== 'string' || !model.trim()) {
@@ -68,6 +78,10 @@ export class ChatService extends EventEmitter {
68
78
 
69
79
  // ===== 内部管道(v3.0):内部车间 + 用户车间(pipe 注册)按 phase 插入 =====
70
80
  this._userStages = [];
81
+ this._stageOverrides = new Map(); // v3.1 出口:内部步骤名 → 用户实现
82
+ this._disabledStages = new Set(); // v3.1 出口:被 unstage 关闭的内部步骤
83
+ this._extraConfigKeys = new Map(); // v4.0:插件注册的运行时可改字段 → 校验器(可空)
84
+ this.plugins = {}; // v4.0(B4):插件命名空间 —— 插件 API 挂在 chat.plugins.<插件名> 下,互不撞名
71
85
  this._pipeline = this._buildPipeline();
72
86
  }
73
87
 
@@ -92,8 +106,18 @@ export class ChatService extends EventEmitter {
92
106
  }
93
107
  }
94
108
 
109
+ /**
110
+ * 安装插件。
111
+ * - 插件实现 `install(chat, options)`
112
+ * - 若 install 返回一个对象,会挂到 **`chat.plugins[plugin.name]`**(v4.0 命名空间,插件之间不会撞名)
113
+ * - 插件要提供卸载时,自己在返回值里带 `uninstall()`(或调用 `plugin.uninstall(chat)`)
114
+ * @returns {ChatService} this
115
+ */
95
116
  use(plugin, options = {}) {
96
- plugin.install(this, options);
117
+ const api = plugin.install(this, options);
118
+ if (plugin.name && api && typeof api === 'object') {
119
+ this.plugins[plugin.name] = api;
120
+ }
97
121
  return this;
98
122
  }
99
123
 
@@ -115,8 +139,7 @@ export class ChatService extends EventEmitter {
115
139
  if (typeof stage.name !== 'string' || !stage.name.trim()) {
116
140
  throw new ConfigurationError('pipe: 需要 name(车间名,非空字符串)');
117
141
  }
118
- const INTERNAL = ['prepareInput', 'beforeSend', 'autoContinue', 'buildRequest', 'send'];
119
- if (INTERNAL.includes(stage.name)) {
142
+ if (INTERNAL_STAGES.includes(stage.name)) {
120
143
  throw new ConfigurationError(`pipe: "${stage.name}" 是内部车间名,请换一个名字`);
121
144
  }
122
145
  if (this._userStages.some(s => s.name === stage.name)) {
@@ -141,19 +164,104 @@ export class ChatService extends EventEmitter {
141
164
  return this._pipeline.names();
142
165
  }
143
166
 
144
- /** 重建内部管道:内部 5 车间 + 用户车间按 phase 插入 */
167
+ // ========== 出口 API(v3.1):替换 / 关闭 内部步骤 ==========
168
+
169
+ /** 内部步骤名(顺序即执行顺序):prepareInput / beforeSend / autoContinue / buildRequest / send */
170
+ get internalStages() {
171
+ return [...INTERNAL_STAGES];
172
+ }
173
+
174
+ /** 内部步骤的默认实现表 */
175
+ _defaultStageRuns() {
176
+ return {
177
+ prepareInput: (ctx) => this._stagePrepareInput(ctx),
178
+ beforeSend: (ctx) => this._stageBeforeSend(ctx),
179
+ autoContinue: (ctx) => this._stageAutoContinue(ctx),
180
+ buildRequest: (ctx) => this._stageBuildRequest(ctx),
181
+ send: (ctx) => this._stageSend(ctx),
182
+ };
183
+ }
184
+
185
+ /**
186
+ * 用你自己的实现**顶替**某个内部步骤。框架不再执行该步的默认行为。
187
+ * 例:`chat.replaceStage('send', async (ctx) => { ctx.result = await 我的发送(ctx.body) })`
188
+ * 想"包一层"默认实现:先 `const base = chat.getStage('send')`,再在自己的函数里调用它。
189
+ * @param {string} name 步骤名(见 internalStages)
190
+ * @param {Function} run (ctx) => void | Promise<void>
191
+ * @throws {ConfigurationError} 步骤名未知 / run 不是函数
192
+ */
193
+ replaceStage(name, run) {
194
+ if (!INTERNAL_STAGES.includes(name)) {
195
+ throw new ConfigurationError(`replaceStage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(', ')}`);
196
+ }
197
+ if (typeof run !== 'function') {
198
+ throw new ConfigurationError('replaceStage: 第二个参数必须是函数 (ctx) => void | Promise<void>');
199
+ }
200
+ this._stageOverrides.set(name, run);
201
+ this._rebuildPipeline();
202
+ return this;
203
+ }
204
+
205
+ /** 撤销替换,恢复该步骤的默认实现(不影响 unstage 的关闭状态) */
206
+ restoreStage(name) {
207
+ if (!INTERNAL_STAGES.includes(name)) {
208
+ throw new ConfigurationError(`restoreStage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(', ')}`);
209
+ }
210
+ this._stageOverrides.delete(name);
211
+ this._rebuildPipeline();
212
+ return this;
213
+ }
214
+
215
+ /**
216
+ * **关闭**某个内部步骤(从管道里移除,不再执行)。
217
+ * 例:`chat.unstage('autoContinue')` —— 彻底不要"自动续写检测"这一步。
218
+ * 警告:关闭 prepareInput / buildRequest / send 会让流程失去必要产物(如 ctx.body)。
219
+ */
220
+ unstage(name) {
221
+ if (!INTERNAL_STAGES.includes(name)) {
222
+ throw new ConfigurationError(`unstage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(', ')}`);
223
+ }
224
+ this._disabledStages.add(name);
225
+ this._rebuildPipeline();
226
+ return this;
227
+ }
228
+
229
+ /** 重新启用被 unstage 关闭的步骤 */
230
+ restage(name) {
231
+ if (!INTERNAL_STAGES.includes(name)) {
232
+ throw new ConfigurationError(`restage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(', ')}`);
233
+ }
234
+ this._disabledStages.delete(name);
235
+ this._rebuildPipeline();
236
+ return this;
237
+ }
238
+
239
+ /** 取得某个内部步骤的默认实现(用于"包一层":做自己的事,再调用它) */
240
+ getStage(name) {
241
+ if (!INTERNAL_STAGES.includes(name)) {
242
+ throw new ConfigurationError(`getStage: 未知的内部步骤 "${name}"。可选: ${INTERNAL_STAGES.join(', ')}`);
243
+ }
244
+ return this._defaultStageRuns()[name];
245
+ }
246
+
247
+ /**
248
+ * 重建内部管道:内部步骤(可被 replaceStage 顶替 / 被 unstage 关闭)+ 用户车间按 phase 插入
249
+ * 顺序:prepareInput → beforeSend → [用户 beforeSend 车间] → autoContinue → buildRequest → send → [用户 afterSend 车间]
250
+ */
145
251
  _buildPipeline() {
146
252
  const p = new Pipeline();
147
- p.register({ name: 'prepareInput', run: (ctx) => this._stagePrepareInput(ctx) });
148
- p.register({ name: 'beforeSend', run: (ctx) => this._stageBeforeSend(ctx) });
149
- for (const s of this._userStages) {
150
- if (s.phase === 'beforeSend') p.register({ name: s.name, run: (ctx) => s.run(ctx) });
151
- }
152
- p.register({ name: 'autoContinue', run: (ctx) => this._stageAutoContinue(ctx) });
153
- p.register({ name: 'buildRequest', run: (ctx) => this._stageBuildRequest(ctx) });
154
- p.register({ name: 'send', run: (ctx) => this._stageSend(ctx) });
155
- for (const s of this._userStages) {
156
- if (s.phase === 'afterSend') p.register({ name: s.name, run: (ctx) => s.run(ctx) });
253
+ const defaults = this._defaultStageRuns();
254
+ const insertUsers = (phase) => {
255
+ for (const s of this._userStages) {
256
+ if (s.phase === phase) p.register({ name: s.name, run: (ctx) => s.run(ctx) });
257
+ }
258
+ };
259
+ for (const name of INTERNAL_STAGES) {
260
+ if (!this._disabledStages.has(name)) {
261
+ p.register({ name, run: this._stageOverrides.get(name) || defaults[name] });
262
+ }
263
+ if (name === 'beforeSend') insertUsers('beforeSend');
264
+ if (name === 'send') insertUsers('afterSend');
157
265
  }
158
266
  return p;
159
267
  }
@@ -167,95 +275,100 @@ export class ChatService extends EventEmitter {
167
275
  this._adapter = adapter;
168
276
  }
169
277
 
170
- abort() {
171
- if (this._abortController) {
172
- this._abortController.abort();
278
+ /**
279
+ * 中断请求(v4.0:按请求隔离)。
280
+ * - 不传参数:中断**所有**活跃请求(旧版只会中断"最后一个",多请求时会中断错的那个)
281
+ * - 传 requestId:只中断那一个(id 见 `chat.activeRequests`)
282
+ */
283
+ abort(requestId) {
284
+ if (requestId) {
285
+ const controller = this._activeRequests.get(requestId);
286
+ if (controller) controller.abort();
287
+ return;
173
288
  }
289
+ for (const controller of this._activeRequests.values()) controller.abort();
174
290
  }
175
291
 
176
- get isGenerating() { return this._isGenerating; }
292
+ /** 是否有请求正在生成中 */
293
+ get isGenerating() { return this._activeRequests.size > 0; }
177
294
 
178
- async continueLast() {
179
- const target = this._prepareContinue();
180
- this.messages.update(target.id, { prefix: true });
181
- try {
182
- await this._request({ addUser: false, isStream: false, mergeToEntry: target.id });
183
- this.messages.update(target.id, {
184
- _complete: true, prefix: undefined, _ephemeral: false
185
- });
186
- return target;
187
- } catch (err) {
188
- this.messages.update(target.id, {
189
- _complete: true, prefix: undefined, _ephemeral: false
190
- });
191
- if (err.name === 'AbortError') {
192
- this.emit('aborted', { timestamp: Date.now() });
193
- return target;
194
- }
195
- throw err;
196
- }
295
+ /** 当前活跃请求的 id 列表(调试 / 精确中断用) */
296
+ get activeRequests() { return [...this._activeRequests.keys()]; }
297
+
298
+ /** 内部:登记一个请求,返回 { id, signal } */
299
+ _beginRequest() {
300
+ const id = 'req_' + (++this._requestSeq);
301
+ const controller = new AbortController();
302
+ this._activeRequests.set(id, controller);
303
+ return { id, signal: controller.signal };
197
304
  }
198
305
 
199
- async continueLastStream(onProgress, onDone) {
200
- const target = this._prepareContinue();
201
- this.messages.update(target.id, { prefix: true });
202
- const baseLen = (target.content || '').length;
306
+ /** 内部:注销一个请求 */
307
+ _endRequest(id) {
308
+ this._activeRequests.delete(id);
309
+ }
203
310
 
204
- try {
205
- await this._request({
206
- addUser: false, isStream: true, mergeToEntry: target.id,
207
- onProgress: (chunk) => {
208
- if (onProgress) {
209
- onProgress({ ...chunk, content: (chunk.content || '').slice(baseLen) });
210
- }
211
- },
212
- onDone: (final) => { if (onDone) onDone(final); }
213
- });
214
- this.messages.update(target.id, {
215
- _complete: true, prefix: undefined, _ephemeral: false
216
- });
217
- return target;
218
- } catch (err) {
219
- // 保留已累积的内容,标记完成
220
- this.messages.update(target.id, {
221
- _complete: true, prefix: undefined, _ephemeral: false
222
- });
223
- // abort 是用户主动行为,静默处理,通知外部
224
- if (err.name === 'AbortError') {
225
- this.emit('aborted', { timestamp: Date.now() });
226
- return target;
311
+ // 注(v4.0):continueLast / continueLastStream / _prepareContinue 已迁出核心。
312
+ // 续写是「玩法」,由 src/plugins/continuation.js 提供 —— chat.use(createContinuationPlugin())。
313
+
314
+ /**
315
+ * 注册"运行时可改的配置字段"(v4.0)。
316
+ * 让插件自带配置(如 continuation 的 autoContinue)不必回头改核心白名单。
317
+ * @param {string[]|Object<string, Function>} keys 字段名数组,或 { 字段名: 校验器(value)=>boolean }
318
+ * @returns {ChatService} this
319
+ * @throws {ConfigurationError} 字段名非法 / 校验器不是函数
320
+ */
321
+ registerConfigKeys(keys) {
322
+ const entries = Array.isArray(keys) ? keys.map(k => [k, null]) : Object.entries(keys || {});
323
+ for (const [key, validator] of entries) {
324
+ if (typeof key !== 'string' || !key.trim()) {
325
+ throw new ConfigurationError('registerConfigKeys: 字段名必须是非空字符串');
227
326
  }
228
- throw err;
327
+ if (validator != null && typeof validator !== 'function') {
328
+ throw new ConfigurationError(`registerConfigKeys: "${key}" 的校验器必须是函数`);
329
+ }
330
+ this._extraConfigKeys.set(key.trim(), validator || null);
229
331
  }
332
+ return this;
230
333
  }
231
334
 
232
- _prepareContinue() {
233
- const msgs = this.messages.getAll();
234
- const last = msgs[msgs.length - 1];
235
- if (last && (last.role === 'assistant' || last._ephemeral)) return last;
236
- throw new Error('最后一条消息不是 assistant,无法续写');
335
+ /** 注销运行时可改字段(插件卸载时调用)@returns {ChatService} this */
336
+ unregisterConfigKeys(keys) {
337
+ for (const k of (Array.isArray(keys) ? keys : [keys])) this._extraConfigKeys.delete(k);
338
+ return this;
339
+ }
340
+
341
+ /** 当前允许在运行时修改的配置字段(内置基础字段 + 插件注册) */
342
+ get configKeys() {
343
+ return [...SESSION_CONFIG_KEYS, ...this._extraConfigKeys.keys()];
237
344
  }
238
345
 
239
346
  /**
240
- * 运行时修改配置(仅会话层字段)。
347
+ * 运行时修改配置(仅"可改字段":内置基础字段 + 插件注册字段)。
241
348
  *
242
- * 白名单:model / temperature / maxTokens / modelParams / system / retry / ephemeralContinue。
243
349
  * - 运输层(apiKey/baseUrl/headers):归适配器,要改就换适配器实例
244
350
  * - 请求参数默认值:走适配器工厂 options 或请求级覆盖(chat.send(x, params))
245
- * - 能力表:归 modelRegistry 插件
351
+ * - 插件自有配置:插件用 `registerConfigKeys` 登记后才可改(工厂 options 仍是首选)
246
352
  *
247
353
  * 与构造器一样经过校验,不能注入非法值。
248
- * @throws {ConfigurationError} 非白名单字段或非法值
354
+ * @throws {ConfigurationError} 非白名单字段、非法值、或插件校验器拒绝
249
355
  */
250
356
  updateConfig(partial) {
357
+ const allowed = this.configKeys;
251
358
  for (const key of Object.keys(partial)) {
252
- if (!SESSION_CONFIG_KEYS.includes(key)) {
359
+ if (!allowed.includes(key)) {
253
360
  throw new ConfigurationError(
254
- `updateConfig 不支持修改 "${key}"。允许的会话层字段: ${SESSION_CONFIG_KEYS.join(', ')}`
361
+ `updateConfig 不支持修改 "${key}"。允许的字段: ${allowed.join(', ')}`
255
362
  );
256
363
  }
257
364
  }
258
365
  this._assertValidConfig(partial);
366
+ // 插件注册字段的校验(校验器返回 false 即拒绝)
367
+ for (const [key, validator] of this._extraConfigKeys) {
368
+ if (validator && key in partial && validator(partial[key]) === false) {
369
+ throw new ConfigurationError(`updateConfig: 字段 "${key}" 的值不合法: ${JSON.stringify(partial[key])}`);
370
+ }
371
+ }
259
372
  Object.assign(this.config, partial);
260
373
  if (typeof partial.system === 'string') this.systemPrompts.set(partial.system);
261
374
  this.emit('config-updated', { changes: partial, timestamp: Date.now() });
@@ -314,6 +427,7 @@ export class ChatService extends EventEmitter {
314
427
  onProgress: options.onProgress || null,
315
428
  onDone: options.onDone || null,
316
429
  mergeToEntry: options.mergeToEntry || null,
430
+ signal: options.signal || null, // v4.0(B1):本请求自己的中断信号
317
431
  body: null,
318
432
  result: null,
319
433
  };
@@ -333,29 +447,28 @@ export class ChatService extends EventEmitter {
333
447
  /** 车间 1:加用户消息 → 合并请求参数 → 广播 sending */
334
448
  async _stagePrepareInput(ctx) {
335
449
  this._addUserMessage(ctx.userInput, ctx.addUser);
336
- // 合并请求参数:先于 beforeRequest 钩子,钩子拿到的是本次生效的配置
450
+ // 合并请求参数(合并后的 ctx.config 供后续车间使用)
337
451
  ctx.config = this._mergeRequestConfig(ctx.options.params);
338
452
  this.emit('sending', { addUser: ctx.addUser, userInput: ctx.userInput, timestamp: Date.now() });
339
453
  }
340
454
 
341
- /** 车间 2:发送前钩子(兼容桥:事件钩子照发;阶段 4 会迁到 beforeSend 公开位置) */
455
+ /**
456
+ * 车间 2:发送前 —— **v4.0 起默认什么都不做**。
457
+ * 需要在这里做事的(注入、改写消息/配置):`chat.replaceStage('beforeSend', fn)`
458
+ * 或挂一个 `chat.pipe({ phase: 'beforeSend' })` 车间(在它之后执行)。
459
+ * 旧的 `_hooks.on('beforeRequest')` 兼容桥已于 v4.0 移除。
460
+ */
342
461
  async _stageBeforeSend(ctx) {
343
- await this._hooks.emitAsync('beforeRequest', {
344
- messages: ctx.messages,
345
- config: ctx.config,
346
- options: ctx.options
347
- });
462
+ // 默认空实现(no-op)
348
463
  }
349
464
 
350
- /** 车间 3:自动续写检测(ephemeralContinue 时,底部若有 prefix 消息则续写) */
465
+ /**
466
+ * 车间 3:自动续写检测 —— **v4.0 起默认什么都不做**。
467
+ * 续写是玩法:需要它的装上 continuation 插件,由插件 replaceStage('autoContinue', ...) 接管。
468
+ * 步骤名与位置保留,方便插件替换、也方便使用者 `unstage('autoContinue')` 明确移除。
469
+ */
351
470
  async _stageAutoContinue(ctx) {
352
- if (this.config.ephemeralContinue && !ctx.mergeToEntry) {
353
- const msgs = ctx.messages.getAll();
354
- const last = msgs[msgs.length - 1];
355
- if (last && last.prefix && (last.role === 'assistant' || last._ephemeral)) {
356
- ctx.mergeToEntry = last.id;
357
- }
358
- }
471
+ // 默认空实现(no-op)
359
472
  }
360
473
 
361
474
  /** 车间 4:构建请求体 */
@@ -375,7 +488,8 @@ export class ChatService extends EventEmitter {
375
488
  mergeToEntry: ctx.mergeToEntry,
376
489
  config: ctx.config,
377
490
  maxRetries: retryCfg.maxRetries ?? 0,
378
- retryDelay: retryCfg.retryDelay ?? 1000
491
+ retryDelay: retryCfg.retryDelay ?? 1000,
492
+ signal: ctx.signal
379
493
  });
380
494
  }
381
495
 
@@ -389,9 +503,10 @@ export class ChatService extends EventEmitter {
389
503
  }
390
504
 
391
505
  /** retry 循环,占位消息只 push 一次 */
392
- async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, config, maxRetries, retryDelay }) {
506
+ async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, config, maxRetries, retryDelay, signal }) {
393
507
  let lastError = null;
394
- const adapterOptions = { signal: this._abortController?.signal };
508
+ // v4.0(B1):使用"本请求自己的"信号(不再读写共享的 this._abortController)
509
+ const adapterOptions = { signal };
395
510
 
396
511
  let placeholder = null;
397
512
  let base = null;
@@ -411,28 +526,22 @@ export class ChatService extends EventEmitter {
411
526
  await new Promise(r => setTimeout(r, retryDelay));
412
527
  }
413
528
  try {
414
- this._isGenerating = true;
415
529
  if (isStream) {
416
530
  return await this._stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone, config });
417
531
  } else {
418
532
  const resp = await this._adapter.send(body, config, adapterOptions);
419
- let result = this._handleResult(this._adapter.parseResponse(resp), mergeToEntry);
420
- // 响应后处理钩子(tool-calling 用),仅非续写时
421
- if (this._processResponse && !mergeToEntry) {
422
- result = await this._processResponse(result, { isStream: false });
423
- }
424
- return result;
533
+ // 响应落位后交给管道:afterSend 车间接管后续处理(如工具调用循环)
534
+ return this._handleResult(this._adapter.parseResponse(resp), mergeToEntry);
425
535
  }
426
536
  } catch (error) {
427
537
  lastError = error;
428
- if (error.name === 'AbortError' || this._abortController?.signal.aborted) {
429
- this._isGenerating = false; throw error;
538
+ if (error.name === 'AbortError' || signal?.aborted) {
539
+ throw error;
430
540
  }
431
541
  if (error instanceof NetworkError && attempt < maxRetries) continue;
432
- this._isGenerating = false; throw error;
542
+ throw error;
433
543
  }
434
544
  }
435
- this._isGenerating = false;
436
545
  throw lastError;
437
546
  }
438
547
 
@@ -451,7 +560,8 @@ export class ChatService extends EventEmitter {
451
560
 
452
561
  await this._adapter.stream(body, config,
453
562
  // === onProgress:每收到一个 SSE chunk 触发 ===
454
- (snap) => {
563
+ // snap = 全量累积快照;delta = 本次新增片段(v4.0 起两个都给你)
564
+ (snap, delta) => {
455
565
  // snap.content 是 adapter 累积的全部 API 新内容(不是增量)
456
566
  // 续写时:base.content(旧内容)+ snap.content(API 新内容)= 完整内容
457
567
  // 普通发送时:直接用 snap.content
@@ -466,19 +576,12 @@ export class ChatService extends EventEmitter {
466
576
  }
467
577
  if (snap.toolCalls) placeholder.toolCalls = [...snap.toolCalls];
468
578
  this.emit('stream-progress', snap);
469
- if (onProgress) onProgress(snap);
579
+ if (onProgress) onProgress(snap, delta); // v4.0:第二个参数是本次增量
470
580
  },
471
581
  // === onDone:流结束,做最终落位 ===
472
582
  async (final) => {
473
583
  finalMsg = final;
474
584
 
475
- // 工具调用钩子(仅普通发送时,续写不走 tool-calling 循环)
476
- if (this._processResponse && !mergeToEntry) {
477
- finalMsg = await this._processResponse(finalMsg, {
478
- isStream: true, onProgress, onDone
479
- });
480
- }
481
-
482
585
  if (mergeToEntry && base) {
483
586
  // 续写:把 base + 最终内容写回 target,标记完成
484
587
  const newContent = base.content + (final.content || '');
@@ -539,9 +642,9 @@ export class ChatService extends EventEmitter {
539
642
  * @param {Object} [params] - 请求级参数覆盖(model/temperature/modelParams 等,仅本次生效)
540
643
  */
541
644
  async send(userInput, params) {
542
- this._abortController = new AbortController();
645
+ const { id, signal } = this._beginRequest();
543
646
  try {
544
- return await this._request({ userInput, addUser: true, isStream: false, params });
647
+ return await this._request({ userInput, addUser: true, isStream: false, params, signal });
545
648
  } catch (err) {
546
649
  if (err.name === 'AbortError') {
547
650
  this.emit('aborted', { timestamp: Date.now() });
@@ -549,8 +652,7 @@ export class ChatService extends EventEmitter {
549
652
  }
550
653
  throw err;
551
654
  } finally {
552
- this._abortController = null;
553
- this._isGenerating = false;
655
+ this._endRequest(id);
554
656
  }
555
657
  }
556
658
 
@@ -568,9 +670,9 @@ export class ChatService extends EventEmitter {
568
670
  onProgress = params;
569
671
  params = undefined;
570
672
  }
571
- this._abortController = new AbortController();
673
+ const { id, signal } = this._beginRequest();
572
674
  try {
573
- return await this._request({ userInput, addUser: true, isStream: true, onProgress, onDone, params });
675
+ return await this._request({ userInput, addUser: true, isStream: true, onProgress, onDone, params, signal });
574
676
  } catch (err) {
575
677
  if (err.name === 'AbortError') {
576
678
  this.emit('aborted', { timestamp: Date.now() });
@@ -578,19 +680,20 @@ export class ChatService extends EventEmitter {
578
680
  }
579
681
  throw err;
580
682
  } finally {
581
- this._abortController = null;
582
- this._isGenerating = false;
683
+ this._endRequest(id);
583
684
  }
584
685
  }
585
686
 
586
687
  /**
587
688
  * 重发当前消息(不加用户消息)。
588
689
  * @param {Object} [params] - 请求级参数覆盖(仅本次生效)
690
+ * @param {Object} [options] - { mergeToEntry } 可选:把结果合并进指定的已有消息(续写等场景)
589
691
  */
590
- async sendExisting(params) {
591
- this._abortController = new AbortController();
692
+ async sendExisting(params, options = {}) {
693
+ const mergeToEntry = options?.mergeToEntry || null;
694
+ const { id, signal } = this._beginRequest();
592
695
  try {
593
- return await this._request({ addUser: false, isStream: false, params });
696
+ return await this._request({ addUser: false, isStream: false, params, mergeToEntry, signal });
594
697
  } catch (err) {
595
698
  if (err.name === 'AbortError') {
596
699
  this.emit('aborted', { timestamp: Date.now() });
@@ -598,27 +701,34 @@ export class ChatService extends EventEmitter {
598
701
  }
599
702
  throw err;
600
703
  } finally {
601
- this._abortController = null;
602
- this._isGenerating = false;
704
+ this._endRequest(id);
603
705
  }
604
706
  }
605
707
 
606
708
  /**
607
709
  * 流式重发(不加用户消息)。
608
710
  * @param {Object} [params] - 请求级参数覆盖(仅本次生效)
711
+ * @param {Object} [options] - { mergeToEntry } 可选:把结果合并进指定的已有消息
609
712
  * @param {Function} [onProgress] - 流式进度回调
610
713
  * @param {Function} [onDone] - 流式完成回调
611
714
  */
612
- async sendExistingStream(params, onProgress, onDone) {
613
- // 兼容旧签名 sendExistingStream(onProgress, onDone)
715
+ async sendExistingStream(params, options, onProgress, onDone) {
716
+ // 兼容签名1:sendExistingStream(onProgress, onDone)
614
717
  if (typeof params === 'function') {
615
- onDone = onProgress;
718
+ onDone = options;
616
719
  onProgress = params;
720
+ options = {};
617
721
  params = undefined;
722
+ } else if (typeof options === 'function') {
723
+ // 兼容签名2:sendExistingStream(params, onProgress, onDone)
724
+ onDone = onProgress;
725
+ onProgress = options;
726
+ options = {};
618
727
  }
619
- this._abortController = new AbortController();
728
+ const mergeToEntry = options?.mergeToEntry || null;
729
+ const { id, signal } = this._beginRequest();
620
730
  try {
621
- return await this._request({ addUser: false, isStream: true, onProgress, onDone, params });
731
+ return await this._request({ addUser: false, isStream: true, onProgress, onDone, params, mergeToEntry, signal });
622
732
  } catch (err) {
623
733
  if (err.name === 'AbortError') {
624
734
  this.emit('aborted', { timestamp: Date.now() });
@@ -626,8 +736,7 @@ export class ChatService extends EventEmitter {
626
736
  }
627
737
  throw err;
628
738
  } finally {
629
- this._abortController = null;
630
- this._isGenerating = false;
739
+ this._endRequest(id);
631
740
  }
632
741
  }
633
742
  }
@@ -44,6 +44,19 @@ export class ConfigurationError extends Error {
44
44
  }
45
45
  }
46
46
 
47
+ // 校验错误 (传参非法、索引越界、调用前提不满足等 —— 使用者传错了东西)
48
+ // 继承 ConfigurationError:老的 `catch (e) { if (e instanceof ConfigurationError) }` 仍然命中,
49
+ // 需要精细区分时用 `instanceof ValidationError`。
50
+ export class ValidationError extends ConfigurationError {
51
+ /**
52
+ * @param {string} message - 错误消息
53
+ */
54
+ constructor(message) {
55
+ super(message)
56
+ this.name = 'ValidationError'
57
+ }
58
+ }
59
+
47
60
  // 解析错误 (解析 API 响应失败、响应格式不正确,不符合预期等)
48
61
  export class ParsingError extends Error {
49
62
  /**