my-ai-chat-framework 2.7.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.
- package/CHANGELOG.md +117 -0
- package/LICENSE +20 -20
- package/README.md +103 -16
- package/README_ZH.md +392 -0
- package/dist/my-ai-chat-framework.browser.es.js +1238 -421
- package/dist/my-ai-chat-framework.browser.es.js.map +1 -1
- package/dist/my-ai-chat-framework.browser.umd.js +1245 -420
- package/dist/my-ai-chat-framework.browser.umd.js.map +1 -1
- package/dist/my-ai-chat-framework.node.cjs.js +1245 -420
- package/dist/my-ai-chat-framework.node.cjs.js.map +1 -1
- package/docs/DEVELOPER.md +569 -0
- package/docs/README.md +33 -0
- package/package.json +31 -8
- package/src/adapters/openai.js +124 -23
- package/src/core/ChatService.js +596 -133
- package/src/core/Errors.js +72 -59
- package/src/core/MessageStore.js +27 -0
- package/src/core/Pipeline.js +48 -0
- package/src/core/SystemPromptStore.js +120 -118
- package/src/index.js +7 -4
- package/src/plugins/continuation.js +156 -0
- package/src/plugins/model-registry.js +236 -187
- package/src/plugins/tool-calling.js +245 -187
- package/src/utils/MessageFormatter.js +296 -204
- package/src/utils/typeCheck.js +11 -11
- package/src/utils/url.js +17 -17
package/src/core/ChatService.js
CHANGED
|
@@ -1,151 +1,498 @@
|
|
|
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
|
+
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'];
|
|
42
|
+
|
|
43
|
+
// 内部车间四步 + 发送(顺序即执行顺序);v3.1 起每一步都可以被 replaceStage 顶替、被 unstage 关闭
|
|
44
|
+
const INTERNAL_STAGES = ['prepareInput', 'beforeSend', 'autoContinue', 'buildRequest', 'send'];
|
|
5
45
|
|
|
6
46
|
export class ChatService extends EventEmitter {
|
|
7
47
|
constructor(config = {}) {
|
|
8
48
|
super();
|
|
9
49
|
this.config = { ...config };
|
|
10
|
-
|
|
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
|
+
}
|
|
11
59
|
this.systemPrompts = new SystemPromptStore();
|
|
12
60
|
this._adapter = null;
|
|
13
|
-
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
16
|
-
this._processResponse = null;
|
|
61
|
+
// v4.0(B1):每个请求各自持有 AbortController —— 多请求互不干扰(工具循环里再发请求也安全)
|
|
62
|
+
this._activeRequests = new Map(); // requestId → AbortController
|
|
63
|
+
this._requestSeq = 0;
|
|
17
64
|
|
|
18
65
|
const model = this.config.model || this.config.modelParams?.model;
|
|
19
66
|
if (!model || typeof model !== 'string' || !model.trim()) {
|
|
20
67
|
throw new ConfigurationError('缺少 model 配置');
|
|
21
68
|
}
|
|
22
69
|
|
|
23
|
-
|
|
70
|
+
this._assertValidConfig(this.config);
|
|
71
|
+
|
|
72
|
+
if (typeof config.system === 'string' && config.system.trim()) {
|
|
73
|
+
this.systemPrompts.set(config.system);
|
|
74
|
+
}
|
|
75
|
+
if (config.adapter) {
|
|
76
|
+
this.setAdapter(config.adapter);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ===== 内部管道(v3.0):内部车间 + 用户车间(pipe 注册)按 phase 插入 =====
|
|
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.<插件名> 下,互不撞名
|
|
85
|
+
this._pipeline = this._buildPipeline();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 校验配置中的可校验字段(model 必填由构造器负责,这里只校验存在性)
|
|
90
|
+
* @throws {ConfigurationError}
|
|
91
|
+
*/
|
|
92
|
+
_assertValidConfig(cfg) {
|
|
93
|
+
const temperature = cfg.modelParams?.temperature ?? cfg.temperature;
|
|
24
94
|
if (temperature !== undefined && (typeof temperature !== 'number' || temperature < 0 || temperature > 2)) {
|
|
25
95
|
throw new ConfigurationError(`temperature 必须在 0-2 之间,当前值: ${temperature}`);
|
|
26
96
|
}
|
|
27
97
|
|
|
28
|
-
const maxTokens =
|
|
98
|
+
const maxTokens = cfg.modelParams?.maxTokens ?? cfg.maxTokens;
|
|
29
99
|
if (maxTokens !== undefined && (typeof maxTokens !== 'number' || maxTokens < 1 || !Number.isInteger(maxTokens))) {
|
|
30
100
|
throw new ConfigurationError(`maxTokens 必须为正整数,当前值: ${maxTokens}`);
|
|
31
101
|
}
|
|
32
102
|
|
|
33
|
-
|
|
34
|
-
|
|
103
|
+
const model = cfg.model ?? cfg.modelParams?.model;
|
|
104
|
+
if (model !== undefined && (typeof model !== 'string' || !model.trim())) {
|
|
105
|
+
throw new ConfigurationError('model 必须是非空字符串');
|
|
35
106
|
}
|
|
36
107
|
}
|
|
37
108
|
|
|
38
|
-
|
|
39
|
-
|
|
109
|
+
/**
|
|
110
|
+
* 安装插件。
|
|
111
|
+
* - 插件实现 `install(chat, options)`
|
|
112
|
+
* - 若 install 返回一个对象,会挂到 **`chat.plugins[plugin.name]`**(v4.0 命名空间,插件之间不会撞名)
|
|
113
|
+
* - 插件要提供卸载时,自己在返回值里带 `uninstall()`(或调用 `plugin.uninstall(chat)`)
|
|
114
|
+
* @returns {ChatService} this
|
|
115
|
+
*/
|
|
116
|
+
use(plugin, options = {}) {
|
|
117
|
+
const api = plugin.install(this, options);
|
|
118
|
+
if (plugin.name && api && typeof api === 'object') {
|
|
119
|
+
this.plugins[plugin.name] = api;
|
|
120
|
+
}
|
|
40
121
|
return this;
|
|
41
122
|
}
|
|
42
123
|
|
|
124
|
+
// ========== 公开管道 API(v3.0 · 阶段 2)==========
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 往请求管道里挂一个"车间"。
|
|
128
|
+
* - phase 'beforeSend'(默认):在内部 beforeRequest 钩子之后、构建请求体之前执行
|
|
129
|
+
* - phase 'afterSend' :在内部发送(流式/非流式)完成、结果落位之后执行(可改 ctx.result)
|
|
130
|
+
* 不注册任何车间 = 行为与版本 2.8.x 完全一致。
|
|
131
|
+
* @param {PipeStage} stage
|
|
132
|
+
* @returns {ChatService} this
|
|
133
|
+
* @throws {ConfigurationError} 参数非法 / 名字被占用 / 与内部车间重名
|
|
134
|
+
*/
|
|
135
|
+
pipe(stage) {
|
|
136
|
+
if (!stage || typeof stage !== 'object' || typeof stage.run !== 'function') {
|
|
137
|
+
throw new ConfigurationError('pipe: 需要 { name, phase?, run(ctx) },run 必须是函数');
|
|
138
|
+
}
|
|
139
|
+
if (typeof stage.name !== 'string' || !stage.name.trim()) {
|
|
140
|
+
throw new ConfigurationError('pipe: 需要 name(车间名,非空字符串)');
|
|
141
|
+
}
|
|
142
|
+
if (INTERNAL_STAGES.includes(stage.name)) {
|
|
143
|
+
throw new ConfigurationError(`pipe: "${stage.name}" 是内部车间名,请换一个名字`);
|
|
144
|
+
}
|
|
145
|
+
if (this._userStages.some(s => s.name === stage.name)) {
|
|
146
|
+
throw new ConfigurationError(`pipe: 车间 "${stage.name}" 已存在,请先 chat.unpipe("${stage.name}")`);
|
|
147
|
+
}
|
|
148
|
+
const phase = stage.phase === 'afterSend' ? 'afterSend' : 'beforeSend';
|
|
149
|
+
this._userStages.push({ name: stage.name, phase, run: stage.run });
|
|
150
|
+
this._rebuildPipeline();
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 移除一个用户车间(移除不存在的名字是安全的)。@returns {ChatService} this */
|
|
155
|
+
unpipe(name) {
|
|
156
|
+
const before = this._userStages.length;
|
|
157
|
+
this._userStages = this._userStages.filter(s => s.name !== name);
|
|
158
|
+
if (this._userStages.length !== before) this._rebuildPipeline();
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 查看当前管道里所有车间名(调试/说明用,含内部车间) */
|
|
163
|
+
get pipelineStages() {
|
|
164
|
+
return this._pipeline.names();
|
|
165
|
+
}
|
|
166
|
+
|
|
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
|
+
*/
|
|
251
|
+
_buildPipeline() {
|
|
252
|
+
const p = new Pipeline();
|
|
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');
|
|
265
|
+
}
|
|
266
|
+
return p;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
_rebuildPipeline() {
|
|
270
|
+
this._pipeline = this._buildPipeline();
|
|
271
|
+
}
|
|
272
|
+
|
|
43
273
|
setAdapter(adapter) {
|
|
274
|
+
assertAdapter(adapter);
|
|
44
275
|
this._adapter = adapter;
|
|
45
276
|
}
|
|
46
277
|
|
|
47
|
-
|
|
48
|
-
|
|
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;
|
|
288
|
+
}
|
|
289
|
+
for (const controller of this._activeRequests.values()) controller.abort();
|
|
49
290
|
}
|
|
50
291
|
|
|
51
|
-
|
|
292
|
+
/** 是否有请求正在生成中 */
|
|
293
|
+
get isGenerating() { return this._activeRequests.size > 0; }
|
|
52
294
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
} catch (err) {
|
|
63
|
-
this.messages.update(target.id, { prefix: undefined });
|
|
64
|
-
throw err;
|
|
65
|
-
}
|
|
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 };
|
|
66
304
|
}
|
|
67
305
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const baseLen = (target.content || '').length;
|
|
306
|
+
/** 内部:注销一个请求 */
|
|
307
|
+
_endRequest(id) {
|
|
308
|
+
this._activeRequests.delete(id);
|
|
309
|
+
}
|
|
73
310
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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: 字段名必须是非空字符串');
|
|
326
|
+
}
|
|
327
|
+
if (validator != null && typeof validator !== 'function') {
|
|
328
|
+
throw new ConfigurationError(`registerConfigKeys: "${key}" 的校验器必须是函数`);
|
|
329
|
+
}
|
|
330
|
+
this._extraConfigKeys.set(key.trim(), validator || null);
|
|
91
331
|
}
|
|
332
|
+
return this;
|
|
92
333
|
}
|
|
93
334
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
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()];
|
|
99
344
|
}
|
|
100
345
|
|
|
346
|
+
/**
|
|
347
|
+
* 运行时修改配置(仅"可改字段":内置基础字段 + 插件注册字段)。
|
|
348
|
+
*
|
|
349
|
+
* - 运输层(apiKey/baseUrl/headers):归适配器,要改就换适配器实例
|
|
350
|
+
* - 请求参数默认值:走适配器工厂 options 或请求级覆盖(chat.send(x, params))
|
|
351
|
+
* - 插件自有配置:插件用 `registerConfigKeys` 登记后才可改(工厂 options 仍是首选)
|
|
352
|
+
*
|
|
353
|
+
* 与构造器一样经过校验,不能注入非法值。
|
|
354
|
+
* @throws {ConfigurationError} 非白名单字段、非法值、或插件校验器拒绝
|
|
355
|
+
*/
|
|
101
356
|
updateConfig(partial) {
|
|
357
|
+
const allowed = this.configKeys;
|
|
358
|
+
for (const key of Object.keys(partial)) {
|
|
359
|
+
if (!allowed.includes(key)) {
|
|
360
|
+
throw new ConfigurationError(
|
|
361
|
+
`updateConfig 不支持修改 "${key}"。允许的字段: ${allowed.join(', ')}`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
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
|
+
}
|
|
102
372
|
Object.assign(this.config, partial);
|
|
103
373
|
if (typeof partial.system === 'string') this.systemPrompts.set(partial.system);
|
|
104
374
|
this.emit('config-updated', { changes: partial, timestamp: Date.now() });
|
|
105
375
|
}
|
|
106
376
|
|
|
107
377
|
// ========== 编排角色 ==========
|
|
108
|
-
async _request(options) {
|
|
109
|
-
let { userInput, addUser = true, isStream = false, onProgress, onDone, mergeToEntry } = options;
|
|
110
|
-
|
|
111
|
-
this.emit('sending', { addUser, userInput, timestamp: Date.now() });
|
|
112
|
-
this._addUserMessage(userInput, addUser);
|
|
113
378
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
379
|
+
/**
|
|
380
|
+
* 合并请求参数(近者优先):
|
|
381
|
+
* 适配器默认(getRequestDefaults) ← 会话配置(this.config) ← 本次覆盖(params)
|
|
382
|
+
* modelParams 深层合并;平铺便捷键(temperature/maxTokens/reasoningEffort)自动折叠进 modelParams
|
|
383
|
+
*/
|
|
384
|
+
_mergeRequestConfig(params = {}) {
|
|
385
|
+
const adapterDefaults = this._adapter?.getRequestDefaults?.() || {};
|
|
386
|
+
const requestConfig = { ...adapterDefaults, ...this.config, ...params };
|
|
387
|
+
const modelParams = {
|
|
388
|
+
...(adapterDefaults.modelParams || {}),
|
|
389
|
+
...(this.config.modelParams || {}),
|
|
390
|
+
...(params.modelParams || {})
|
|
391
|
+
};
|
|
392
|
+
for (const key of ['temperature', 'maxTokens', 'reasoningEffort']) {
|
|
393
|
+
const value = params[key] ?? this.config[key] ?? adapterDefaults[key];
|
|
394
|
+
if (value !== undefined) modelParams[key] = value;
|
|
130
395
|
}
|
|
396
|
+
requestConfig.modelParams = modelParams;
|
|
397
|
+
return requestConfig;
|
|
398
|
+
}
|
|
131
399
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
400
|
+
/**
|
|
401
|
+
* 内部调度(v3.0 提案 · 阶段 1):造一辆小车 ctx,开过内部管道,返回结果。
|
|
402
|
+
* 行为与旧版 _request 完全一致,只是把固定线拆成了车间(见 _stage* 方法)。
|
|
403
|
+
*
|
|
404
|
+
* ctx 字段(车间可见):
|
|
405
|
+
* options —— 原始请求选项
|
|
406
|
+
* userInput —— 用户输入
|
|
407
|
+
* addUser —— 是否新增用户消息
|
|
408
|
+
* messages —— MessageStore(可读可改)
|
|
409
|
+
* systemPrompts —— SystemPromptStore
|
|
410
|
+
* config —— 合并后的请求级配置(prepareInput 车间产出)
|
|
411
|
+
* isStream —— 本次是否流式
|
|
412
|
+
* onProgress —— 流式进度回调(可为 null)
|
|
413
|
+
* onDone —— 流式完成回调(可为 null)
|
|
414
|
+
* mergeToEntry —— 续写目标 id(空 = 普通发送)
|
|
415
|
+
* body —— adapter 构建出的请求体(buildRequest 车间产出)
|
|
416
|
+
* result —— 最终结果(send 车间产出)
|
|
417
|
+
*/
|
|
418
|
+
async _request(options = {}) {
|
|
419
|
+
const ctx = {
|
|
420
|
+
options,
|
|
421
|
+
userInput: options.userInput,
|
|
422
|
+
addUser: options.addUser !== false,
|
|
423
|
+
messages: this.messages,
|
|
424
|
+
systemPrompts: this.systemPrompts,
|
|
425
|
+
config: null,
|
|
426
|
+
isStream: !!options.isStream,
|
|
427
|
+
onProgress: options.onProgress || null,
|
|
428
|
+
onDone: options.onDone || null,
|
|
429
|
+
mergeToEntry: options.mergeToEntry || null,
|
|
430
|
+
signal: options.signal || null, // v4.0(B1):本请求自己的中断信号
|
|
431
|
+
body: null,
|
|
432
|
+
result: null,
|
|
433
|
+
};
|
|
137
434
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
maxRetries: retryCfg.maxRetries ?? 0,
|
|
141
|
-
retryDelay: retryCfg.retryDelay ?? 1000
|
|
142
|
-
});
|
|
435
|
+
await this._pipeline.run(ctx);
|
|
436
|
+
return ctx.result;
|
|
143
437
|
} catch (error) {
|
|
438
|
+
// AbortError 不是异常,是用户主动行为,由公共 API 层处理
|
|
439
|
+
if (error.name === 'AbortError') throw error;
|
|
144
440
|
this.emit('error', { error, timestamp: Date.now() });
|
|
145
441
|
throw error;
|
|
146
442
|
}
|
|
147
443
|
}
|
|
148
444
|
|
|
445
|
+
// ===== 内部车间(阶段 1):与原 _request 的固定线 1:1 对应 =====
|
|
446
|
+
|
|
447
|
+
/** 车间 1:加用户消息 → 合并请求参数 → 广播 sending */
|
|
448
|
+
async _stagePrepareInput(ctx) {
|
|
449
|
+
this._addUserMessage(ctx.userInput, ctx.addUser);
|
|
450
|
+
// 合并请求参数(合并后的 ctx.config 供后续车间使用)
|
|
451
|
+
ctx.config = this._mergeRequestConfig(ctx.options.params);
|
|
452
|
+
this.emit('sending', { addUser: ctx.addUser, userInput: ctx.userInput, timestamp: Date.now() });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 车间 2:发送前 —— **v4.0 起默认什么都不做**。
|
|
457
|
+
* 需要在这里做事的(注入、改写消息/配置):`chat.replaceStage('beforeSend', fn)`
|
|
458
|
+
* 或挂一个 `chat.pipe({ phase: 'beforeSend' })` 车间(在它之后执行)。
|
|
459
|
+
* 旧的 `_hooks.on('beforeRequest')` 兼容桥已于 v4.0 移除。
|
|
460
|
+
*/
|
|
461
|
+
async _stageBeforeSend(ctx) {
|
|
462
|
+
// 默认空实现(no-op)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* 车间 3:自动续写检测 —— **v4.0 起默认什么都不做**。
|
|
467
|
+
* 续写是玩法:需要它的装上 continuation 插件,由插件 replaceStage('autoContinue', ...) 接管。
|
|
468
|
+
* 步骤名与位置保留,方便插件替换、也方便使用者 `unstage('autoContinue')` 明确移除。
|
|
469
|
+
*/
|
|
470
|
+
async _stageAutoContinue(ctx) {
|
|
471
|
+
// 默认空实现(no-op)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** 车间 4:构建请求体 */
|
|
475
|
+
async _stageBuildRequest(ctx) {
|
|
476
|
+
ctx.body = this._adapter.buildRequest(
|
|
477
|
+
ctx.messages.getAll(), ctx.config, ctx.systemPrompts.getEnabled()
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** 车间 5:发送(流式/非流式 + 重试 + 占位消息 + _processResponse 工具钩子) */
|
|
482
|
+
async _stageSend(ctx) {
|
|
483
|
+
const retryCfg = this.config.retry || {};
|
|
484
|
+
ctx.result = await this._withRetry(ctx.body, {
|
|
485
|
+
isStream: ctx.isStream,
|
|
486
|
+
onProgress: ctx.onProgress,
|
|
487
|
+
onDone: ctx.onDone,
|
|
488
|
+
mergeToEntry: ctx.mergeToEntry,
|
|
489
|
+
config: ctx.config,
|
|
490
|
+
maxRetries: retryCfg.maxRetries ?? 0,
|
|
491
|
+
retryDelay: retryCfg.retryDelay ?? 1000,
|
|
492
|
+
signal: ctx.signal
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
149
496
|
_addUserMessage(userInput, addUser) {
|
|
150
497
|
if (addUser && userInput !== undefined) {
|
|
151
498
|
const msg = typeof userInput === 'string'
|
|
@@ -156,9 +503,10 @@ export class ChatService extends EventEmitter {
|
|
|
156
503
|
}
|
|
157
504
|
|
|
158
505
|
/** retry 循环,占位消息只 push 一次 */
|
|
159
|
-
async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, maxRetries, retryDelay }) {
|
|
506
|
+
async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, config, maxRetries, retryDelay, signal }) {
|
|
160
507
|
let lastError = null;
|
|
161
|
-
|
|
508
|
+
// v4.0(B1):使用"本请求自己的"信号(不再读写共享的 this._abortController)
|
|
509
|
+
const adapterOptions = { signal };
|
|
162
510
|
|
|
163
511
|
let placeholder = null;
|
|
164
512
|
let base = null;
|
|
@@ -178,55 +526,66 @@ export class ChatService extends EventEmitter {
|
|
|
178
526
|
await new Promise(r => setTimeout(r, retryDelay));
|
|
179
527
|
}
|
|
180
528
|
try {
|
|
181
|
-
this._isGenerating = true;
|
|
182
529
|
if (isStream) {
|
|
183
|
-
return await this._stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone });
|
|
530
|
+
return await this._stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone, config });
|
|
184
531
|
} else {
|
|
185
|
-
const resp = await this._adapter.send(body,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (this._processResponse && !mergeToEntry) {
|
|
189
|
-
result = await this._processResponse(result, { isStream: false });
|
|
190
|
-
}
|
|
191
|
-
return result;
|
|
532
|
+
const resp = await this._adapter.send(body, config, adapterOptions);
|
|
533
|
+
// 响应落位后交给管道:afterSend 车间接管后续处理(如工具调用循环)
|
|
534
|
+
return this._handleResult(this._adapter.parseResponse(resp), mergeToEntry);
|
|
192
535
|
}
|
|
193
536
|
} catch (error) {
|
|
194
537
|
lastError = error;
|
|
195
|
-
if (error.name === 'AbortError' ||
|
|
196
|
-
|
|
538
|
+
if (error.name === 'AbortError' || signal?.aborted) {
|
|
539
|
+
throw error;
|
|
197
540
|
}
|
|
198
541
|
if (error instanceof NetworkError && attempt < maxRetries) continue;
|
|
199
|
-
|
|
542
|
+
throw error;
|
|
200
543
|
}
|
|
201
544
|
}
|
|
202
|
-
this._isGenerating = false;
|
|
203
545
|
throw lastError;
|
|
204
546
|
}
|
|
205
547
|
|
|
206
|
-
/**
|
|
207
|
-
|
|
548
|
+
/**
|
|
549
|
+
* 流式处理:ChatService 维护占位消息,adapter 只负责解析 SSE
|
|
550
|
+
*
|
|
551
|
+
* 参数说明:
|
|
552
|
+
* - placeholder:流式期间实时更新的目标消息(续写时 = target 本身,普通发送时 = 新建的空消息)
|
|
553
|
+
* - base:续写时保存的旧内容快照,用于和 API 新内容拼接
|
|
554
|
+
* - mergeToEntry:续写目标的 id,有值时走续写逻辑
|
|
555
|
+
* - onProgress:外部回调,每收到一个 chunk 触发
|
|
556
|
+
* - onDone:流结束回调
|
|
557
|
+
*/
|
|
558
|
+
async _stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone, config }) {
|
|
208
559
|
let finalMsg = null;
|
|
209
560
|
|
|
210
|
-
await this._adapter.stream(body,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
561
|
+
await this._adapter.stream(body, config,
|
|
562
|
+
// === onProgress:每收到一个 SSE chunk 触发 ===
|
|
563
|
+
// snap = 全量累积快照;delta = 本次新增片段(v4.0 起两个都给你)
|
|
564
|
+
(snap, delta) => {
|
|
565
|
+
// snap.content 是 adapter 累积的全部 API 新内容(不是增量)
|
|
566
|
+
// 续写时:base.content(旧内容)+ snap.content(API 新内容)= 完整内容
|
|
567
|
+
// 普通发送时:直接用 snap.content
|
|
568
|
+
placeholder.content = mergeToEntry && base
|
|
569
|
+
? base.content + (snap.content || '')
|
|
570
|
+
: (snap.content || '');
|
|
571
|
+
// 思维链同样拼接:续写时 base 旧思维 + 新思维
|
|
572
|
+
if (snap.reasoningContent) {
|
|
573
|
+
placeholder.reasoningContent = mergeToEntry && base
|
|
574
|
+
? base.reasoning + snap.reasoningContent
|
|
575
|
+
: snap.reasoningContent;
|
|
576
|
+
}
|
|
214
577
|
if (snap.toolCalls) placeholder.toolCalls = [...snap.toolCalls];
|
|
215
578
|
this.emit('stream-progress', snap);
|
|
216
|
-
if (onProgress) onProgress(snap);
|
|
579
|
+
if (onProgress) onProgress(snap, delta); // v4.0:第二个参数是本次增量
|
|
217
580
|
},
|
|
581
|
+
// === onDone:流结束,做最终落位 ===
|
|
218
582
|
async (final) => {
|
|
219
583
|
finalMsg = final;
|
|
220
584
|
|
|
221
|
-
if (this._processResponse && !mergeToEntry) {
|
|
222
|
-
finalMsg = await this._processResponse(finalMsg, {
|
|
223
|
-
isStream: true, onProgress, onDone
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
585
|
if (mergeToEntry && base) {
|
|
586
|
+
// 续写:把 base + 最终内容写回 target,标记完成
|
|
228
587
|
const newContent = base.content + (final.content || '');
|
|
229
|
-
const changes = { content: newContent, _complete: true };
|
|
588
|
+
const changes = { content: newContent, _complete: true, _ephemeral: false, prefix: undefined };
|
|
230
589
|
if (final.reasoningContent) changes.reasoningContent = base.reasoning + final.reasoningContent;
|
|
231
590
|
if (final.toolCalls) changes.toolCalls = final.toolCalls;
|
|
232
591
|
this.messages.update(mergeToEntry, changes);
|
|
@@ -234,6 +593,7 @@ export class ChatService extends EventEmitter {
|
|
|
234
593
|
this.emit('message', updated);
|
|
235
594
|
if (onDone) onDone(updated);
|
|
236
595
|
} else {
|
|
596
|
+
// 普通发送:标记占位消息完成
|
|
237
597
|
placeholder._complete = true;
|
|
238
598
|
this.emit('message', finalMsg);
|
|
239
599
|
if (onDone) onDone(finalMsg);
|
|
@@ -241,6 +601,7 @@ export class ChatService extends EventEmitter {
|
|
|
241
601
|
},
|
|
242
602
|
adapterOptions
|
|
243
603
|
);
|
|
604
|
+
// 返回续写目标 id 或最终消息(供 _withRetry 返回给调用方)
|
|
244
605
|
return mergeToEntry || finalMsg;
|
|
245
606
|
}
|
|
246
607
|
|
|
@@ -255,7 +616,9 @@ export class ChatService extends EventEmitter {
|
|
|
255
616
|
}
|
|
256
617
|
const changes = {
|
|
257
618
|
content: (entry.content || '') + (assistantMsg.content || ''),
|
|
258
|
-
_complete: true
|
|
619
|
+
_complete: true,
|
|
620
|
+
_ephemeral: false,
|
|
621
|
+
prefix: undefined
|
|
259
622
|
};
|
|
260
623
|
if (assistantMsg.reasoningContent) {
|
|
261
624
|
changes.reasoningContent = (entry.reasoningContent || '') + assistantMsg.reasoningContent;
|
|
@@ -272,27 +635,127 @@ export class ChatService extends EventEmitter {
|
|
|
272
635
|
}
|
|
273
636
|
|
|
274
637
|
// ========== 公共 API ==========
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* 发送消息。
|
|
641
|
+
* @param {string|Object} userInput - 用户输入
|
|
642
|
+
* @param {Object} [params] - 请求级参数覆盖(model/temperature/modelParams 等,仅本次生效)
|
|
643
|
+
*/
|
|
644
|
+
async send(userInput, params) {
|
|
645
|
+
const { id, signal } = this._beginRequest();
|
|
646
|
+
try {
|
|
647
|
+
return await this._request({ userInput, addUser: true, isStream: false, params, signal });
|
|
648
|
+
} catch (err) {
|
|
649
|
+
if (err.name === 'AbortError') {
|
|
650
|
+
this.emit('aborted', { timestamp: Date.now() });
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
throw err;
|
|
654
|
+
} finally {
|
|
655
|
+
this._endRequest(id);
|
|
656
|
+
}
|
|
279
657
|
}
|
|
280
658
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
659
|
+
/**
|
|
660
|
+
* 流式发送。
|
|
661
|
+
* @param {string|Object} userInput - 用户输入
|
|
662
|
+
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
663
|
+
* @param {Function} [onProgress] - 流式进度回调
|
|
664
|
+
* @param {Function} [onDone] - 流式完成回调
|
|
665
|
+
*/
|
|
666
|
+
async stream(userInput, params, onProgress, onDone) {
|
|
667
|
+
// 兼容旧签名 stream(input, onProgress, onDone)
|
|
668
|
+
if (typeof params === 'function') {
|
|
669
|
+
onDone = onProgress;
|
|
670
|
+
onProgress = params;
|
|
671
|
+
params = undefined;
|
|
672
|
+
}
|
|
673
|
+
const { id, signal } = this._beginRequest();
|
|
674
|
+
try {
|
|
675
|
+
return await this._request({ userInput, addUser: true, isStream: true, onProgress, onDone, params, signal });
|
|
676
|
+
} catch (err) {
|
|
677
|
+
if (err.name === 'AbortError') {
|
|
678
|
+
this.emit('aborted', { timestamp: Date.now() });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
throw err;
|
|
682
|
+
} finally {
|
|
683
|
+
this._endRequest(id);
|
|
684
|
+
}
|
|
285
685
|
}
|
|
286
686
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
687
|
+
/**
|
|
688
|
+
* 重发当前消息(不加用户消息)。
|
|
689
|
+
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
690
|
+
* @param {Object} [options] - { mergeToEntry } 可选:把结果合并进指定的已有消息(续写等场景)
|
|
691
|
+
*/
|
|
692
|
+
async sendExisting(params, options = {}) {
|
|
693
|
+
const mergeToEntry = options?.mergeToEntry || null;
|
|
694
|
+
const { id, signal } = this._beginRequest();
|
|
695
|
+
try {
|
|
696
|
+
return await this._request({ addUser: false, isStream: false, params, mergeToEntry, signal });
|
|
697
|
+
} catch (err) {
|
|
698
|
+
if (err.name === 'AbortError') {
|
|
699
|
+
this.emit('aborted', { timestamp: Date.now() });
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
throw err;
|
|
703
|
+
} finally {
|
|
704
|
+
this._endRequest(id);
|
|
705
|
+
}
|
|
291
706
|
}
|
|
292
707
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
708
|
+
/**
|
|
709
|
+
* 流式重发(不加用户消息)。
|
|
710
|
+
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
711
|
+
* @param {Object} [options] - { mergeToEntry } 可选:把结果合并进指定的已有消息
|
|
712
|
+
* @param {Function} [onProgress] - 流式进度回调
|
|
713
|
+
* @param {Function} [onDone] - 流式完成回调
|
|
714
|
+
*/
|
|
715
|
+
async sendExistingStream(params, options, onProgress, onDone) {
|
|
716
|
+
// 兼容签名1:sendExistingStream(onProgress, onDone)
|
|
717
|
+
if (typeof params === 'function') {
|
|
718
|
+
onDone = options;
|
|
719
|
+
onProgress = params;
|
|
720
|
+
options = {};
|
|
721
|
+
params = undefined;
|
|
722
|
+
} else if (typeof options === 'function') {
|
|
723
|
+
// 兼容签名2:sendExistingStream(params, onProgress, onDone)
|
|
724
|
+
onDone = onProgress;
|
|
725
|
+
onProgress = options;
|
|
726
|
+
options = {};
|
|
727
|
+
}
|
|
728
|
+
const mergeToEntry = options?.mergeToEntry || null;
|
|
729
|
+
const { id, signal } = this._beginRequest();
|
|
730
|
+
try {
|
|
731
|
+
return await this._request({ addUser: false, isStream: true, onProgress, onDone, params, mergeToEntry, signal });
|
|
732
|
+
} catch (err) {
|
|
733
|
+
if (err.name === 'AbortError') {
|
|
734
|
+
this.emit('aborted', { timestamp: Date.now() });
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
throw err;
|
|
738
|
+
} finally {
|
|
739
|
+
this._endRequest(id);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* 校验 adapter 是否满足协议(v3.0 · 阶段 3)。
|
|
746
|
+
* 缺少必要方法时抛 ConfigurationError,并列出缺少的方法名——写"同级适配器"不再靠猜。
|
|
747
|
+
* @param {*} adapter
|
|
748
|
+
* @throws {ConfigurationError}
|
|
749
|
+
*/
|
|
750
|
+
export function assertAdapter(adapter) {
|
|
751
|
+
if (!adapter || typeof adapter !== 'object') {
|
|
752
|
+
throw new ConfigurationError('setAdapter: adapter 必须是对象(如 openaiAdapter / createOpenAIAdapter() 实例)');
|
|
753
|
+
}
|
|
754
|
+
const required = ['buildRequest', 'send', 'stream', 'parseResponse'];
|
|
755
|
+
const missing = required.filter(k => typeof adapter[k] !== 'function');
|
|
756
|
+
if (missing.length) {
|
|
757
|
+
throw new ConfigurationError(
|
|
758
|
+
`setAdapter: adapter 缺少必要方法: ${missing.join(', ')}(协议见 ChatService.js 顶部的 @typedef ChatAdapter)`
|
|
759
|
+
);
|
|
297
760
|
}
|
|
298
761
|
}
|