my-ai-chat-framework 2.7.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,44 @@
1
- import { EventEmitter } from './EventEmitter.js';
1
+ import { EventEmitter } from './EventEmitter.js';
2
2
  import { MessageStore } from './MessageStore.js';
3
3
  import { SystemPromptStore } from './SystemPromptStore.js';
4
4
  import { ConfigurationError, NetworkError } from './Errors.js';
5
+ import { Pipeline } from './Pipeline.js';
6
+
7
+ /**
8
+ * @typedef {Object} ChatAdapter 适配器协议(v3.0:写同级适配器只需实现这 4+1 个方法)
9
+ * @property {Function} buildRequest (messages, config, systemPrompts) => requestBody
10
+ * @property {Function} send (body, config, opts) => Promise<apiResponse>
11
+ * @property {Function} stream (body, config, onProgress, onDone, opts) => Promise<void>
12
+ * @property {Function} parseResponse (apiResponse) => assistantMessage
13
+ * @property {Function} [getRequestDefaults] () => 默认请求参数(可选)
14
+ * @property {Function} [install] (chatService, options) 插件式安装(可选)
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} PipelineContext 管道小车 ctx(public pipe 车间可见/可改的全部字段)
19
+ * @property {Object} options 原始请求选项
20
+ * @property {*} userInput 用户输入(可为 undefined)
21
+ * @property {boolean} addUser 是否已新增/将新增用户消息
22
+ * @property {MessageStore} messages 消息列表(可读可改:push / unshift / update)
23
+ * @property {SystemPromptStore} systemPrompts 系统提示词(可读可改)
24
+ * @property {Object} config 合并后的请求级配置(prepareInput 后生效)
25
+ * @property {boolean} isStream 本次是否流式
26
+ * @property {Function|null} onProgress 流式进度回调
27
+ * @property {Function|null} onDone 流式完成回调
28
+ * @property {string|null} mergeToEntry 续写目标 id(null = 普通发送)
29
+ * @property {*} body 请求体(buildRequest 后产出)
30
+ * @property {*} result 最终结果(send 后产出)
31
+ */
32
+
33
+ /**
34
+ * @typedef {Object} PipeStage 车间(v3.0 公开扩展单元)
35
+ * @property {string} name 车间名(唯一,不能与内部车间重名)
36
+ * @property {string} [phase] 'beforeSend'(默认)| 'afterSend'
37
+ * @property {Function} run (ctx: PipelineContext) => void | Promise<void>
38
+ */
39
+
40
+ // updateConfig 白名单:会话层字段(运输层/请求默认参数归适配器,能力表归插件)
41
+ const SESSION_CONFIG_KEYS = ['model', 'temperature', 'maxTokens', 'modelParams', 'system', 'retry', 'ephemeralContinue'];
5
42
 
6
43
  export class ChatService extends EventEmitter {
7
44
  constructor(config = {}) {
@@ -20,32 +57,120 @@ export class ChatService extends EventEmitter {
20
57
  throw new ConfigurationError('缺少 model 配置');
21
58
  }
22
59
 
23
- const temperature = this.config.modelParams?.temperature ?? this.config.temperature;
60
+ this._assertValidConfig(this.config);
61
+
62
+ if (typeof config.system === 'string' && config.system.trim()) {
63
+ this.systemPrompts.set(config.system);
64
+ }
65
+ if (config.adapter) {
66
+ this.setAdapter(config.adapter);
67
+ }
68
+
69
+ // ===== 内部管道(v3.0):内部车间 + 用户车间(pipe 注册)按 phase 插入 =====
70
+ this._userStages = [];
71
+ this._pipeline = this._buildPipeline();
72
+ }
73
+
74
+ /**
75
+ * 校验配置中的可校验字段(model 必填由构造器负责,这里只校验存在性)
76
+ * @throws {ConfigurationError}
77
+ */
78
+ _assertValidConfig(cfg) {
79
+ const temperature = cfg.modelParams?.temperature ?? cfg.temperature;
24
80
  if (temperature !== undefined && (typeof temperature !== 'number' || temperature < 0 || temperature > 2)) {
25
81
  throw new ConfigurationError(`temperature 必须在 0-2 之间,当前值: ${temperature}`);
26
82
  }
27
83
 
28
- const maxTokens = this.config.modelParams?.maxTokens ?? this.config.maxTokens;
84
+ const maxTokens = cfg.modelParams?.maxTokens ?? cfg.maxTokens;
29
85
  if (maxTokens !== undefined && (typeof maxTokens !== 'number' || maxTokens < 1 || !Number.isInteger(maxTokens))) {
30
86
  throw new ConfigurationError(`maxTokens 必须为正整数,当前值: ${maxTokens}`);
31
87
  }
32
88
 
33
- if (typeof config.system === 'string' && config.system.trim()) {
34
- this.systemPrompts.set(config.system);
89
+ const model = cfg.model ?? cfg.modelParams?.model;
90
+ if (model !== undefined && (typeof model !== 'string' || !model.trim())) {
91
+ throw new ConfigurationError('model 必须是非空字符串');
92
+ }
93
+ }
94
+
95
+ use(plugin, options = {}) {
96
+ plugin.install(this, options);
97
+ return this;
98
+ }
99
+
100
+ // ========== 公开管道 API(v3.0 · 阶段 2)==========
101
+
102
+ /**
103
+ * 往请求管道里挂一个"车间"。
104
+ * - phase 'beforeSend'(默认):在内部 beforeRequest 钩子之后、构建请求体之前执行
105
+ * - phase 'afterSend' :在内部发送(流式/非流式)完成、结果落位之后执行(可改 ctx.result)
106
+ * 不注册任何车间 = 行为与版本 2.8.x 完全一致。
107
+ * @param {PipeStage} stage
108
+ * @returns {ChatService} this
109
+ * @throws {ConfigurationError} 参数非法 / 名字被占用 / 与内部车间重名
110
+ */
111
+ pipe(stage) {
112
+ if (!stage || typeof stage !== 'object' || typeof stage.run !== 'function') {
113
+ throw new ConfigurationError('pipe: 需要 { name, phase?, run(ctx) },run 必须是函数');
114
+ }
115
+ if (typeof stage.name !== 'string' || !stage.name.trim()) {
116
+ throw new ConfigurationError('pipe: 需要 name(车间名,非空字符串)');
35
117
  }
118
+ const INTERNAL = ['prepareInput', 'beforeSend', 'autoContinue', 'buildRequest', 'send'];
119
+ if (INTERNAL.includes(stage.name)) {
120
+ throw new ConfigurationError(`pipe: "${stage.name}" 是内部车间名,请换一个名字`);
121
+ }
122
+ if (this._userStages.some(s => s.name === stage.name)) {
123
+ throw new ConfigurationError(`pipe: 车间 "${stage.name}" 已存在,请先 chat.unpipe("${stage.name}")`);
124
+ }
125
+ const phase = stage.phase === 'afterSend' ? 'afterSend' : 'beforeSend';
126
+ this._userStages.push({ name: stage.name, phase, run: stage.run });
127
+ this._rebuildPipeline();
128
+ return this;
36
129
  }
37
130
 
38
- use(plugin) {
39
- plugin.install(this);
131
+ /** 移除一个用户车间(移除不存在的名字是安全的)。@returns {ChatService} this */
132
+ unpipe(name) {
133
+ const before = this._userStages.length;
134
+ this._userStages = this._userStages.filter(s => s.name !== name);
135
+ if (this._userStages.length !== before) this._rebuildPipeline();
40
136
  return this;
41
137
  }
42
138
 
139
+ /** 查看当前管道里所有车间名(调试/说明用,含内部车间) */
140
+ get pipelineStages() {
141
+ return this._pipeline.names();
142
+ }
143
+
144
+ /** 重建内部管道:内部 5 车间 + 用户车间按 phase 插入 */
145
+ _buildPipeline() {
146
+ 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) });
157
+ }
158
+ return p;
159
+ }
160
+
161
+ _rebuildPipeline() {
162
+ this._pipeline = this._buildPipeline();
163
+ }
164
+
43
165
  setAdapter(adapter) {
166
+ assertAdapter(adapter);
44
167
  this._adapter = adapter;
45
168
  }
46
169
 
47
170
  abort() {
48
- if (this._abortController) this._abortController.abort();
171
+ if (this._abortController) {
172
+ this._abortController.abort();
173
+ }
49
174
  }
50
175
 
51
176
  get isGenerating() { return this._isGenerating; }
@@ -56,18 +181,23 @@ export class ChatService extends EventEmitter {
56
181
  try {
57
182
  await this._request({ addUser: false, isStream: false, mergeToEntry: target.id });
58
183
  this.messages.update(target.id, {
59
- _complete: true, prefix: undefined, _ephemeral: undefined
184
+ _complete: true, prefix: undefined, _ephemeral: false
60
185
  });
61
186
  return target;
62
187
  } catch (err) {
63
- this.messages.update(target.id, { prefix: undefined });
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
+ }
64
195
  throw err;
65
196
  }
66
197
  }
67
198
 
68
199
  async continueLastStream(onProgress, onDone) {
69
200
  const target = this._prepareContinue();
70
- console.log('[DEBUG] continueLastStream target.id:', target.id);
71
201
  this.messages.update(target.id, { prefix: true });
72
202
  const baseLen = (target.content || '').length;
73
203
 
@@ -82,11 +212,19 @@ export class ChatService extends EventEmitter {
82
212
  onDone: (final) => { if (onDone) onDone(final); }
83
213
  });
84
214
  this.messages.update(target.id, {
85
- _complete: true, prefix: undefined, _ephemeral: undefined
215
+ _complete: true, prefix: undefined, _ephemeral: false
86
216
  });
87
217
  return target;
88
218
  } catch (err) {
89
- this.messages.update(target.id, { prefix: undefined });
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;
227
+ }
90
228
  throw err;
91
229
  }
92
230
  }
@@ -98,52 +236,147 @@ export class ChatService extends EventEmitter {
98
236
  throw new Error('最后一条消息不是 assistant,无法续写');
99
237
  }
100
238
 
239
+ /**
240
+ * 运行时修改配置(仅会话层字段)。
241
+ *
242
+ * 白名单:model / temperature / maxTokens / modelParams / system / retry / ephemeralContinue。
243
+ * - 运输层(apiKey/baseUrl/headers):归适配器,要改就换适配器实例
244
+ * - 请求参数默认值:走适配器工厂 options 或请求级覆盖(chat.send(x, params))
245
+ * - 能力表:归 modelRegistry 插件
246
+ *
247
+ * 与构造器一样经过校验,不能注入非法值。
248
+ * @throws {ConfigurationError} 非白名单字段或非法值
249
+ */
101
250
  updateConfig(partial) {
251
+ for (const key of Object.keys(partial)) {
252
+ if (!SESSION_CONFIG_KEYS.includes(key)) {
253
+ throw new ConfigurationError(
254
+ `updateConfig 不支持修改 "${key}"。允许的会话层字段: ${SESSION_CONFIG_KEYS.join(', ')}`
255
+ );
256
+ }
257
+ }
258
+ this._assertValidConfig(partial);
102
259
  Object.assign(this.config, partial);
103
260
  if (typeof partial.system === 'string') this.systemPrompts.set(partial.system);
104
261
  this.emit('config-updated', { changes: partial, timestamp: Date.now() });
105
262
  }
106
263
 
107
264
  // ========== 编排角色 ==========
108
- async _request(options) {
109
- let { userInput, addUser = true, isStream = false, onProgress, onDone, mergeToEntry } = options;
110
265
 
111
- this.emit('sending', { addUser, userInput, timestamp: Date.now() });
112
- this._addUserMessage(userInput, addUser);
266
+ /**
267
+ * 合并请求参数(近者优先):
268
+ * 适配器默认(getRequestDefaults) ← 会话配置(this.config) ← 本次覆盖(params)
269
+ * modelParams 深层合并;平铺便捷键(temperature/maxTokens/reasoningEffort)自动折叠进 modelParams
270
+ */
271
+ _mergeRequestConfig(params = {}) {
272
+ const adapterDefaults = this._adapter?.getRequestDefaults?.() || {};
273
+ const requestConfig = { ...adapterDefaults, ...this.config, ...params };
274
+ const modelParams = {
275
+ ...(adapterDefaults.modelParams || {}),
276
+ ...(this.config.modelParams || {}),
277
+ ...(params.modelParams || {})
278
+ };
279
+ for (const key of ['temperature', 'maxTokens', 'reasoningEffort']) {
280
+ const value = params[key] ?? this.config[key] ?? adapterDefaults[key];
281
+ if (value !== undefined) modelParams[key] = value;
282
+ }
283
+ requestConfig.modelParams = modelParams;
284
+ return requestConfig;
285
+ }
113
286
 
114
- // 发送前钩子:在 adapter 转换 messages 之前执行,可修改 messages/config
115
- await this._hooks.emitAsync('beforeRequest', {
287
+ /**
288
+ * 内部调度(v3.0 提案 · 阶段 1):造一辆小车 ctx,开过内部管道,返回结果。
289
+ * 行为与旧版 _request 完全一致,只是把固定线拆成了车间(见 _stage* 方法)。
290
+ *
291
+ * ctx 字段(车间可见):
292
+ * options —— 原始请求选项
293
+ * userInput —— 用户输入
294
+ * addUser —— 是否新增用户消息
295
+ * messages —— MessageStore(可读可改)
296
+ * systemPrompts —— SystemPromptStore
297
+ * config —— 合并后的请求级配置(prepareInput 车间产出)
298
+ * isStream —— 本次是否流式
299
+ * onProgress —— 流式进度回调(可为 null)
300
+ * onDone —— 流式完成回调(可为 null)
301
+ * mergeToEntry —— 续写目标 id(空 = 普通发送)
302
+ * body —— adapter 构建出的请求体(buildRequest 车间产出)
303
+ * result —— 最终结果(send 车间产出)
304
+ */
305
+ async _request(options = {}) {
306
+ const ctx = {
307
+ options,
308
+ userInput: options.userInput,
309
+ addUser: options.addUser !== false,
116
310
  messages: this.messages,
117
- config: this.config,
118
- options
311
+ systemPrompts: this.systemPrompts,
312
+ config: null,
313
+ isStream: !!options.isStream,
314
+ onProgress: options.onProgress || null,
315
+ onDone: options.onDone || null,
316
+ mergeToEntry: options.mergeToEntry || null,
317
+ body: null,
318
+ result: null,
319
+ };
320
+ try {
321
+ await this._pipeline.run(ctx);
322
+ return ctx.result;
323
+ } catch (error) {
324
+ // AbortError 不是异常,是用户主动行为,由公共 API 层处理
325
+ if (error.name === 'AbortError') throw error;
326
+ this.emit('error', { error, timestamp: Date.now() });
327
+ throw error;
328
+ }
329
+ }
330
+
331
+ // ===== 内部车间(阶段 1):与原 _request 的固定线 1:1 对应 =====
332
+
333
+ /** 车间 1:加用户消息 → 合并请求参数 → 广播 sending */
334
+ async _stagePrepareInput(ctx) {
335
+ this._addUserMessage(ctx.userInput, ctx.addUser);
336
+ // 合并请求参数:先于 beforeRequest 钩子,钩子拿到的是本次生效的配置
337
+ ctx.config = this._mergeRequestConfig(ctx.options.params);
338
+ this.emit('sending', { addUser: ctx.addUser, userInput: ctx.userInput, timestamp: Date.now() });
339
+ }
340
+
341
+ /** 车间 2:发送前钩子(兼容桥:事件钩子照发;阶段 4 会迁到 beforeSend 公开位置) */
342
+ async _stageBeforeSend(ctx) {
343
+ await this._hooks.emitAsync('beforeRequest', {
344
+ messages: ctx.messages,
345
+ config: ctx.config,
346
+ options: ctx.options
119
347
  });
348
+ }
120
349
 
121
- // 自动续写:启用 autoContinue 时,检测底部是否有 prefix 标记的消息,自动设为续写目标
122
- // 用于 ephemeral 注入场景:注入一条带 prefix 的引导消息后,自动合并结果回原消息
123
- // 不启用时 prefix 仅透传给 API,结果仍作为新消息追加
124
- if (this.config.ephemeralContinue && !mergeToEntry) {
125
- const msgs = this.messages.getAll();
350
+ /** 车间 3:自动续写检测(ephemeralContinue 时,底部若有 prefix 消息则续写) */
351
+ async _stageAutoContinue(ctx) {
352
+ if (this.config.ephemeralContinue && !ctx.mergeToEntry) {
353
+ const msgs = ctx.messages.getAll();
126
354
  const last = msgs[msgs.length - 1];
127
355
  if (last && last.prefix && (last.role === 'assistant' || last._ephemeral)) {
128
- mergeToEntry = last.id;
356
+ ctx.mergeToEntry = last.id;
129
357
  }
130
358
  }
359
+ }
131
360
 
132
- const body = this._adapter.buildRequest(
133
- this.messages.getAll(), this.config, this.systemPrompts.getEnabled()
361
+ /** 车间 4:构建请求体 */
362
+ async _stageBuildRequest(ctx) {
363
+ ctx.body = this._adapter.buildRequest(
364
+ ctx.messages.getAll(), ctx.config, ctx.systemPrompts.getEnabled()
134
365
  );
366
+ }
135
367
 
368
+ /** 车间 5:发送(流式/非流式 + 重试 + 占位消息 + _processResponse 工具钩子) */
369
+ async _stageSend(ctx) {
136
370
  const retryCfg = this.config.retry || {};
137
- try {
138
- return await this._withRetry(body, {
139
- isStream, onProgress, onDone, mergeToEntry,
140
- maxRetries: retryCfg.maxRetries ?? 0,
141
- retryDelay: retryCfg.retryDelay ?? 1000
142
- });
143
- } catch (error) {
144
- this.emit('error', { error, timestamp: Date.now() });
145
- throw error;
146
- }
371
+ ctx.result = await this._withRetry(ctx.body, {
372
+ isStream: ctx.isStream,
373
+ onProgress: ctx.onProgress,
374
+ onDone: ctx.onDone,
375
+ mergeToEntry: ctx.mergeToEntry,
376
+ config: ctx.config,
377
+ maxRetries: retryCfg.maxRetries ?? 0,
378
+ retryDelay: retryCfg.retryDelay ?? 1000
379
+ });
147
380
  }
148
381
 
149
382
  _addUserMessage(userInput, addUser) {
@@ -156,7 +389,7 @@ export class ChatService extends EventEmitter {
156
389
  }
157
390
 
158
391
  /** retry 循环,占位消息只 push 一次 */
159
- async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, maxRetries, retryDelay }) {
392
+ async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, config, maxRetries, retryDelay }) {
160
393
  let lastError = null;
161
394
  const adapterOptions = { signal: this._abortController?.signal };
162
395
 
@@ -180,9 +413,9 @@ export class ChatService extends EventEmitter {
180
413
  try {
181
414
  this._isGenerating = true;
182
415
  if (isStream) {
183
- return await this._stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone });
416
+ return await this._stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone, config });
184
417
  } else {
185
- const resp = await this._adapter.send(body, this.config, adapterOptions);
418
+ const resp = await this._adapter.send(body, config, adapterOptions);
186
419
  let result = this._handleResult(this._adapter.parseResponse(resp), mergeToEntry);
187
420
  // 响应后处理钩子(tool-calling 用),仅非续写时
188
421
  if (this._processResponse && !mergeToEntry) {
@@ -203,21 +436,43 @@ export class ChatService extends EventEmitter {
203
436
  throw lastError;
204
437
  }
205
438
 
206
- /** 流式:ChatService 维护占位,adapter 只管解析 */
207
- async _stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone }) {
439
+ /**
440
+ * 流式处理:ChatService 维护占位消息,adapter 只负责解析 SSE
441
+ *
442
+ * 参数说明:
443
+ * - placeholder:流式期间实时更新的目标消息(续写时 = target 本身,普通发送时 = 新建的空消息)
444
+ * - base:续写时保存的旧内容快照,用于和 API 新内容拼接
445
+ * - mergeToEntry:续写目标的 id,有值时走续写逻辑
446
+ * - onProgress:外部回调,每收到一个 chunk 触发
447
+ * - onDone:流结束回调
448
+ */
449
+ async _stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone, config }) {
208
450
  let finalMsg = null;
209
451
 
210
- await this._adapter.stream(body, this.config,
452
+ await this._adapter.stream(body, config,
453
+ // === onProgress:每收到一个 SSE chunk 触发 ===
211
454
  (snap) => {
212
- placeholder.content = snap.content || '';
213
- if (snap.reasoningContent) placeholder.reasoningContent = snap.reasoningContent;
455
+ // snap.content 是 adapter 累积的全部 API 新内容(不是增量)
456
+ // 续写时:base.content(旧内容)+ snap.content(API 新内容)= 完整内容
457
+ // 普通发送时:直接用 snap.content
458
+ placeholder.content = mergeToEntry && base
459
+ ? base.content + (snap.content || '')
460
+ : (snap.content || '');
461
+ // 思维链同样拼接:续写时 base 旧思维 + 新思维
462
+ if (snap.reasoningContent) {
463
+ placeholder.reasoningContent = mergeToEntry && base
464
+ ? base.reasoning + snap.reasoningContent
465
+ : snap.reasoningContent;
466
+ }
214
467
  if (snap.toolCalls) placeholder.toolCalls = [...snap.toolCalls];
215
468
  this.emit('stream-progress', snap);
216
469
  if (onProgress) onProgress(snap);
217
470
  },
471
+ // === onDone:流结束,做最终落位 ===
218
472
  async (final) => {
219
473
  finalMsg = final;
220
474
 
475
+ // 工具调用钩子(仅普通发送时,续写不走 tool-calling 循环)
221
476
  if (this._processResponse && !mergeToEntry) {
222
477
  finalMsg = await this._processResponse(finalMsg, {
223
478
  isStream: true, onProgress, onDone
@@ -225,8 +480,9 @@ export class ChatService extends EventEmitter {
225
480
  }
226
481
 
227
482
  if (mergeToEntry && base) {
483
+ // 续写:把 base + 最终内容写回 target,标记完成
228
484
  const newContent = base.content + (final.content || '');
229
- const changes = { content: newContent, _complete: true };
485
+ const changes = { content: newContent, _complete: true, _ephemeral: false, prefix: undefined };
230
486
  if (final.reasoningContent) changes.reasoningContent = base.reasoning + final.reasoningContent;
231
487
  if (final.toolCalls) changes.toolCalls = final.toolCalls;
232
488
  this.messages.update(mergeToEntry, changes);
@@ -234,6 +490,7 @@ export class ChatService extends EventEmitter {
234
490
  this.emit('message', updated);
235
491
  if (onDone) onDone(updated);
236
492
  } else {
493
+ // 普通发送:标记占位消息完成
237
494
  placeholder._complete = true;
238
495
  this.emit('message', finalMsg);
239
496
  if (onDone) onDone(finalMsg);
@@ -241,6 +498,7 @@ export class ChatService extends EventEmitter {
241
498
  },
242
499
  adapterOptions
243
500
  );
501
+ // 返回续写目标 id 或最终消息(供 _withRetry 返回给调用方)
244
502
  return mergeToEntry || finalMsg;
245
503
  }
246
504
 
@@ -255,7 +513,9 @@ export class ChatService extends EventEmitter {
255
513
  }
256
514
  const changes = {
257
515
  content: (entry.content || '') + (assistantMsg.content || ''),
258
- _complete: true
516
+ _complete: true,
517
+ _ephemeral: false,
518
+ prefix: undefined
259
519
  };
260
520
  if (assistantMsg.reasoningContent) {
261
521
  changes.reasoningContent = (entry.reasoningContent || '') + assistantMsg.reasoningContent;
@@ -272,27 +532,121 @@ export class ChatService extends EventEmitter {
272
532
  }
273
533
 
274
534
  // ========== 公共 API ==========
275
- async send(userInput) {
535
+
536
+ /**
537
+ * 发送消息。
538
+ * @param {string|Object} userInput - 用户输入
539
+ * @param {Object} [params] - 请求级参数覆盖(model/temperature/modelParams 等,仅本次生效)
540
+ */
541
+ async send(userInput, params) {
276
542
  this._abortController = new AbortController();
277
- try { return await this._request({ userInput, addUser: true, isStream: false }); }
278
- finally { this._abortController = null; this._isGenerating = false; }
543
+ try {
544
+ return await this._request({ userInput, addUser: true, isStream: false, params });
545
+ } catch (err) {
546
+ if (err.name === 'AbortError') {
547
+ this.emit('aborted', { timestamp: Date.now() });
548
+ return;
549
+ }
550
+ throw err;
551
+ } finally {
552
+ this._abortController = null;
553
+ this._isGenerating = false;
554
+ }
279
555
  }
280
556
 
281
- async stream(userInput, onProgress, onDone) {
557
+ /**
558
+ * 流式发送。
559
+ * @param {string|Object} userInput - 用户输入
560
+ * @param {Object} [params] - 请求级参数覆盖(仅本次生效)
561
+ * @param {Function} [onProgress] - 流式进度回调
562
+ * @param {Function} [onDone] - 流式完成回调
563
+ */
564
+ async stream(userInput, params, onProgress, onDone) {
565
+ // 兼容旧签名 stream(input, onProgress, onDone)
566
+ if (typeof params === 'function') {
567
+ onDone = onProgress;
568
+ onProgress = params;
569
+ params = undefined;
570
+ }
282
571
  this._abortController = new AbortController();
283
- try { return await this._request({ userInput, addUser: true, isStream: true, onProgress, onDone }); }
284
- finally { this._abortController = null; this._isGenerating = false; }
572
+ try {
573
+ return await this._request({ userInput, addUser: true, isStream: true, onProgress, onDone, params });
574
+ } catch (err) {
575
+ if (err.name === 'AbortError') {
576
+ this.emit('aborted', { timestamp: Date.now() });
577
+ return;
578
+ }
579
+ throw err;
580
+ } finally {
581
+ this._abortController = null;
582
+ this._isGenerating = false;
583
+ }
285
584
  }
286
585
 
287
- async sendExisting() {
586
+ /**
587
+ * 重发当前消息(不加用户消息)。
588
+ * @param {Object} [params] - 请求级参数覆盖(仅本次生效)
589
+ */
590
+ async sendExisting(params) {
288
591
  this._abortController = new AbortController();
289
- try { return await this._request({ addUser: false, isStream: false }); }
290
- finally { this._abortController = null; this._isGenerating = false; }
592
+ try {
593
+ return await this._request({ addUser: false, isStream: false, params });
594
+ } catch (err) {
595
+ if (err.name === 'AbortError') {
596
+ this.emit('aborted', { timestamp: Date.now() });
597
+ return;
598
+ }
599
+ throw err;
600
+ } finally {
601
+ this._abortController = null;
602
+ this._isGenerating = false;
603
+ }
291
604
  }
292
605
 
293
- async sendExistingStream(onProgress, onDone) {
606
+ /**
607
+ * 流式重发(不加用户消息)。
608
+ * @param {Object} [params] - 请求级参数覆盖(仅本次生效)
609
+ * @param {Function} [onProgress] - 流式进度回调
610
+ * @param {Function} [onDone] - 流式完成回调
611
+ */
612
+ async sendExistingStream(params, onProgress, onDone) {
613
+ // 兼容旧签名 sendExistingStream(onProgress, onDone)
614
+ if (typeof params === 'function') {
615
+ onDone = onProgress;
616
+ onProgress = params;
617
+ params = undefined;
618
+ }
294
619
  this._abortController = new AbortController();
295
- try { return await this._request({ addUser: false, isStream: true, onProgress, onDone }); }
296
- finally { this._abortController = null; this._isGenerating = false; }
620
+ try {
621
+ return await this._request({ addUser: false, isStream: true, onProgress, onDone, params });
622
+ } catch (err) {
623
+ if (err.name === 'AbortError') {
624
+ this.emit('aborted', { timestamp: Date.now() });
625
+ return;
626
+ }
627
+ throw err;
628
+ } finally {
629
+ this._abortController = null;
630
+ this._isGenerating = false;
631
+ }
632
+ }
633
+ }
634
+
635
+ /**
636
+ * 校验 adapter 是否满足协议(v3.0 · 阶段 3)。
637
+ * 缺少必要方法时抛 ConfigurationError,并列出缺少的方法名——写"同级适配器"不再靠猜。
638
+ * @param {*} adapter
639
+ * @throws {ConfigurationError}
640
+ */
641
+ export function assertAdapter(adapter) {
642
+ if (!adapter || typeof adapter !== 'object') {
643
+ throw new ConfigurationError('setAdapter: adapter 必须是对象(如 openaiAdapter / createOpenAIAdapter() 实例)');
644
+ }
645
+ const required = ['buildRequest', 'send', 'stream', 'parseResponse'];
646
+ const missing = required.filter(k => typeof adapter[k] !== 'function');
647
+ if (missing.length) {
648
+ throw new ConfigurationError(
649
+ `setAdapter: adapter 缺少必要方法: ${missing.join(', ')}(协议见 ChatService.js 顶部的 @typedef ChatAdapter)`
650
+ );
297
651
  }
298
652
  }