my-ai-chat-framework 2.0.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,110 +1,652 @@
1
- import { EventEmitter } from './EventEmitter.js';
2
- import { MessageStore } from './MessageStore.js';
3
-
4
- /**
5
- * 核心聊天服务
6
- * 极简设计:只负责消息存储、事件发射、插件管理、请求委托
7
- */
8
- export class ChatService extends EventEmitter {
9
- constructor(config = {}) {
10
- super();
11
- this.config = {
12
- apiKey: config.apiKey || '',
13
- model: config.model || 'gpt-3.5-turbo',
14
- ...config
15
- };
16
- this.messages = new MessageStore();
17
- this._plugins = [];
18
- this._adapter = null; // 当前使用的 API 适配器
19
- this._pendingRequest = null;
20
- }
21
-
22
- /**
23
- * 加载插件
24
- * @param {object} plugin - 必须包含 install 方法
25
- */
26
- use(plugin) {
27
- if (typeof plugin.install === 'function') {
28
- plugin.install(this);
29
- this._plugins.push(plugin);
30
- } else {
31
- throw new Error('插件必须提供 install 方法');
32
- }
33
- return this;
34
- }
35
-
36
- /**
37
- * 设置 API 适配器
38
- */
39
- setAdapter(adapter) {
40
- this._adapter = adapter;
41
- return this;
42
- }
43
-
44
- /**
45
- * 发送消息(非流式)
46
- * @param {string|object} input - 字符串或消息对象
47
- */
48
- async send(input) {
49
- const userMsg = typeof input === 'string'
50
- ? { role: 'user', content: input }
51
- : { role: 'user', ...input };
52
-
53
- this.messages.add(userMsg);
54
- this.emit('sending', userMsg);
55
-
56
- // 构建请求
57
- const requestBody = this._adapter.buildRequest(this.messages.getAll(), this.config);
58
- const response = await this._adapter.send(requestBody, this.config);
59
- const assistantMsg = this._adapter.parseResponse(response);
60
-
61
- this.messages.add(assistantMsg);
62
- this.emit('message', assistantMsg);
63
- return assistantMsg;
64
- }
65
-
66
- /**
67
- * 流式发送消息
68
- * @param {string|object} input
69
- * @param {function} onProgress - 每次收到数据块时调用,参数为累积的当前消息对象
70
- * @param {function} onDone - 完成时调用,参数为最终消息对象
71
- */
72
- async stream(input, onProgress, onDone) {
73
- const userMsg = typeof input === 'string'
74
- ? { role: 'user', content: input }
75
- : { role: 'user', ...input };
76
-
77
- this.messages.add(userMsg);
78
- this.emit('sending', userMsg);
79
-
80
- const requestBody = this._adapter.buildRequest(this.messages.getAll(), this.config);
81
-
82
- let accumulated = { role: 'assistant', content: '' };
83
- await this._adapter.stream(
84
- requestBody,
85
- this.config,
86
- (chunk) => {
87
- // 累积消息
88
- if (chunk.content) accumulated.content += chunk.content;
89
- if (chunk.toolCalls) accumulated.toolCalls = chunk.toolCalls;
90
- if (chunk.reasoningContent) accumulated.reasoningContent = chunk.reasoningContent;
91
- // 触发进度事件
92
- this.emit('stream-progress', { ...accumulated });
93
- if (onProgress) onProgress({ ...accumulated });
94
- },
95
- (finalMessage) => {
96
- this.messages.add(finalMessage);
97
- this.emit('message', finalMessage);
98
- if (onDone) onDone(finalMessage);
99
- }
100
- );
101
- }
102
-
103
- /**
104
- * 注册工具(由工具调用插件实现)
105
- * 这里只留一个空方法,插件会覆盖它
106
- */
107
- registerTool(name, description, executor) {
108
- throw new Error('工具调用插件未加载,请先使用 use(toolCallingPlugin)');
109
- }
110
- }
1
+ import { EventEmitter } from './EventEmitter.js';
2
+ import { MessageStore } from './MessageStore.js';
3
+ import { SystemPromptStore } from './SystemPromptStore.js';
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'];
42
+
43
+ export class ChatService extends EventEmitter {
44
+ constructor(config = {}) {
45
+ super();
46
+ this.config = { ...config };
47
+ this.messages = new MessageStore();
48
+ this.systemPrompts = new SystemPromptStore();
49
+ this._adapter = null;
50
+ this._abortController = null;
51
+ this._isGenerating = false;
52
+ this._hooks = new EventEmitter();
53
+ this._processResponse = null;
54
+
55
+ const model = this.config.model || this.config.modelParams?.model;
56
+ if (!model || typeof model !== 'string' || !model.trim()) {
57
+ throw new ConfigurationError('缺少 model 配置');
58
+ }
59
+
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;
80
+ if (temperature !== undefined && (typeof temperature !== 'number' || temperature < 0 || temperature > 2)) {
81
+ throw new ConfigurationError(`temperature 必须在 0-2 之间,当前值: ${temperature}`);
82
+ }
83
+
84
+ const maxTokens = cfg.modelParams?.maxTokens ?? cfg.maxTokens;
85
+ if (maxTokens !== undefined && (typeof maxTokens !== 'number' || maxTokens < 1 || !Number.isInteger(maxTokens))) {
86
+ throw new ConfigurationError(`maxTokens 必须为正整数,当前值: ${maxTokens}`);
87
+ }
88
+
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(车间名,非空字符串)');
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;
129
+ }
130
+
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();
136
+ return this;
137
+ }
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
+
165
+ setAdapter(adapter) {
166
+ assertAdapter(adapter);
167
+ this._adapter = adapter;
168
+ }
169
+
170
+ abort() {
171
+ if (this._abortController) {
172
+ this._abortController.abort();
173
+ }
174
+ }
175
+
176
+ get isGenerating() { return this._isGenerating; }
177
+
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
+ }
197
+ }
198
+
199
+ async continueLastStream(onProgress, onDone) {
200
+ const target = this._prepareContinue();
201
+ this.messages.update(target.id, { prefix: true });
202
+ const baseLen = (target.content || '').length;
203
+
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;
227
+ }
228
+ throw err;
229
+ }
230
+ }
231
+
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,无法续写');
237
+ }
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
+ */
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);
259
+ Object.assign(this.config, partial);
260
+ if (typeof partial.system === 'string') this.systemPrompts.set(partial.system);
261
+ this.emit('config-updated', { changes: partial, timestamp: Date.now() });
262
+ }
263
+
264
+ // ========== 编排角色 ==========
265
+
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
+ }
286
+
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,
310
+ messages: this.messages,
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
347
+ });
348
+ }
349
+
350
+ /** 车间 3:自动续写检测(ephemeralContinue 时,底部若有 prefix 消息则续写) */
351
+ 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
+ }
359
+ }
360
+
361
+ /** 车间 4:构建请求体 */
362
+ async _stageBuildRequest(ctx) {
363
+ ctx.body = this._adapter.buildRequest(
364
+ ctx.messages.getAll(), ctx.config, ctx.systemPrompts.getEnabled()
365
+ );
366
+ }
367
+
368
+ /** 车间 5:发送(流式/非流式 + 重试 + 占位消息 + _processResponse 工具钩子) */
369
+ async _stageSend(ctx) {
370
+ const retryCfg = this.config.retry || {};
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
+ });
380
+ }
381
+
382
+ _addUserMessage(userInput, addUser) {
383
+ if (addUser && userInput !== undefined) {
384
+ const msg = typeof userInput === 'string'
385
+ ? { role: 'user', content: userInput }
386
+ : { role: 'user', ...userInput };
387
+ this.messages.add(msg);
388
+ }
389
+ }
390
+
391
+ /** retry 循环,占位消息只 push 一次 */
392
+ async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, config, maxRetries, retryDelay }) {
393
+ let lastError = null;
394
+ const adapterOptions = { signal: this._abortController?.signal };
395
+
396
+ let placeholder = null;
397
+ let base = null;
398
+ if (isStream) {
399
+ if (mergeToEntry) {
400
+ placeholder = this.messages.getAll().find(m => m.id === mergeToEntry);
401
+ if (placeholder) base = { content: placeholder.content || '', reasoning: placeholder.reasoningContent || '' };
402
+ }
403
+ if (!placeholder) {
404
+ placeholder = this.messages.add({ role: 'assistant', content: '', _complete: false });
405
+ }
406
+ }
407
+
408
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
409
+ if (attempt > 0) {
410
+ this.emit('retry', { attempt, maxRetries, lastError, timestamp: Date.now() });
411
+ await new Promise(r => setTimeout(r, retryDelay));
412
+ }
413
+ try {
414
+ this._isGenerating = true;
415
+ if (isStream) {
416
+ return await this._stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone, config });
417
+ } else {
418
+ 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;
425
+ }
426
+ } catch (error) {
427
+ lastError = error;
428
+ if (error.name === 'AbortError' || this._abortController?.signal.aborted) {
429
+ this._isGenerating = false; throw error;
430
+ }
431
+ if (error instanceof NetworkError && attempt < maxRetries) continue;
432
+ this._isGenerating = false; throw error;
433
+ }
434
+ }
435
+ this._isGenerating = false;
436
+ throw lastError;
437
+ }
438
+
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 }) {
450
+ let finalMsg = null;
451
+
452
+ await this._adapter.stream(body, config,
453
+ // === onProgress:每收到一个 SSE chunk 触发 ===
454
+ (snap) => {
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
+ }
467
+ if (snap.toolCalls) placeholder.toolCalls = [...snap.toolCalls];
468
+ this.emit('stream-progress', snap);
469
+ if (onProgress) onProgress(snap);
470
+ },
471
+ // === onDone:流结束,做最终落位 ===
472
+ async (final) => {
473
+ finalMsg = final;
474
+
475
+ // 工具调用钩子(仅普通发送时,续写不走 tool-calling 循环)
476
+ if (this._processResponse && !mergeToEntry) {
477
+ finalMsg = await this._processResponse(finalMsg, {
478
+ isStream: true, onProgress, onDone
479
+ });
480
+ }
481
+
482
+ if (mergeToEntry && base) {
483
+ // 续写:把 base + 最终内容写回 target,标记完成
484
+ const newContent = base.content + (final.content || '');
485
+ const changes = { content: newContent, _complete: true, _ephemeral: false, prefix: undefined };
486
+ if (final.reasoningContent) changes.reasoningContent = base.reasoning + final.reasoningContent;
487
+ if (final.toolCalls) changes.toolCalls = final.toolCalls;
488
+ this.messages.update(mergeToEntry, changes);
489
+ const updated = this.messages.getAll().find(m => m.id === mergeToEntry);
490
+ this.emit('message', updated);
491
+ if (onDone) onDone(updated);
492
+ } else {
493
+ // 普通发送:标记占位消息完成
494
+ placeholder._complete = true;
495
+ this.emit('message', finalMsg);
496
+ if (onDone) onDone(finalMsg);
497
+ }
498
+ },
499
+ adapterOptions
500
+ );
501
+ // 返回续写目标 id 或最终消息(供 _withRetry 返回给调用方)
502
+ return mergeToEntry || finalMsg;
503
+ }
504
+
505
+ /** 非流式结果落位 */
506
+ _handleResult(assistantMsg, mergeToEntry) {
507
+ if (mergeToEntry) {
508
+ const entry = this.messages.getAll().find(m => m.id === mergeToEntry);
509
+ if (!entry) {
510
+ this.messages.add(assistantMsg);
511
+ this.emit('message', assistantMsg);
512
+ return assistantMsg;
513
+ }
514
+ const changes = {
515
+ content: (entry.content || '') + (assistantMsg.content || ''),
516
+ _complete: true,
517
+ _ephemeral: false,
518
+ prefix: undefined
519
+ };
520
+ if (assistantMsg.reasoningContent) {
521
+ changes.reasoningContent = (entry.reasoningContent || '') + assistantMsg.reasoningContent;
522
+ }
523
+ if (assistantMsg.toolCalls) changes.toolCalls = assistantMsg.toolCalls;
524
+ this.messages.update(mergeToEntry, changes);
525
+ const updated = this.messages.getAll().find(m => m.id === mergeToEntry);
526
+ this.emit('message', updated);
527
+ return updated;
528
+ }
529
+ this.messages.add(assistantMsg);
530
+ this.emit('message', assistantMsg);
531
+ return assistantMsg;
532
+ }
533
+
534
+ // ========== 公共 API ==========
535
+
536
+ /**
537
+ * 发送消息。
538
+ * @param {string|Object} userInput - 用户输入
539
+ * @param {Object} [params] - 请求级参数覆盖(model/temperature/modelParams 等,仅本次生效)
540
+ */
541
+ async send(userInput, params) {
542
+ this._abortController = new AbortController();
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
+ }
555
+ }
556
+
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
+ }
571
+ this._abortController = new AbortController();
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
+ }
584
+ }
585
+
586
+ /**
587
+ * 重发当前消息(不加用户消息)。
588
+ * @param {Object} [params] - 请求级参数覆盖(仅本次生效)
589
+ */
590
+ async sendExisting(params) {
591
+ this._abortController = new AbortController();
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
+ }
604
+ }
605
+
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
+ }
619
+ this._abortController = new AbortController();
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
+ );
651
+ }
652
+ }