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.
- package/CHANGELOG.md +65 -0
- package/LICENSE +20 -20
- package/README.md +100 -13
- package/README_ZH.md +375 -0
- package/dist/my-ai-chat-framework.browser.es.js +758 -248
- package/dist/my-ai-chat-framework.browser.es.js.map +1 -1
- package/dist/my-ai-chat-framework.browser.umd.js +762 -247
- package/dist/my-ai-chat-framework.browser.umd.js.map +1 -1
- package/dist/my-ai-chat-framework.node.cjs.js +762 -247
- package/dist/my-ai-chat-framework.node.cjs.js.map +1 -1
- package/docs/DEVELOPER.md +479 -0
- package/package.json +31 -8
- package/src/adapters/openai.js +82 -7
- package/src/core/ChatService.js +415 -61
- package/src/core/Errors.js +59 -59
- package/src/core/MessageStore.js +27 -0
- package/src/core/Pipeline.js +48 -0
- package/src/core/SystemPromptStore.js +118 -118
- package/src/index.js +6 -4
- package/src/plugins/model-registry.js +223 -187
- package/src/plugins/tool-calling.js +215 -185
- package/src/utils/MessageFormatter.js +204 -204
- package/src/utils/typeCheck.js +11 -11
- package/src/utils/url.js +17 -17
package/src/core/ChatService.js
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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)
|
|
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:
|
|
184
|
+
_complete: true, prefix: undefined, _ephemeral: false
|
|
60
185
|
});
|
|
61
186
|
return target;
|
|
62
187
|
} catch (err) {
|
|
63
|
-
this.messages.update(target.id, {
|
|
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:
|
|
215
|
+
_complete: true, prefix: undefined, _ephemeral: false
|
|
86
216
|
});
|
|
87
217
|
return target;
|
|
88
218
|
} catch (err) {
|
|
89
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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,
|
|
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
|
-
/**
|
|
207
|
-
|
|
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,
|
|
452
|
+
await this._adapter.stream(body, config,
|
|
453
|
+
// === onProgress:每收到一个 SSE chunk 触发 ===
|
|
211
454
|
(snap) => {
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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 {
|
|
278
|
-
|
|
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
|
-
|
|
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 {
|
|
284
|
-
|
|
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
|
-
|
|
586
|
+
/**
|
|
587
|
+
* 重发当前消息(不加用户消息)。
|
|
588
|
+
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
589
|
+
*/
|
|
590
|
+
async sendExisting(params) {
|
|
288
591
|
this._abortController = new AbortController();
|
|
289
|
-
try {
|
|
290
|
-
|
|
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
|
-
|
|
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 {
|
|
296
|
-
|
|
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
|
}
|