my-ai-chat-framework 2.0.0 → 2.7.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,10 +1,8 @@
1
- Object.defineProperties(exports, {
2
- __esModule: { value: true },
3
- [Symbol.toStringTag]: { value: "Module" }
4
- });
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
5
2
  //#region src/core/EventEmitter.js
6
3
  /**
7
4
  * 极简事件发射器
5
+ * 支持同步/异步事件,handler 返回 Promise 时自动 await
8
6
  */
9
7
  var EventEmitter = class {
10
8
  constructor() {
@@ -21,6 +19,9 @@ var EventEmitter = class {
21
19
  if (handlers.length) this._events.set(event, handlers);
22
20
  else this._events.delete(event);
23
21
  }
22
+ /**
23
+ * 同步触发事件
24
+ */
24
25
  emit(event, data) {
25
26
  if (!this._events.has(event)) return;
26
27
  for (const handler of this._events.get(event)) try {
@@ -29,6 +30,18 @@ var EventEmitter = class {
29
30
  console.error(`事件 ${event} 处理出错:`, err);
30
31
  }
31
32
  }
33
+ /**
34
+ * 异步触发事件,逐个 await handler
35
+ * handler 返回 Promise 时自动等待
36
+ */
37
+ async emitAsync(event, data) {
38
+ if (!this._events.has(event)) return;
39
+ for (const handler of this._events.get(event)) try {
40
+ await handler(data);
41
+ } catch (err) {
42
+ console.error(`事件 ${event} 处理出错:`, err);
43
+ }
44
+ }
32
45
  };
33
46
  //#endregion
34
47
  //#region src/core/MessageStore.js
@@ -39,6 +52,10 @@ var EventEmitter = class {
39
52
  * id?: string,
40
53
  * role: 'user'|'assistant'|'system'|'tool',
41
54
  * content: string,
55
+ * images?: Array<string>, // 图片引用(URL / id),框架不存储图片数据
56
+ * _ephemeral?: boolean, // 临时消息:仅底部连续时参与请求,续写后转正
57
+ * _complete?: boolean, // 流式是否完成(false=未完成/中断)
58
+ * prefix?: boolean, // 前缀续写标记
42
59
  * toolCalls?: Array,
43
60
  * toolCallId?: string,
44
61
  * timestamp?: number,
@@ -88,6 +105,22 @@ var MessageStore = class {
88
105
  metadata
89
106
  });
90
107
  }
108
+ /**
109
+ * 快速添加一次性的、带续写标记的 assistant 消息
110
+ * 适用于思维链引导、临时注入等场景
111
+ * @param {string} content — 引导内容
112
+ * @param {Object} [options] — { reasoningContent, prefix }
113
+ * @returns {Object} 添加的消息
114
+ */
115
+ addOnceAssistant(content, options = {}) {
116
+ return this.add({
117
+ role: "assistant",
118
+ content,
119
+ reasoningContent: options.reasoningContent,
120
+ prefix: options.prefix !== false,
121
+ _ephemeral: true
122
+ });
123
+ }
91
124
  getLast() {
92
125
  return this._messages[this._messages.length - 1] || null;
93
126
  }
@@ -106,106 +139,667 @@ var MessageStore = class {
106
139
  if (lastAssistantIndex >= 0) return this._messages.splice(lastAssistantIndex + 1);
107
140
  return [];
108
141
  }
142
+ /**
143
+ * 更新消息:按 id 查找并合并 changes,不新增消息
144
+ * @param {string} id — 消息 id
145
+ * @param {Object} changes — 要合并的字段
146
+ * @returns {Object|null} 更新后的消息,未找到返回 null
147
+ */
148
+ update(id, changes) {
149
+ for (const msg of this._messages) if (msg.id === id) {
150
+ Object.assign(msg, changes);
151
+ msg.timestamp = changes.timestamp || Date.now();
152
+ return msg;
153
+ }
154
+ return null;
155
+ }
109
156
  };
110
157
  //#endregion
111
- //#region src/core/ChatService.js
158
+ //#region src/core/SystemPromptStore.js
112
159
  /**
113
- * 核心聊天服务
114
- * 极简设计:只负责消息存储、事件发射、插件管理、请求委托
160
+ * SystemPromptStore — 系统提示词存储
161
+ *
162
+ * 职责:管理多条 system prompt 的增删改查与开关
163
+ * 与 MessageStore 分离,因为 system prompt 是"AI 行为准则",不是对话事件
164
+ *
165
+ * 每条记录格式:
166
+ * {
167
+ * id: string,
168
+ * content: string,
169
+ * enabled: boolean,
170
+ * timestamp: number
171
+ * }
115
172
  */
173
+ var SystemPromptStore = class {
174
+ constructor() {
175
+ this._prompts = [];
176
+ this._idCounter = 0;
177
+ }
178
+ /**
179
+ * 添加一条 system prompt
180
+ * @param {string} content — 提示词内容
181
+ * @param {boolean} [enabled=true] — 是否启用
182
+ * @returns {Object} 添加的记录
183
+ */
184
+ add(content, enabled = true) {
185
+ if (!content || typeof content !== "string" || !content.trim()) throw new Error("[SystemPromptStore] content 必须是非空字符串");
186
+ const record = {
187
+ id: `sys_${Date.now()}_${++this._idCounter}`,
188
+ content: content.trim(),
189
+ enabled: Boolean(enabled),
190
+ timestamp: Date.now()
191
+ };
192
+ this._prompts.push(record);
193
+ return record;
194
+ }
195
+ /**
196
+ * 删除指定索引的 system prompt
197
+ * @param {number} index
198
+ * @returns {Object} 被删除的记录
199
+ */
200
+ remove(index) {
201
+ if (index < 0 || index >= this._prompts.length) throw new Error(`[SystemPromptStore] 索引越界: ${index}`);
202
+ return this._prompts.splice(index, 1)[0];
203
+ }
204
+ /**
205
+ * 切换指定索引的启用/禁用状态
206
+ * @param {number} index
207
+ * @returns {boolean} 切换后的状态
208
+ */
209
+ toggle(index) {
210
+ if (index < 0 || index >= this._prompts.length) throw new Error(`[SystemPromptStore] 索引越界: ${index}`);
211
+ this._prompts[index].enabled = !this._prompts[index].enabled;
212
+ return this._prompts[index].enabled;
213
+ }
214
+ /**
215
+ * 更新指定索引的 content
216
+ * @param {number} index
217
+ * @param {string} content
218
+ */
219
+ update(index, content) {
220
+ if (index < 0 || index >= this._prompts.length) throw new Error(`[SystemPromptStore] 索引越界: ${index}`);
221
+ if (!content || typeof content !== "string" || !content.trim()) throw new Error("[SystemPromptStore] content 必须是非空字符串");
222
+ this._prompts[index].content = content.trim();
223
+ this._prompts[index].timestamp = Date.now();
224
+ }
225
+ /**
226
+ * 清空并用一条 content 替换(便捷方法,常用于 config.system = '...')
227
+ * @param {string} content
228
+ */
229
+ set(content) {
230
+ this._prompts = [];
231
+ if (content && typeof content === "string" && content.trim()) this.add(content, true);
232
+ }
233
+ /**
234
+ * 按 enabled 筛选后,转为适配器可用的格式
235
+ * @returns {Array<{role:'system', content:string}>}
236
+ */
237
+ getEnabled() {
238
+ return this._prompts.filter((p) => p.enabled).map((p) => ({
239
+ role: "system",
240
+ content: p.content
241
+ }));
242
+ }
243
+ /**
244
+ * 返回所有记录(含 enabled 状态,用于 UI 展示)
245
+ * @returns {Array}
246
+ */
247
+ getAll() {
248
+ return [...this._prompts];
249
+ }
250
+ /**
251
+ * 清空所有 system prompt
252
+ */
253
+ clear() {
254
+ this._prompts = [];
255
+ }
256
+ };
257
+ //#endregion
258
+ //#region src/core/Errors.js
259
+ /**
260
+ * 自定义错误类
261
+ * 用于区分不同类型的错误,方便用户通过 `error.name` 或 `instanceof` 处理
262
+ */
263
+ var APIError = class extends Error {
264
+ /**
265
+ * @param {string} message - 错误消息(通常来自 API 响应)
266
+ * @param {number} statusCode - HTTP 状态码
267
+ * @param {any} originalError - 原始错误对象或相关信息
268
+ * @param {string} responseText - 原始响应文本(如果有)
269
+ */
270
+ constructor(message, statusCode, originalError, responseText) {
271
+ super(message);
272
+ this.name = "APIError";
273
+ this.statusCode = statusCode;
274
+ this.originalError = originalError;
275
+ this.responseText = responseText;
276
+ }
277
+ };
278
+ var NetworkError = class extends Error {
279
+ /**
280
+ * @param {string} message - 错误消息
281
+ * @param {any} originalError - 原始错误对象或相关信息
282
+ */
283
+ constructor(message, originalError) {
284
+ super(message);
285
+ this.name = "NetworkError";
286
+ this.originalError = originalError;
287
+ }
288
+ };
289
+ var ConfigurationError = class extends Error {
290
+ /**
291
+ * @param {string} message - 错误消息
292
+ */
293
+ constructor(message) {
294
+ super(message);
295
+ this.name = "ConfigurationError";
296
+ }
297
+ };
298
+ var ParsingError = class extends Error {
299
+ /**
300
+ * @param {string} message - 错误消息
301
+ * @param {any} originalError - 原始错误对象或相关信息
302
+ * @param {string} responseText - 原始响应文本(如果有)
303
+ **/
304
+ constructor(message, originalError, responseText) {
305
+ super(message);
306
+ this.name = "ParsingError";
307
+ this.originalError = originalError;
308
+ this.responseText = responseText;
309
+ }
310
+ };
311
+ //#endregion
312
+ //#region src/core/ChatService.js
116
313
  var ChatService = class extends EventEmitter {
117
314
  constructor(config = {}) {
118
315
  super();
119
- this.config = {
120
- apiKey: config.apiKey || "",
121
- model: config.model || "gpt-3.5-turbo",
122
- ...config
123
- };
316
+ this.config = { ...config };
124
317
  this.messages = new MessageStore();
125
- this._plugins = [];
318
+ this.systemPrompts = new SystemPromptStore();
126
319
  this._adapter = null;
127
- this._pendingRequest = null;
320
+ this._abortController = null;
321
+ this._isGenerating = false;
322
+ this._hooks = new EventEmitter();
323
+ this._processResponse = null;
324
+ const model = this.config.model || this.config.modelParams?.model;
325
+ if (!model || typeof model !== "string" || !model.trim()) throw new ConfigurationError("缺少 model 配置");
326
+ const temperature = this.config.modelParams?.temperature ?? this.config.temperature;
327
+ if (temperature !== void 0 && (typeof temperature !== "number" || temperature < 0 || temperature > 2)) throw new ConfigurationError(`temperature 必须在 0-2 之间,当前值: ${temperature}`);
328
+ const maxTokens = this.config.modelParams?.maxTokens ?? this.config.maxTokens;
329
+ if (maxTokens !== void 0 && (typeof maxTokens !== "number" || maxTokens < 1 || !Number.isInteger(maxTokens))) throw new ConfigurationError(`maxTokens 必须为正整数,当前值: ${maxTokens}`);
330
+ if (typeof config.system === "string" && config.system.trim()) this.systemPrompts.set(config.system);
128
331
  }
129
- /**
130
- * 加载插件
131
- * @param {object} plugin - 必须包含 install 方法
132
- */
133
332
  use(plugin) {
134
- if (typeof plugin.install === "function") {
135
- plugin.install(this);
136
- this._plugins.push(plugin);
137
- } else throw new Error("插件必须提供 install 方法");
333
+ plugin.install(this);
138
334
  return this;
139
335
  }
140
- /**
141
- * 设置 API 适配器
142
- */
143
336
  setAdapter(adapter) {
144
337
  this._adapter = adapter;
145
- return this;
146
338
  }
147
- /**
148
- * 发送消息(非流式)
149
- * @param {string|object} input - 字符串或消息对象
150
- */
151
- async send(input) {
152
- const userMsg = typeof input === "string" ? {
153
- role: "user",
154
- content: input
155
- } : {
156
- role: "user",
157
- ...input
158
- };
159
- this.messages.add(userMsg);
160
- this.emit("sending", userMsg);
161
- const requestBody = this._adapter.buildRequest(this.messages.getAll(), this.config);
162
- const response = await this._adapter.send(requestBody, this.config);
163
- const assistantMsg = this._adapter.parseResponse(response);
339
+ abort() {
340
+ if (this._abortController) this._abortController.abort();
341
+ }
342
+ get isGenerating() {
343
+ return this._isGenerating;
344
+ }
345
+ async continueLast() {
346
+ const target = this._prepareContinue();
347
+ this.messages.update(target.id, { prefix: true });
348
+ try {
349
+ await this._request({
350
+ addUser: false,
351
+ isStream: false,
352
+ mergeToEntry: target.id
353
+ });
354
+ this.messages.update(target.id, {
355
+ _complete: true,
356
+ prefix: void 0,
357
+ _ephemeral: void 0
358
+ });
359
+ return target;
360
+ } catch (err) {
361
+ this.messages.update(target.id, { prefix: void 0 });
362
+ throw err;
363
+ }
364
+ }
365
+ async continueLastStream(onProgress, onDone) {
366
+ const target = this._prepareContinue();
367
+ console.log("[DEBUG] continueLastStream target.id:", target.id);
368
+ this.messages.update(target.id, { prefix: true });
369
+ const baseLen = (target.content || "").length;
370
+ try {
371
+ await this._request({
372
+ addUser: false,
373
+ isStream: true,
374
+ mergeToEntry: target.id,
375
+ onProgress: (chunk) => {
376
+ if (onProgress) onProgress({
377
+ ...chunk,
378
+ content: (chunk.content || "").slice(baseLen)
379
+ });
380
+ },
381
+ onDone: (final) => {
382
+ if (onDone) onDone(final);
383
+ }
384
+ });
385
+ this.messages.update(target.id, {
386
+ _complete: true,
387
+ prefix: void 0,
388
+ _ephemeral: void 0
389
+ });
390
+ return target;
391
+ } catch (err) {
392
+ this.messages.update(target.id, { prefix: void 0 });
393
+ throw err;
394
+ }
395
+ }
396
+ _prepareContinue() {
397
+ const msgs = this.messages.getAll();
398
+ const last = msgs[msgs.length - 1];
399
+ if (last && (last.role === "assistant" || last._ephemeral)) return last;
400
+ throw new Error("最后一条消息不是 assistant,无法续写");
401
+ }
402
+ updateConfig(partial) {
403
+ Object.assign(this.config, partial);
404
+ if (typeof partial.system === "string") this.systemPrompts.set(partial.system);
405
+ this.emit("config-updated", {
406
+ changes: partial,
407
+ timestamp: Date.now()
408
+ });
409
+ }
410
+ async _request(options) {
411
+ let { userInput, addUser = true, isStream = false, onProgress, onDone, mergeToEntry } = options;
412
+ this.emit("sending", {
413
+ addUser,
414
+ userInput,
415
+ timestamp: Date.now()
416
+ });
417
+ this._addUserMessage(userInput, addUser);
418
+ await this._hooks.emitAsync("beforeRequest", {
419
+ messages: this.messages,
420
+ config: this.config,
421
+ options
422
+ });
423
+ if (this.config.ephemeralContinue && !mergeToEntry) {
424
+ const msgs = this.messages.getAll();
425
+ const last = msgs[msgs.length - 1];
426
+ if (last && last.prefix && (last.role === "assistant" || last._ephemeral)) mergeToEntry = last.id;
427
+ }
428
+ const body = this._adapter.buildRequest(this.messages.getAll(), this.config, this.systemPrompts.getEnabled());
429
+ const retryCfg = this.config.retry || {};
430
+ try {
431
+ return await this._withRetry(body, {
432
+ isStream,
433
+ onProgress,
434
+ onDone,
435
+ mergeToEntry,
436
+ maxRetries: retryCfg.maxRetries ?? 0,
437
+ retryDelay: retryCfg.retryDelay ?? 1e3
438
+ });
439
+ } catch (error) {
440
+ this.emit("error", {
441
+ error,
442
+ timestamp: Date.now()
443
+ });
444
+ throw error;
445
+ }
446
+ }
447
+ _addUserMessage(userInput, addUser) {
448
+ if (addUser && userInput !== void 0) {
449
+ const msg = typeof userInput === "string" ? {
450
+ role: "user",
451
+ content: userInput
452
+ } : {
453
+ role: "user",
454
+ ...userInput
455
+ };
456
+ this.messages.add(msg);
457
+ }
458
+ }
459
+ /** retry 循环,占位消息只 push 一次 */
460
+ async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, maxRetries, retryDelay }) {
461
+ let lastError = null;
462
+ const adapterOptions = { signal: this._abortController?.signal };
463
+ let placeholder = null;
464
+ let base = null;
465
+ if (isStream) {
466
+ if (mergeToEntry) {
467
+ placeholder = this.messages.getAll().find((m) => m.id === mergeToEntry);
468
+ if (placeholder) base = {
469
+ content: placeholder.content || "",
470
+ reasoning: placeholder.reasoningContent || ""
471
+ };
472
+ }
473
+ if (!placeholder) placeholder = this.messages.add({
474
+ role: "assistant",
475
+ content: "",
476
+ _complete: false
477
+ });
478
+ }
479
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
480
+ if (attempt > 0) {
481
+ this.emit("retry", {
482
+ attempt,
483
+ maxRetries,
484
+ lastError,
485
+ timestamp: Date.now()
486
+ });
487
+ await new Promise((r) => setTimeout(r, retryDelay));
488
+ }
489
+ try {
490
+ this._isGenerating = true;
491
+ if (isStream) return await this._stream(body, adapterOptions, {
492
+ placeholder,
493
+ base,
494
+ mergeToEntry,
495
+ onProgress,
496
+ onDone
497
+ });
498
+ else {
499
+ const resp = await this._adapter.send(body, this.config, adapterOptions);
500
+ let result = this._handleResult(this._adapter.parseResponse(resp), mergeToEntry);
501
+ if (this._processResponse && !mergeToEntry) result = await this._processResponse(result, { isStream: false });
502
+ return result;
503
+ }
504
+ } catch (error) {
505
+ lastError = error;
506
+ if (error.name === "AbortError" || this._abortController?.signal.aborted) {
507
+ this._isGenerating = false;
508
+ throw error;
509
+ }
510
+ if (error instanceof NetworkError && attempt < maxRetries) continue;
511
+ this._isGenerating = false;
512
+ throw error;
513
+ }
514
+ }
515
+ this._isGenerating = false;
516
+ throw lastError;
517
+ }
518
+ /** 流式:ChatService 维护占位,adapter 只管解析 */
519
+ async _stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone }) {
520
+ let finalMsg = null;
521
+ await this._adapter.stream(body, this.config, (snap) => {
522
+ placeholder.content = snap.content || "";
523
+ if (snap.reasoningContent) placeholder.reasoningContent = snap.reasoningContent;
524
+ if (snap.toolCalls) placeholder.toolCalls = [...snap.toolCalls];
525
+ this.emit("stream-progress", snap);
526
+ if (onProgress) onProgress(snap);
527
+ }, async (final) => {
528
+ finalMsg = final;
529
+ if (this._processResponse && !mergeToEntry) finalMsg = await this._processResponse(finalMsg, {
530
+ isStream: true,
531
+ onProgress,
532
+ onDone
533
+ });
534
+ if (mergeToEntry && base) {
535
+ const changes = {
536
+ content: base.content + (final.content || ""),
537
+ _complete: true
538
+ };
539
+ if (final.reasoningContent) changes.reasoningContent = base.reasoning + final.reasoningContent;
540
+ if (final.toolCalls) changes.toolCalls = final.toolCalls;
541
+ this.messages.update(mergeToEntry, changes);
542
+ const updated = this.messages.getAll().find((m) => m.id === mergeToEntry);
543
+ this.emit("message", updated);
544
+ if (onDone) onDone(updated);
545
+ } else {
546
+ placeholder._complete = true;
547
+ this.emit("message", finalMsg);
548
+ if (onDone) onDone(finalMsg);
549
+ }
550
+ }, adapterOptions);
551
+ return mergeToEntry || finalMsg;
552
+ }
553
+ /** 非流式结果落位 */
554
+ _handleResult(assistantMsg, mergeToEntry) {
555
+ if (mergeToEntry) {
556
+ const entry = this.messages.getAll().find((m) => m.id === mergeToEntry);
557
+ if (!entry) {
558
+ this.messages.add(assistantMsg);
559
+ this.emit("message", assistantMsg);
560
+ return assistantMsg;
561
+ }
562
+ const changes = {
563
+ content: (entry.content || "") + (assistantMsg.content || ""),
564
+ _complete: true
565
+ };
566
+ if (assistantMsg.reasoningContent) changes.reasoningContent = (entry.reasoningContent || "") + assistantMsg.reasoningContent;
567
+ if (assistantMsg.toolCalls) changes.toolCalls = assistantMsg.toolCalls;
568
+ this.messages.update(mergeToEntry, changes);
569
+ const updated = this.messages.getAll().find((m) => m.id === mergeToEntry);
570
+ this.emit("message", updated);
571
+ return updated;
572
+ }
164
573
  this.messages.add(assistantMsg);
165
574
  this.emit("message", assistantMsg);
166
575
  return assistantMsg;
167
576
  }
168
- /**
169
- * 流式发送消息
170
- * @param {string|object} input
171
- * @param {function} onProgress - 每次收到数据块时调用,参数为累积的当前消息对象
172
- * @param {function} onDone - 完成时调用,参数为最终消息对象
173
- */
174
- async stream(input, onProgress, onDone) {
175
- const userMsg = typeof input === "string" ? {
176
- role: "user",
177
- content: input
178
- } : {
179
- role: "user",
180
- ...input
181
- };
182
- this.messages.add(userMsg);
183
- this.emit("sending", userMsg);
184
- const requestBody = this._adapter.buildRequest(this.messages.getAll(), this.config);
185
- let accumulated = {
186
- role: "assistant",
187
- content: ""
188
- };
189
- await this._adapter.stream(requestBody, this.config, (chunk) => {
190
- if (chunk.content) accumulated.content += chunk.content;
191
- if (chunk.toolCalls) accumulated.toolCalls = chunk.toolCalls;
192
- if (chunk.reasoningContent) accumulated.reasoningContent = chunk.reasoningContent;
193
- this.emit("stream-progress", { ...accumulated });
194
- if (onProgress) onProgress({ ...accumulated });
195
- }, (finalMessage) => {
196
- this.messages.add(finalMessage);
197
- this.emit("message", finalMessage);
198
- if (onDone) onDone(finalMessage);
577
+ async send(userInput) {
578
+ this._abortController = new AbortController();
579
+ try {
580
+ return await this._request({
581
+ userInput,
582
+ addUser: true,
583
+ isStream: false
584
+ });
585
+ } finally {
586
+ this._abortController = null;
587
+ this._isGenerating = false;
588
+ }
589
+ }
590
+ async stream(userInput, onProgress, onDone) {
591
+ this._abortController = new AbortController();
592
+ try {
593
+ return await this._request({
594
+ userInput,
595
+ addUser: true,
596
+ isStream: true,
597
+ onProgress,
598
+ onDone
599
+ });
600
+ } finally {
601
+ this._abortController = null;
602
+ this._isGenerating = false;
603
+ }
604
+ }
605
+ async sendExisting() {
606
+ this._abortController = new AbortController();
607
+ try {
608
+ return await this._request({
609
+ addUser: false,
610
+ isStream: false
611
+ });
612
+ } finally {
613
+ this._abortController = null;
614
+ this._isGenerating = false;
615
+ }
616
+ }
617
+ async sendExistingStream(onProgress, onDone) {
618
+ this._abortController = new AbortController();
619
+ try {
620
+ return await this._request({
621
+ addUser: false,
622
+ isStream: true,
623
+ onProgress,
624
+ onDone
625
+ });
626
+ } finally {
627
+ this._abortController = null;
628
+ this._isGenerating = false;
629
+ }
630
+ }
631
+ };
632
+ //#endregion
633
+ //#region src/utils/url.js
634
+ /**
635
+ * URL 工具函数
636
+ */
637
+ /**
638
+ * 拼接 baseUrl 和 path
639
+ * 注意:path 应以 '/' 开头,否则会替换 baseUrl 的最后一段
640
+ * @param {string} baseUrl - 基础 URL(如 https://api.deepseek.com)
641
+ * @param {string} path - 路径(如 /v1/chat/completions)
642
+ * @returns {string} 完整的 URL
643
+ */
644
+ function joinUrl(baseUrl, path) {
645
+ return baseUrl.replace(/\/$/, "") + (path.startsWith("/") ? path : "/" + path);
646
+ }
647
+ //#endregion
648
+ //#region src/utils/typeCheck.js
649
+ /**
650
+ * 类型判断工具
651
+ */
652
+ /**
653
+ * 判断一个值是否为字符串
654
+ * @param {any} value - 要检查的值
655
+ * @returns {boolean} 如果是字符串则返回 true
656
+ */
657
+ function isString(value) {
658
+ return typeof value === "string";
659
+ }
660
+ //#endregion
661
+ //#region src/utils/MessageFormatter.js
662
+ /**
663
+ * MessageFormatter — 可注册的消息格式转换器
664
+ *
665
+ * 职责:
666
+ * 1. 内置 OpenA I兼容格式的转换逻辑
667
+ * 2. 支持 register(name, fn) 注册自定义格式(如 Anthropic、Gemini 等)
668
+ * 3. 统一处理 capabilities 过滤(reasoning、vision 等)
669
+ * 4. 所有适配器通过此工具获取 API 消息数组,消除重复代码
670
+ *
671
+ * 用法:
672
+ * import { MessageFormatter } from 'my-ai-chat-framework';
673
+ *
674
+ * // 使用内置格式
675
+ * const msgs = MessageFormatter.format({
676
+ * messages, systemPrompts, capabilities, resolveImage
677
+ * }); // 默认 'openai'
678
+ *
679
+ * // 注册自定义格式
680
+ * MessageFormatter.register('anthropic', ({ messages, systemPrompts, capabilities }) => {
681
+ * // 返回 Anthropic 格式的消息数组
682
+ * });
683
+ */
684
+ var IS_URL = /^https?:\/\//i;
685
+ function defaultResolveImage(imageId) {
686
+ if (IS_URL.test(imageId)) return { url: imageId };
687
+ return null;
688
+ }
689
+ function buildMultimodalContent(textContent, images, resolveImage) {
690
+ const content = [{
691
+ type: "text",
692
+ text: textContent
693
+ }];
694
+ for (const ref of images) {
695
+ const resolved = resolveImage(ref);
696
+ if (!resolved) continue;
697
+ if (resolved.url) content.push({
698
+ type: "image_url",
699
+ image_url: { url: resolved.url }
199
700
  });
701
+ else if (resolved.data) {
702
+ const mime = resolved.mimeType || "image/png";
703
+ content.push({
704
+ type: "image_url",
705
+ image_url: { url: `data:${mime};base64,${resolved.data}` }
706
+ });
707
+ }
200
708
  }
201
- /**
202
- * 注册工具(由工具调用插件实现)
203
- * 这里只留一个空方法,插件会覆盖它
204
- */
205
- registerTool(name, description, executor) {
206
- throw new Error("工具调用插件未加载,请先使用 use(toolCallingPlugin)");
709
+ return content;
710
+ }
711
+ /** OpenAI 兼容格式 */
712
+ function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
713
+ const rImg = typeof resolveImage === "function" ? resolveImage : defaultResolveImage;
714
+ const result = [];
715
+ for (const sp of systemPrompts) if (sp?.content && isString(sp.content) && sp.content.trim()) result.push({
716
+ role: "system",
717
+ content: sp.content.trim()
718
+ });
719
+ let ephemEnd = messages.length;
720
+ for (let i = messages.length - 1; i >= 0; i--) if (messages[i]._ephemeral) ephemEnd = i;
721
+ else break;
722
+ for (let i = 0; i < messages.length; i++) {
723
+ const msg = messages[i];
724
+ if (msg._ephemeral) {
725
+ if (i < ephemEnd) continue;
726
+ }
727
+ if (msg.role === "system" && !msg._ephemeral) continue;
728
+ if (msg.role === "tool") {
729
+ result.push({
730
+ role: "tool",
731
+ content: msg.content || "",
732
+ tool_call_id: msg.toolCallId
733
+ });
734
+ continue;
735
+ }
736
+ if (msg.role === "assistant" && msg.toolCalls?.length) {
737
+ const entry = {
738
+ role: "assistant",
739
+ content: msg.content || "",
740
+ tool_calls: msg.toolCalls
741
+ };
742
+ if (msg.prefix) entry.prefix = true;
743
+ if (capabilities?.reasoning && msg.reasoningContent) entry.reasoning_content = msg.reasoningContent;
744
+ result.push(entry);
745
+ continue;
746
+ }
747
+ const hasText = msg.content && isString(msg.content) && msg.content.trim();
748
+ const hasImages = msg.images && Array.isArray(msg.images) && msg.images.length > 0;
749
+ if (!hasText && !hasImages) {
750
+ if (msg.role === "assistant" && msg.prefix) {
751
+ const prefixEntry = {
752
+ role: "assistant",
753
+ content: "",
754
+ prefix: true
755
+ };
756
+ if (capabilities?.reasoning && msg.reasoningContent) prefixEntry.reasoning_content = msg.reasoningContent;
757
+ result.push(prefixEntry);
758
+ }
759
+ continue;
760
+ }
761
+ const entry = { role: msg.role };
762
+ if (hasImages) {
763
+ if (!capabilities?.vision) throw new Error("[MessageFormatter] 消息包含图片但模型不支持视觉(capabilities.vision=false)。请切换模型或移除图片。");
764
+ entry.content = buildMultimodalContent(msg.content || "", msg.images, rImg);
765
+ } else entry.content = msg.content;
766
+ if (msg.prefix) entry.prefix = true;
767
+ result.push(entry);
768
+ }
769
+ return result;
770
+ }
771
+ var MessageFormatter = {
772
+ _formats: new Map([["openai", toOpenAI]]),
773
+ register(name, fn) {
774
+ if (!name || typeof name !== "string" || !name.trim()) throw new Error("[MessageFormatter] 格式名称必须是非空字符串");
775
+ if (typeof fn !== "function") throw new Error("[MessageFormatter] 转换函数必须是 function");
776
+ this._formats.set(name.trim(), fn);
777
+ },
778
+ unregister(name) {
779
+ if (name === "openai") throw new Error("[MessageFormatter] 内置格式 \"openai\" 不可移除");
780
+ this._formats.delete(name);
781
+ },
782
+ listFormats() {
783
+ return [...this._formats.keys()];
784
+ },
785
+ format(options = {}) {
786
+ const formatName = options.format || "openai";
787
+ const fn = this._formats.get(formatName);
788
+ if (!fn) throw new Error(`[MessageFormatter] 未知格式 "${formatName}"。可用格式: ${[...this._formats.keys()].join(", ")}`);
789
+ return fn({
790
+ messages: options.messages || [],
791
+ systemPrompts: options.systemPrompts || [],
792
+ capabilities: options.capabilities || {},
793
+ resolveImage: options.resolveImage
794
+ });
207
795
  }
208
796
  };
797
+ function assembleMessages(options = {}) {
798
+ return MessageFormatter.format({
799
+ ...options,
800
+ format: "openai"
801
+ });
802
+ }
209
803
  //#endregion
210
804
  //#region src/adapters/openai.js
211
805
  /**
@@ -217,40 +811,66 @@ var openaiAdapter = {
217
811
  install(chatService) {
218
812
  chatService.setAdapter(this);
219
813
  },
220
- buildRequest(messages, config) {
221
- const apiMessages = messages.map((msg) => {
222
- const base = {
223
- role: msg.role,
224
- content: msg.content
225
- };
226
- if (msg.role === "assistant" && msg.toolCalls) base.tool_calls = msg.toolCalls;
227
- if (msg.role === "tool" && msg.toolCallId) base.tool_call_id = msg.toolCallId;
228
- return base;
229
- });
230
- return {
231
- model: config.model,
232
- messages: apiMessages,
233
- temperature: config.temperature ?? .7,
234
- max_tokens: config.maxTokens ?? 2e3,
814
+ buildRequest(messages, config, systemPrompts = []) {
815
+ const model = config.model || config.modelParams?.model;
816
+ if (!model) throw new Error("Missing required config: model (either at top level or in modelParams)");
817
+ const temperature = config.modelParams?.temperature ?? config.temperature ?? .7;
818
+ const maxTokens = config.modelParams?.maxTokens ?? config.maxTokens ?? 2e3;
819
+ const reasoningEffort = config.modelParams?.reasoningEffort ?? config.reasoningEffort;
820
+ const requestBody = {
821
+ model,
822
+ messages: MessageFormatter.format({
823
+ messages,
824
+ systemPrompts,
825
+ capabilities: config.capabilities || {},
826
+ resolveImage: config.resolveImage,
827
+ format: config.messageFormat
828
+ }),
829
+ temperature,
830
+ max_tokens: maxTokens,
235
831
  stream: false
236
832
  };
833
+ if (config.tools && Array.isArray(config.tools) && config.tools.length > 0) {
834
+ requestBody.tools = config.tools;
835
+ requestBody.tool_choice = "auto";
836
+ }
837
+ if (reasoningEffort && model === "deepseek-reasoner") requestBody.reasoning_effort = reasoningEffort;
838
+ return requestBody;
839
+ },
840
+ _getUrl(config) {
841
+ config = config || {};
842
+ const { apiUrl, baseUrl, path } = config;
843
+ const defaultPath = "/chat/completions";
844
+ if (apiUrl && isString(apiUrl)) return apiUrl;
845
+ if (baseUrl && isString(baseUrl)) return joinUrl(baseUrl, path && isString(path) ? path : defaultPath);
846
+ return "https://api.openai.com/v1/chat/completions";
237
847
  },
238
- async send(requestBody, config) {
239
- const response = await fetch(config.apiUrl || "https://api.openai.com/v1/chat/completions", {
240
- method: "POST",
241
- headers: {
848
+ async send(requestBody, config, options = {}) {
849
+ try {
850
+ const url = this._getUrl(config);
851
+ const headers = {
242
852
  "Content-Type": "application/json",
243
- "Authorization": `Bearer ${config.apiKey}`
244
- },
245
- body: JSON.stringify(requestBody)
246
- });
247
- if (!response.ok) {
248
- const error = await response.text();
249
- throw new Error(`API 请求失败: ${response.status} ${error}`);
853
+ "Authorization": `Bearer ${config.apiKey}`,
854
+ ...config.headers || {},
855
+ ...options.headers || {}
856
+ };
857
+ const response = await fetch(url, {
858
+ method: "POST",
859
+ headers,
860
+ body: JSON.stringify(requestBody),
861
+ signal: options.signal
862
+ });
863
+ if (!response.ok) await this.handleErrorResponse(response);
864
+ return await response.json();
865
+ } catch (error) {
866
+ if (error instanceof APIError) throw error;
867
+ if (error.name === "AbortError") throw error;
868
+ if (error.name === "TypeError" && error.message.includes("fetch")) throw new NetworkError(`Request failed: ${error.message}`, error);
869
+ throw error;
250
870
  }
251
- return await response.json();
252
871
  },
253
872
  parseResponse(apiResponse) {
873
+ if (!apiResponse || !apiResponse.choices || !apiResponse.choices[0] || !apiResponse.choices[0].message) throw new ParsingError("Invalid API response: missing choices or message", apiResponse, JSON.stringify(apiResponse));
254
874
  const msg = apiResponse.choices[0].message;
255
875
  const internal = {
256
876
  role: msg.role,
@@ -260,32 +880,43 @@ var openaiAdapter = {
260
880
  if (msg.reasoning_content) internal.reasoningContent = msg.reasoning_content;
261
881
  return internal;
262
882
  },
263
- async stream(requestBody, config, onProgress, onDone) {
883
+ async stream(requestBody, config, onProgress, onDone, options = {}) {
264
884
  const streamBody = {
265
885
  ...requestBody,
266
886
  stream: true
267
887
  };
268
- const response = await fetch(config.apiUrl || "https://api.openai.com/v1/chat/completions", {
269
- method: "POST",
270
- headers: {
271
- "Content-Type": "application/json",
272
- "Authorization": `Bearer ${config.apiKey}`,
273
- "Accept": "text/event-stream"
274
- },
275
- body: JSON.stringify(streamBody)
276
- });
277
- if (!response.ok) {
278
- const error = await response.text();
279
- throw new Error(`流式请求失败: ${response.status} ${error}`);
888
+ const url = this._getUrl(config);
889
+ let response;
890
+ const headers = {
891
+ "Content-Type": "application/json",
892
+ "Authorization": `Bearer ${config.apiKey}`,
893
+ "Accept": "text/event-stream",
894
+ ...config.headers || {},
895
+ ...options.headers || {}
896
+ };
897
+ try {
898
+ response = await fetch(url, {
899
+ method: "POST",
900
+ headers,
901
+ body: JSON.stringify(streamBody),
902
+ signal: options.signal
903
+ });
904
+ } catch (error) {
905
+ if (error.name === "AbortError") throw error;
906
+ if (error.name === "TypeError" && error.message.includes("fetch")) throw new NetworkError(`Stream request failed: ${error.message}`, error);
907
+ throw error;
280
908
  }
909
+ if (!response.ok) await this.handleErrorResponse(response);
281
910
  const reader = response.body.getReader();
282
911
  const decoder = new TextDecoder();
283
912
  let buffer = "";
284
913
  let accumulated = {
285
914
  role: "assistant",
286
- content: ""
915
+ content: "",
916
+ reasoningContent: ""
287
917
  };
288
918
  while (true) {
919
+ if (options.signal?.aborted) break;
289
920
  const { done, value } = await reader.read();
290
921
  if (done) break;
291
922
  buffer += decoder.decode(value, { stream: true });
@@ -293,22 +924,34 @@ var openaiAdapter = {
293
924
  buffer = lines.pop();
294
925
  for (const line of lines) {
295
926
  const dataLine = line.replace(/^data: /, "").trim();
296
- if (!dataLine || dataLine === "[DONE]") continue;
927
+ if (!dataLine) continue;
928
+ if (dataLine === "[DONE]") break;
297
929
  try {
298
930
  const delta = JSON.parse(dataLine).choices?.[0]?.delta;
299
931
  if (!delta) continue;
300
- const update = {
301
- role: delta.role || "assistant",
302
- content: delta.content || ""
303
- };
304
- if (delta.tool_calls) update.toolCalls = delta.tool_calls;
305
- if (delta.reasoning_content) update.reasoningContent = delta.reasoning_content;
306
- if (update.content) accumulated.content += update.content;
307
- if (update.toolCalls) accumulated.toolCalls = update.toolCalls;
308
- if (update.reasoningContent) accumulated.reasoningContent = update.reasoningContent;
932
+ if (delta.content) accumulated.content += delta.content;
933
+ if (delta.tool_calls) {
934
+ if (!accumulated.toolCalls) accumulated.toolCalls = [];
935
+ for (const toolCallDelta of delta.tool_calls) {
936
+ const index = toolCallDelta.index;
937
+ if (!accumulated.toolCalls[index]) accumulated.toolCalls[index] = {
938
+ id: "",
939
+ type: "function",
940
+ function: {
941
+ name: "",
942
+ arguments: ""
943
+ }
944
+ };
945
+ if (toolCallDelta.id) accumulated.toolCalls[index].id = toolCallDelta.id;
946
+ if (toolCallDelta.type) accumulated.toolCalls[index].type = toolCallDelta.type;
947
+ if (toolCallDelta.function?.name) accumulated.toolCalls[index].function.name += toolCallDelta.function.name;
948
+ if (toolCallDelta.function?.arguments) accumulated.toolCalls[index].function.arguments += toolCallDelta.function.arguments;
949
+ }
950
+ }
951
+ if (delta.reasoning_content) accumulated.reasoningContent += delta.reasoning_content;
309
952
  onProgress({ ...accumulated });
310
953
  } catch (e) {
311
- console.warn("解析流式数据块失败:", e, dataLine);
954
+ console.warn("stream parsing failed :", e, dataLine);
312
955
  }
313
956
  }
314
957
  }
@@ -318,13 +961,32 @@ var openaiAdapter = {
318
961
  toolCalls: accumulated.toolCalls,
319
962
  reasoningContent: accumulated.reasoningContent
320
963
  });
964
+ },
965
+ async handleErrorResponse(response) {
966
+ let errorText = await response.text();
967
+ let errorMessage = `HTTP ${response.status}`;
968
+ try {
969
+ const errorJson = JSON.parse(errorText);
970
+ errorMessage = errorJson.error?.message || errorJson.message || errorText.slice(0, 200);
971
+ } catch (e) {
972
+ errorMessage = errorText.length > 200 ? errorText.slice(0, 200) + "..." : errorText;
973
+ }
974
+ throw new APIError(errorMessage, response.status, null, errorText);
321
975
  }
322
976
  };
323
977
  //#endregion
324
978
  //#region src/plugins/tool-calling.js
325
979
  /**
326
980
  * 工具调用插件
327
- * 拦截消息中的 tool_calls,执行注册的工具,并将结果插入对话,然后继续请求
981
+ * 功能:拦截助手消息中的 tool_calls,执行对应的工具,将结果作为 tool 消息加入对话,
982
+ * 然后自动继续对话(通过 sendExisting / sendExistingStream),直到没有新的工具调用。
983
+ *
984
+ * 设计要点:
985
+ * - 支持普通请求和流式请求(通过 isStream 标志区分)
986
+ * - 支持多次工具调用循环(maxIterations 防止无限循环)
987
+ * - 串行执行工具(可后续升级为并行)
988
+ * - 工具执行失败时,仍然返回错误信息给 AI,而不是中断整个流程
989
+ * - 触发 tool-error 事件,方便用户监听工具执行异常
328
990
  */
329
991
  var toolCallingPlugin = {
330
992
  name: "tool-calling",
@@ -332,107 +994,313 @@ var toolCallingPlugin = {
332
994
  install(chatService) {
333
995
  this.chatService = chatService;
334
996
  this._tools = /* @__PURE__ */ new Map();
335
- chatService.registerTool = (name, description, executor) => {
997
+ if (!chatService.config.tools) chatService.config.tools = [];
998
+ /**
999
+ * 注册工具
1000
+ * @param {string} name - 工具名称(唯一标识)
1001
+ * @param {string} description - 工具描述(告诉 AI 何时调用)
1002
+ * @param {Function} executor - 异步执行函数,接收参数对象,返回结果(字符串或对象)
1003
+ * @param {Object} parameters - JSON Schema 参数定义(可选,默认为空对象)
1004
+ * @returns {ChatService} 返回 chatService 实例,支持链式调用
1005
+ */
1006
+ chatService.registerTool = (name, description, executor, parameters = {}) => {
336
1007
  this._tools.set(name, {
337
1008
  executor,
338
1009
  description
339
1010
  });
1011
+ const toolDefinition = {
1012
+ type: "function",
1013
+ function: {
1014
+ name,
1015
+ description,
1016
+ parameters: {
1017
+ type: "object",
1018
+ properties: parameters,
1019
+ required: Object.keys(parameters).filter((key) => parameters[key]?.required)
1020
+ }
1021
+ }
1022
+ };
1023
+ if (!chatService.config.tools.find((t) => t.function.name === name)) chatService.config.tools.push(toolDefinition);
340
1024
  return chatService;
341
1025
  };
342
- const originalSend = chatService.send;
343
- chatService.send = async (input) => {
344
- return this._handleWithTools(originalSend, input, false);
345
- };
346
- const originalStream = chatService.stream;
347
- chatService.stream = async (input, onProgress, onDone) => {
348
- return this._handleWithTools(originalStream, input, true, onProgress, onDone);
1026
+ chatService._processResponse = async (response, { isStream, onProgress, onDone }) => {
1027
+ return this._handleWithTools(response, isStream, onProgress, onDone);
349
1028
  };
350
1029
  },
351
- async _handleWithTools(originalMethod, input, isStream, onProgress, onDone) {
1030
+ async _handleWithTools(initialResponse, isStream, onProgress, onDone) {
352
1031
  const self = this;
353
1032
  const chat = this.chatService;
354
1033
  let iteration = 0;
1034
+ /**
1035
+ * 递归处理工具调用
1036
+ * @param {Object} initialResponse - 初始 AI 响应(可能是第一次请求的响应)
1037
+ * @returns {Promise<Object>} 最终 AI 响应(不含 tool_calls)
1038
+ */
355
1039
  async function processResponse(initialResponse) {
356
1040
  let lastResponse = initialResponse;
357
1041
  while (iteration < self.maxIterations) {
358
1042
  const toolCalls = lastResponse?.toolCalls;
359
1043
  if (!toolCalls || toolCalls.length === 0) break;
360
1044
  iteration++;
361
- const toolResults = [];
362
- for (const call of toolCalls) {
1045
+ const toolResults = await Promise.all(toolCalls.map(async (call) => {
363
1046
  const toolName = call.function?.name;
364
- const args = JSON.parse(call.function?.arguments || "{}");
1047
+ let args;
1048
+ try {
1049
+ args = JSON.parse(call.function?.arguments || "{}");
1050
+ } catch (parseErr) {
1051
+ chat.emit("tool-error", {
1052
+ toolName: toolName || "unknown",
1053
+ error: parseErr,
1054
+ toolCallId: call.id,
1055
+ stage: "parse",
1056
+ timestamp: Date.now()
1057
+ });
1058
+ return {
1059
+ tool_call_id: call.id,
1060
+ error: `参数解析失败: ${parseErr.message}`,
1061
+ success: false
1062
+ };
1063
+ }
365
1064
  const tool = self._tools.get(toolName);
366
1065
  if (!tool) {
367
- console.warn(`未找到工具: ${toolName}`);
368
- toolResults.push({
1066
+ chat.emit("tool-error", {
1067
+ toolName,
1068
+ error: /* @__PURE__ */ new Error(`Tool not registered: ${toolName}`),
1069
+ toolCallId: call.id,
1070
+ stage: "lookup",
1071
+ timestamp: Date.now()
1072
+ });
1073
+ return {
369
1074
  tool_call_id: call.id,
370
1075
  error: `工具 ${toolName} 未注册`,
371
1076
  success: false
372
- });
373
- continue;
1077
+ };
374
1078
  }
375
1079
  try {
376
- const result = await tool.executor(args);
377
- toolResults.push({
1080
+ const toolTimeout = chat.config.toolTimeout;
1081
+ let execPromise = tool.executor(args);
1082
+ if (toolTimeout && typeof toolTimeout === "number" && toolTimeout > 0) {
1083
+ const timeoutErr = /* @__PURE__ */ new Error(`工具 ${toolName} 执行超时 (${toolTimeout}ms)`);
1084
+ timeoutErr.name = "ToolTimeoutError";
1085
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(timeoutErr), toolTimeout));
1086
+ execPromise = Promise.race([execPromise, timeoutPromise]);
1087
+ }
1088
+ const result = await execPromise;
1089
+ chat.emit("tool-success", {
1090
+ toolName,
1091
+ result,
1092
+ toolCallId: call.id,
1093
+ timestamp: Date.now()
1094
+ });
1095
+ return {
378
1096
  tool_call_id: call.id,
379
1097
  content: typeof result === "string" ? result : JSON.stringify(result),
380
1098
  success: true
381
- });
1099
+ };
382
1100
  } catch (err) {
383
- console.error(`工具 ${toolName} 执行失败:`, err);
384
- toolResults.push({
1101
+ const stage = err.name === "ToolTimeoutError" ? "timeout" : "execute";
1102
+ chat.emit("tool-error", {
1103
+ toolName,
1104
+ error: err,
1105
+ toolCallId: call.id,
1106
+ stage,
1107
+ timestamp: Date.now()
1108
+ });
1109
+ return {
385
1110
  tool_call_id: call.id,
386
1111
  error: err.message,
387
1112
  success: false
388
- });
1113
+ };
389
1114
  }
1115
+ }));
1116
+ for (const tr of toolResults) {
1117
+ const content = tr.success ? tr.content : tr.error || "工具执行失败";
1118
+ chat.messages.addTool(content, tr.tool_call_id);
390
1119
  }
391
- for (const tr of toolResults) chat.messages.addTool(tr.content || tr.error, tr.tool_call_id);
392
- if (isStream) {
393
- chat.messages.getLast();
394
- await new Promise((resolve) => {
395
- originalStream.call(chat, null, (chunk) => {
396
- if (onProgress) onProgress(chunk);
397
- lastResponse = chunk;
398
- }, (final) => {
399
- lastResponse = final;
400
- resolve();
401
- });
1120
+ if (isStream) await new Promise((resolve) => {
1121
+ chat.sendExistingStream((chunk) => {
1122
+ if (onProgress) onProgress(chunk);
1123
+ lastResponse = chunk;
1124
+ }, (final) => {
1125
+ lastResponse = final;
1126
+ resolve();
402
1127
  });
403
- } else {
404
- const reqBody = chat._adapter.buildRequest(chat.messages.getAll(), chat.config);
405
- const apiResp = await chat._adapter.send(reqBody, chat.config);
406
- lastResponse = chat._adapter.parseResponse(apiResp);
407
- }
1128
+ });
1129
+ else lastResponse = await chat.sendExisting();
408
1130
  }
409
1131
  return lastResponse;
410
1132
  }
411
- if (isStream) {
412
- let finalMessage;
413
- await originalMethod.call(chat, input, (chunk) => {
414
- if (onProgress) onProgress(chunk);
415
- }, async (final) => {
416
- finalMessage = await processResponse(final);
417
- if (onDone) onDone(finalMessage);
418
- });
419
- } else return await processResponse(await originalMethod.call(chat, input));
1133
+ return await processResponse(initialResponse);
420
1134
  }
421
1135
  };
422
1136
  //#endregion
423
- //#region src/index.js
424
- var src_default = {
425
- ChatService,
426
- MessageStore,
427
- EventEmitter,
428
- openaiAdapter,
429
- toolCallingPlugin
1137
+ //#region src/plugins/model-registry.js
1138
+ /**
1139
+ * model-registry 插件
1140
+ *
1141
+ * 职责:
1142
+ * 1. 内置常用模型的能力标签表(model → capabilities 映射)
1143
+ * 2. 安装时自动根据 config.model 查表,写入 config.capabilities
1144
+ * 3. 监听 config-updated 事件,切换模型时自动同步能力
1145
+ * 4. 提供 chat.registerModel(),允许用户追加/覆盖自定义模型
1146
+ *
1147
+ * 能力标签字段(全部可选,缺失视为 false / 未知):
1148
+ * {
1149
+ * reasoning: boolean — 是否返回 reasoning_content(思维链)
1150
+ * vision: boolean — 是否支持图片输入
1151
+ * toolCalling: boolean — 是否支持 function calling
1152
+ * streaming: boolean — 是否支持流式传输
1153
+ * maxInputTokens: number — 最大输入 token 数
1154
+ * maxOutputTokens: number — 最大输出 token 数
1155
+ * }
1156
+ *
1157
+ * 使用:
1158
+ * chat.use(modelRegistryPlugin);
1159
+ * // config.capabilities 现在已自动填充
1160
+ * chat.registerModel('my-custom-model', { toolCalling: true });
1161
+ */
1162
+ var BUILTIN_MODELS = {
1163
+ "deepseek-chat": {
1164
+ reasoning: false,
1165
+ vision: false,
1166
+ toolCalling: true,
1167
+ streaming: true,
1168
+ maxInputTokens: 128e3,
1169
+ maxOutputTokens: 8192
1170
+ },
1171
+ "deepseek-reasoner": {
1172
+ reasoning: true,
1173
+ vision: false,
1174
+ toolCalling: true,
1175
+ streaming: true,
1176
+ maxInputTokens: 128e3,
1177
+ maxOutputTokens: 8192
1178
+ },
1179
+ "gpt-4o": {
1180
+ reasoning: false,
1181
+ vision: true,
1182
+ toolCalling: true,
1183
+ streaming: true,
1184
+ maxInputTokens: 128e3,
1185
+ maxOutputTokens: 16384
1186
+ },
1187
+ "gpt-4o-mini": {
1188
+ reasoning: false,
1189
+ vision: true,
1190
+ toolCalling: true,
1191
+ streaming: true,
1192
+ maxInputTokens: 128e3,
1193
+ maxOutputTokens: 16384
1194
+ },
1195
+ "gpt-3.5-turbo": {
1196
+ reasoning: false,
1197
+ vision: false,
1198
+ toolCalling: true,
1199
+ streaming: true,
1200
+ maxInputTokens: 16385,
1201
+ maxOutputTokens: 4096
1202
+ },
1203
+ "o1": {
1204
+ reasoning: true,
1205
+ vision: false,
1206
+ toolCalling: false,
1207
+ streaming: false,
1208
+ maxInputTokens: 2e5,
1209
+ maxOutputTokens: 1e5
1210
+ },
1211
+ "o3-mini": {
1212
+ reasoning: true,
1213
+ vision: false,
1214
+ toolCalling: true,
1215
+ streaming: true,
1216
+ maxInputTokens: 2e5,
1217
+ maxOutputTokens: 1e5
1218
+ },
1219
+ "claude-3.5-sonnet": {
1220
+ reasoning: false,
1221
+ vision: true,
1222
+ toolCalling: true,
1223
+ streaming: true,
1224
+ maxInputTokens: 2e5,
1225
+ maxOutputTokens: 8192
1226
+ },
1227
+ "claude-3.5-haiku": {
1228
+ reasoning: false,
1229
+ vision: true,
1230
+ toolCalling: true,
1231
+ streaming: true,
1232
+ maxInputTokens: 2e5,
1233
+ maxOutputTokens: 8192
1234
+ }
1235
+ };
1236
+ var DEFAULT_CAPABILITIES = {
1237
+ streaming: true,
1238
+ toolCalling: false,
1239
+ reasoning: false,
1240
+ vision: false
1241
+ };
1242
+ var modelRegistryPlugin = {
1243
+ name: "model-registry",
1244
+ install(chatService) {
1245
+ this.chat = chatService;
1246
+ this._registry = new Map(Object.entries(BUILTIN_MODELS));
1247
+ /**
1248
+ * 注册/覆盖一个模型的能力标签
1249
+ * @param {string} name — 模型名称
1250
+ * @param {Object} capabilities — 能力标签对象(部分字段即可,未提供的取默认值)
1251
+ * @returns {ChatService}
1252
+ */
1253
+ chatService.registerModel = (name, capabilities = {}) => {
1254
+ if (!name || typeof name !== "string" || !name.trim()) throw new Error("[model-registry] 模型名称必须是非空字符串");
1255
+ const merged = {
1256
+ ...DEFAULT_CAPABILITIES,
1257
+ ...capabilities
1258
+ };
1259
+ this._registry.set(name.trim(), merged);
1260
+ if (chatService.config.model === name.trim()) this._syncCapabilities();
1261
+ return chatService;
1262
+ };
1263
+ /**
1264
+ * 列出所有已注册的模型名称
1265
+ * @returns {Array<string>}
1266
+ */
1267
+ chatService.listModels = () => {
1268
+ return [...this._registry.keys()];
1269
+ };
1270
+ this._syncCapabilities();
1271
+ chatService.on("config-updated", ({ changes }) => {
1272
+ if ("model" in changes) this._syncCapabilities();
1273
+ if ("capabilities" in changes && !("model" in changes)) {
1274
+ const current = this._lookupCapabilities();
1275
+ if (current) chatService.config.capabilities = {
1276
+ ...current,
1277
+ ...changes.capabilities
1278
+ };
1279
+ }
1280
+ });
1281
+ },
1282
+ _lookupCapabilities() {
1283
+ const model = this.chat.config.model;
1284
+ if (!model) return null;
1285
+ return this._registry.get(model) || null;
1286
+ },
1287
+ _syncCapabilities() {
1288
+ const caps = this._lookupCapabilities();
1289
+ if (caps) this.chat.config.capabilities = { ...caps };
1290
+ }
430
1291
  };
431
1292
  //#endregion
1293
+ exports.APIError = APIError;
432
1294
  exports.ChatService = ChatService;
1295
+ exports.ConfigurationError = ConfigurationError;
433
1296
  exports.EventEmitter = EventEmitter;
1297
+ exports.MessageFormatter = MessageFormatter;
434
1298
  exports.MessageStore = MessageStore;
435
- exports.default = src_default;
1299
+ exports.NetworkError = NetworkError;
1300
+ exports.ParsingError = ParsingError;
1301
+ exports.SystemPromptStore = SystemPromptStore;
1302
+ exports.assembleMessages = assembleMessages;
1303
+ exports.modelRegistryPlugin = modelRegistryPlugin;
436
1304
  exports.openaiAdapter = openaiAdapter;
437
1305
  exports.toolCallingPlugin = toolCallingPlugin;
438
1306