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
|
@@ -139,6 +139,25 @@ var MessageStore = class {
|
|
|
139
139
|
return [];
|
|
140
140
|
}
|
|
141
141
|
/**
|
|
142
|
+
* 撤回到指定消息 id 的上一个用户消息,删除两者之间的所有消息
|
|
143
|
+
* 适用于重发
|
|
144
|
+
* @param {string} id — 目标消息 id
|
|
145
|
+
* @returns {Array} 被删除的消息列表
|
|
146
|
+
*/
|
|
147
|
+
undoToPreviousUser(id) {
|
|
148
|
+
let targetIndex = -1;
|
|
149
|
+
let previousUserIndex = -1;
|
|
150
|
+
for (let i = this._messages.length - 1; i >= 0; i--) {
|
|
151
|
+
if (this._messages[i].id === id) targetIndex = i;
|
|
152
|
+
if (targetIndex >= 0 && this._messages[i].role === "user") {
|
|
153
|
+
previousUserIndex = i;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (targetIndex >= 0 && previousUserIndex >= 0) return this._messages.splice(previousUserIndex + 1, targetIndex - previousUserIndex);
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
142
161
|
* 更新消息:按 id 查找并合并 changes,不新增消息
|
|
143
162
|
* @param {string} id — 消息 id
|
|
144
163
|
* @param {Object} changes — 要合并的字段
|
|
@@ -308,7 +327,85 @@ var ParsingError = class extends Error {
|
|
|
308
327
|
}
|
|
309
328
|
};
|
|
310
329
|
//#endregion
|
|
330
|
+
//#region src/core/Pipeline.js
|
|
331
|
+
/**
|
|
332
|
+
* Pipeline —— 极简顺序管道(v3.0 提案 · 阶段 1)
|
|
333
|
+
*
|
|
334
|
+
* 三个概念(对照 pipeline-demo.html):
|
|
335
|
+
* - 小车 ctx —— 本次请求的全部"状态",车间之间只通过它交接
|
|
336
|
+
* - 车间 stage —— { name, run(ctx) },只干一件事,不认别的车间
|
|
337
|
+
* - 调度 run() —— for + await,顺序执行(线性,无"回程";洋葱能力留给阶段 2 的 after 车间)
|
|
338
|
+
*/
|
|
339
|
+
var Pipeline = class {
|
|
340
|
+
constructor() {
|
|
341
|
+
this._stages = [];
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* 注册一个车间(追加到末尾)。
|
|
345
|
+
* @param {{name: string, run: (ctx: object) => void | Promise<void>}} stage
|
|
346
|
+
* @returns {Pipeline} this,支持链式
|
|
347
|
+
*/
|
|
348
|
+
register(stage) {
|
|
349
|
+
if (!stage || typeof stage.run !== "function") throw new TypeError("Pipeline.register: stage 需要 { name, run(ctx) },运行 run 必须是函数");
|
|
350
|
+
this._stages.push(stage);
|
|
351
|
+
return this;
|
|
352
|
+
}
|
|
353
|
+
/** 按名字移除车间(移除不存在的名字是安全的) */
|
|
354
|
+
unregister(name) {
|
|
355
|
+
this._stages = this._stages.filter((s) => s.name !== name);
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
/** 列出当前车间名(调试用) */
|
|
359
|
+
names() {
|
|
360
|
+
return this._stages.map((s) => s.name);
|
|
361
|
+
}
|
|
362
|
+
/** 把小车开过所有车间;返回 ctx(车间可改 ctx 上的字段) */
|
|
363
|
+
async run(ctx) {
|
|
364
|
+
for (const stage of this._stages) await stage.run(ctx);
|
|
365
|
+
return ctx;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
//#endregion
|
|
311
369
|
//#region src/core/ChatService.js
|
|
370
|
+
/**
|
|
371
|
+
* @typedef {Object} ChatAdapter 适配器协议(v3.0:写同级适配器只需实现这 4+1 个方法)
|
|
372
|
+
* @property {Function} buildRequest (messages, config, systemPrompts) => requestBody
|
|
373
|
+
* @property {Function} send (body, config, opts) => Promise<apiResponse>
|
|
374
|
+
* @property {Function} stream (body, config, onProgress, onDone, opts) => Promise<void>
|
|
375
|
+
* @property {Function} parseResponse (apiResponse) => assistantMessage
|
|
376
|
+
* @property {Function} [getRequestDefaults] () => 默认请求参数(可选)
|
|
377
|
+
* @property {Function} [install] (chatService, options) 插件式安装(可选)
|
|
378
|
+
*/
|
|
379
|
+
/**
|
|
380
|
+
* @typedef {Object} PipelineContext 管道小车 ctx(public pipe 车间可见/可改的全部字段)
|
|
381
|
+
* @property {Object} options 原始请求选项
|
|
382
|
+
* @property {*} userInput 用户输入(可为 undefined)
|
|
383
|
+
* @property {boolean} addUser 是否已新增/将新增用户消息
|
|
384
|
+
* @property {MessageStore} messages 消息列表(可读可改:push / unshift / update)
|
|
385
|
+
* @property {SystemPromptStore} systemPrompts 系统提示词(可读可改)
|
|
386
|
+
* @property {Object} config 合并后的请求级配置(prepareInput 后生效)
|
|
387
|
+
* @property {boolean} isStream 本次是否流式
|
|
388
|
+
* @property {Function|null} onProgress 流式进度回调
|
|
389
|
+
* @property {Function|null} onDone 流式完成回调
|
|
390
|
+
* @property {string|null} mergeToEntry 续写目标 id(null = 普通发送)
|
|
391
|
+
* @property {*} body 请求体(buildRequest 后产出)
|
|
392
|
+
* @property {*} result 最终结果(send 后产出)
|
|
393
|
+
*/
|
|
394
|
+
/**
|
|
395
|
+
* @typedef {Object} PipeStage 车间(v3.0 公开扩展单元)
|
|
396
|
+
* @property {string} name 车间名(唯一,不能与内部车间重名)
|
|
397
|
+
* @property {string} [phase] 'beforeSend'(默认)| 'afterSend'
|
|
398
|
+
* @property {Function} run (ctx: PipelineContext) => void | Promise<void>
|
|
399
|
+
*/
|
|
400
|
+
var SESSION_CONFIG_KEYS = [
|
|
401
|
+
"model",
|
|
402
|
+
"temperature",
|
|
403
|
+
"maxTokens",
|
|
404
|
+
"modelParams",
|
|
405
|
+
"system",
|
|
406
|
+
"retry",
|
|
407
|
+
"ephemeralContinue"
|
|
408
|
+
];
|
|
312
409
|
var ChatService = class extends EventEmitter {
|
|
313
410
|
constructor(config = {}) {
|
|
314
411
|
super();
|
|
@@ -322,17 +419,106 @@ var ChatService = class extends EventEmitter {
|
|
|
322
419
|
this._processResponse = null;
|
|
323
420
|
const model = this.config.model || this.config.modelParams?.model;
|
|
324
421
|
if (!model || typeof model !== "string" || !model.trim()) throw new ConfigurationError("缺少 model 配置");
|
|
325
|
-
|
|
422
|
+
this._assertValidConfig(this.config);
|
|
423
|
+
if (typeof config.system === "string" && config.system.trim()) this.systemPrompts.set(config.system);
|
|
424
|
+
if (config.adapter) this.setAdapter(config.adapter);
|
|
425
|
+
this._userStages = [];
|
|
426
|
+
this._pipeline = this._buildPipeline();
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* 校验配置中的可校验字段(model 必填由构造器负责,这里只校验存在性)
|
|
430
|
+
* @throws {ConfigurationError}
|
|
431
|
+
*/
|
|
432
|
+
_assertValidConfig(cfg) {
|
|
433
|
+
const temperature = cfg.modelParams?.temperature ?? cfg.temperature;
|
|
326
434
|
if (temperature !== void 0 && (typeof temperature !== "number" || temperature < 0 || temperature > 2)) throw new ConfigurationError(`temperature 必须在 0-2 之间,当前值: ${temperature}`);
|
|
327
|
-
const maxTokens =
|
|
435
|
+
const maxTokens = cfg.modelParams?.maxTokens ?? cfg.maxTokens;
|
|
328
436
|
if (maxTokens !== void 0 && (typeof maxTokens !== "number" || maxTokens < 1 || !Number.isInteger(maxTokens))) throw new ConfigurationError(`maxTokens 必须为正整数,当前值: ${maxTokens}`);
|
|
329
|
-
|
|
437
|
+
const model = cfg.model ?? cfg.modelParams?.model;
|
|
438
|
+
if (model !== void 0 && (typeof model !== "string" || !model.trim())) throw new ConfigurationError("model 必须是非空字符串");
|
|
439
|
+
}
|
|
440
|
+
use(plugin, options = {}) {
|
|
441
|
+
plugin.install(this, options);
|
|
442
|
+
return this;
|
|
330
443
|
}
|
|
331
|
-
|
|
332
|
-
|
|
444
|
+
/**
|
|
445
|
+
* 往请求管道里挂一个"车间"。
|
|
446
|
+
* - phase 'beforeSend'(默认):在内部 beforeRequest 钩子之后、构建请求体之前执行
|
|
447
|
+
* - phase 'afterSend' :在内部发送(流式/非流式)完成、结果落位之后执行(可改 ctx.result)
|
|
448
|
+
* 不注册任何车间 = 行为与版本 2.8.x 完全一致。
|
|
449
|
+
* @param {PipeStage} stage
|
|
450
|
+
* @returns {ChatService} this
|
|
451
|
+
* @throws {ConfigurationError} 参数非法 / 名字被占用 / 与内部车间重名
|
|
452
|
+
*/
|
|
453
|
+
pipe(stage) {
|
|
454
|
+
if (!stage || typeof stage !== "object" || typeof stage.run !== "function") throw new ConfigurationError("pipe: 需要 { name, phase?, run(ctx) },run 必须是函数");
|
|
455
|
+
if (typeof stage.name !== "string" || !stage.name.trim()) throw new ConfigurationError("pipe: 需要 name(车间名,非空字符串)");
|
|
456
|
+
if ([
|
|
457
|
+
"prepareInput",
|
|
458
|
+
"beforeSend",
|
|
459
|
+
"autoContinue",
|
|
460
|
+
"buildRequest",
|
|
461
|
+
"send"
|
|
462
|
+
].includes(stage.name)) throw new ConfigurationError(`pipe: "${stage.name}" 是内部车间名,请换一个名字`);
|
|
463
|
+
if (this._userStages.some((s) => s.name === stage.name)) throw new ConfigurationError(`pipe: 车间 "${stage.name}" 已存在,请先 chat.unpipe("${stage.name}")`);
|
|
464
|
+
const phase = stage.phase === "afterSend" ? "afterSend" : "beforeSend";
|
|
465
|
+
this._userStages.push({
|
|
466
|
+
name: stage.name,
|
|
467
|
+
phase,
|
|
468
|
+
run: stage.run
|
|
469
|
+
});
|
|
470
|
+
this._rebuildPipeline();
|
|
333
471
|
return this;
|
|
334
472
|
}
|
|
473
|
+
/** 移除一个用户车间(移除不存在的名字是安全的)。@returns {ChatService} this */
|
|
474
|
+
unpipe(name) {
|
|
475
|
+
const before = this._userStages.length;
|
|
476
|
+
this._userStages = this._userStages.filter((s) => s.name !== name);
|
|
477
|
+
if (this._userStages.length !== before) this._rebuildPipeline();
|
|
478
|
+
return this;
|
|
479
|
+
}
|
|
480
|
+
/** 查看当前管道里所有车间名(调试/说明用,含内部车间) */
|
|
481
|
+
get pipelineStages() {
|
|
482
|
+
return this._pipeline.names();
|
|
483
|
+
}
|
|
484
|
+
/** 重建内部管道:内部 5 车间 + 用户车间按 phase 插入 */
|
|
485
|
+
_buildPipeline() {
|
|
486
|
+
const p = new Pipeline();
|
|
487
|
+
p.register({
|
|
488
|
+
name: "prepareInput",
|
|
489
|
+
run: (ctx) => this._stagePrepareInput(ctx)
|
|
490
|
+
});
|
|
491
|
+
p.register({
|
|
492
|
+
name: "beforeSend",
|
|
493
|
+
run: (ctx) => this._stageBeforeSend(ctx)
|
|
494
|
+
});
|
|
495
|
+
for (const s of this._userStages) if (s.phase === "beforeSend") p.register({
|
|
496
|
+
name: s.name,
|
|
497
|
+
run: (ctx) => s.run(ctx)
|
|
498
|
+
});
|
|
499
|
+
p.register({
|
|
500
|
+
name: "autoContinue",
|
|
501
|
+
run: (ctx) => this._stageAutoContinue(ctx)
|
|
502
|
+
});
|
|
503
|
+
p.register({
|
|
504
|
+
name: "buildRequest",
|
|
505
|
+
run: (ctx) => this._stageBuildRequest(ctx)
|
|
506
|
+
});
|
|
507
|
+
p.register({
|
|
508
|
+
name: "send",
|
|
509
|
+
run: (ctx) => this._stageSend(ctx)
|
|
510
|
+
});
|
|
511
|
+
for (const s of this._userStages) if (s.phase === "afterSend") p.register({
|
|
512
|
+
name: s.name,
|
|
513
|
+
run: (ctx) => s.run(ctx)
|
|
514
|
+
});
|
|
515
|
+
return p;
|
|
516
|
+
}
|
|
517
|
+
_rebuildPipeline() {
|
|
518
|
+
this._pipeline = this._buildPipeline();
|
|
519
|
+
}
|
|
335
520
|
setAdapter(adapter) {
|
|
521
|
+
assertAdapter(adapter);
|
|
336
522
|
this._adapter = adapter;
|
|
337
523
|
}
|
|
338
524
|
abort() {
|
|
@@ -353,17 +539,24 @@ var ChatService = class extends EventEmitter {
|
|
|
353
539
|
this.messages.update(target.id, {
|
|
354
540
|
_complete: true,
|
|
355
541
|
prefix: void 0,
|
|
356
|
-
_ephemeral:
|
|
542
|
+
_ephemeral: false
|
|
357
543
|
});
|
|
358
544
|
return target;
|
|
359
545
|
} catch (err) {
|
|
360
|
-
this.messages.update(target.id, {
|
|
546
|
+
this.messages.update(target.id, {
|
|
547
|
+
_complete: true,
|
|
548
|
+
prefix: void 0,
|
|
549
|
+
_ephemeral: false
|
|
550
|
+
});
|
|
551
|
+
if (err.name === "AbortError") {
|
|
552
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
553
|
+
return target;
|
|
554
|
+
}
|
|
361
555
|
throw err;
|
|
362
556
|
}
|
|
363
557
|
}
|
|
364
558
|
async continueLastStream(onProgress, onDone) {
|
|
365
559
|
const target = this._prepareContinue();
|
|
366
|
-
console.log("[DEBUG] continueLastStream target.id:", target.id);
|
|
367
560
|
this.messages.update(target.id, { prefix: true });
|
|
368
561
|
const baseLen = (target.content || "").length;
|
|
369
562
|
try {
|
|
@@ -384,11 +577,19 @@ var ChatService = class extends EventEmitter {
|
|
|
384
577
|
this.messages.update(target.id, {
|
|
385
578
|
_complete: true,
|
|
386
579
|
prefix: void 0,
|
|
387
|
-
_ephemeral:
|
|
580
|
+
_ephemeral: false
|
|
388
581
|
});
|
|
389
582
|
return target;
|
|
390
583
|
} catch (err) {
|
|
391
|
-
this.messages.update(target.id, {
|
|
584
|
+
this.messages.update(target.id, {
|
|
585
|
+
_complete: true,
|
|
586
|
+
prefix: void 0,
|
|
587
|
+
_ephemeral: false
|
|
588
|
+
});
|
|
589
|
+
if (err.name === "AbortError") {
|
|
590
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
591
|
+
return target;
|
|
592
|
+
}
|
|
392
593
|
throw err;
|
|
393
594
|
}
|
|
394
595
|
}
|
|
@@ -398,7 +599,20 @@ var ChatService = class extends EventEmitter {
|
|
|
398
599
|
if (last && (last.role === "assistant" || last._ephemeral)) return last;
|
|
399
600
|
throw new Error("最后一条消息不是 assistant,无法续写");
|
|
400
601
|
}
|
|
602
|
+
/**
|
|
603
|
+
* 运行时修改配置(仅会话层字段)。
|
|
604
|
+
*
|
|
605
|
+
* 白名单:model / temperature / maxTokens / modelParams / system / retry / ephemeralContinue。
|
|
606
|
+
* - 运输层(apiKey/baseUrl/headers):归适配器,要改就换适配器实例
|
|
607
|
+
* - 请求参数默认值:走适配器工厂 options 或请求级覆盖(chat.send(x, params))
|
|
608
|
+
* - 能力表:归 modelRegistry 插件
|
|
609
|
+
*
|
|
610
|
+
* 与构造器一样经过校验,不能注入非法值。
|
|
611
|
+
* @throws {ConfigurationError} 非白名单字段或非法值
|
|
612
|
+
*/
|
|
401
613
|
updateConfig(partial) {
|
|
614
|
+
for (const key of Object.keys(partial)) if (!SESSION_CONFIG_KEYS.includes(key)) throw new ConfigurationError(`updateConfig 不支持修改 "${key}"。允许的会话层字段: ${SESSION_CONFIG_KEYS.join(", ")}`);
|
|
615
|
+
this._assertValidConfig(partial);
|
|
402
616
|
Object.assign(this.config, partial);
|
|
403
617
|
if (typeof partial.system === "string") this.systemPrompts.set(partial.system);
|
|
404
618
|
this.emit("config-updated", {
|
|
@@ -406,36 +620,72 @@ var ChatService = class extends EventEmitter {
|
|
|
406
620
|
timestamp: Date.now()
|
|
407
621
|
});
|
|
408
622
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
623
|
+
/**
|
|
624
|
+
* 合并请求参数(近者优先):
|
|
625
|
+
* 适配器默认(getRequestDefaults) ← 会话配置(this.config) ← 本次覆盖(params)
|
|
626
|
+
* modelParams 深层合并;平铺便捷键(temperature/maxTokens/reasoningEffort)自动折叠进 modelParams
|
|
627
|
+
*/
|
|
628
|
+
_mergeRequestConfig(params = {}) {
|
|
629
|
+
const adapterDefaults = this._adapter?.getRequestDefaults?.() || {};
|
|
630
|
+
const requestConfig = {
|
|
631
|
+
...adapterDefaults,
|
|
632
|
+
...this.config,
|
|
633
|
+
...params
|
|
634
|
+
};
|
|
635
|
+
const modelParams = {
|
|
636
|
+
...adapterDefaults.modelParams || {},
|
|
637
|
+
...this.config.modelParams || {},
|
|
638
|
+
...params.modelParams || {}
|
|
639
|
+
};
|
|
640
|
+
for (const key of [
|
|
641
|
+
"temperature",
|
|
642
|
+
"maxTokens",
|
|
643
|
+
"reasoningEffort"
|
|
644
|
+
]) {
|
|
645
|
+
const value = params[key] ?? this.config[key] ?? adapterDefaults[key];
|
|
646
|
+
if (value !== void 0) modelParams[key] = value;
|
|
426
647
|
}
|
|
427
|
-
|
|
428
|
-
|
|
648
|
+
requestConfig.modelParams = modelParams;
|
|
649
|
+
return requestConfig;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* 内部调度(v3.0 提案 · 阶段 1):造一辆小车 ctx,开过内部管道,返回结果。
|
|
653
|
+
* 行为与旧版 _request 完全一致,只是把固定线拆成了车间(见 _stage* 方法)。
|
|
654
|
+
*
|
|
655
|
+
* ctx 字段(车间可见):
|
|
656
|
+
* options —— 原始请求选项
|
|
657
|
+
* userInput —— 用户输入
|
|
658
|
+
* addUser —— 是否新增用户消息
|
|
659
|
+
* messages —— MessageStore(可读可改)
|
|
660
|
+
* systemPrompts —— SystemPromptStore
|
|
661
|
+
* config —— 合并后的请求级配置(prepareInput 车间产出)
|
|
662
|
+
* isStream —— 本次是否流式
|
|
663
|
+
* onProgress —— 流式进度回调(可为 null)
|
|
664
|
+
* onDone —— 流式完成回调(可为 null)
|
|
665
|
+
* mergeToEntry —— 续写目标 id(空 = 普通发送)
|
|
666
|
+
* body —— adapter 构建出的请求体(buildRequest 车间产出)
|
|
667
|
+
* result —— 最终结果(send 车间产出)
|
|
668
|
+
*/
|
|
669
|
+
async _request(options = {}) {
|
|
670
|
+
const ctx = {
|
|
671
|
+
options,
|
|
672
|
+
userInput: options.userInput,
|
|
673
|
+
addUser: options.addUser !== false,
|
|
674
|
+
messages: this.messages,
|
|
675
|
+
systemPrompts: this.systemPrompts,
|
|
676
|
+
config: null,
|
|
677
|
+
isStream: !!options.isStream,
|
|
678
|
+
onProgress: options.onProgress || null,
|
|
679
|
+
onDone: options.onDone || null,
|
|
680
|
+
mergeToEntry: options.mergeToEntry || null,
|
|
681
|
+
body: null,
|
|
682
|
+
result: null
|
|
683
|
+
};
|
|
429
684
|
try {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
onProgress,
|
|
433
|
-
onDone,
|
|
434
|
-
mergeToEntry,
|
|
435
|
-
maxRetries: retryCfg.maxRetries ?? 0,
|
|
436
|
-
retryDelay: retryCfg.retryDelay ?? 1e3
|
|
437
|
-
});
|
|
685
|
+
await this._pipeline.run(ctx);
|
|
686
|
+
return ctx.result;
|
|
438
687
|
} catch (error) {
|
|
688
|
+
if (error.name === "AbortError") throw error;
|
|
439
689
|
this.emit("error", {
|
|
440
690
|
error,
|
|
441
691
|
timestamp: Date.now()
|
|
@@ -443,6 +693,49 @@ var ChatService = class extends EventEmitter {
|
|
|
443
693
|
throw error;
|
|
444
694
|
}
|
|
445
695
|
}
|
|
696
|
+
/** 车间 1:加用户消息 → 合并请求参数 → 广播 sending */
|
|
697
|
+
async _stagePrepareInput(ctx) {
|
|
698
|
+
this._addUserMessage(ctx.userInput, ctx.addUser);
|
|
699
|
+
ctx.config = this._mergeRequestConfig(ctx.options.params);
|
|
700
|
+
this.emit("sending", {
|
|
701
|
+
addUser: ctx.addUser,
|
|
702
|
+
userInput: ctx.userInput,
|
|
703
|
+
timestamp: Date.now()
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
/** 车间 2:发送前钩子(兼容桥:事件钩子照发;阶段 4 会迁到 beforeSend 公开位置) */
|
|
707
|
+
async _stageBeforeSend(ctx) {
|
|
708
|
+
await this._hooks.emitAsync("beforeRequest", {
|
|
709
|
+
messages: ctx.messages,
|
|
710
|
+
config: ctx.config,
|
|
711
|
+
options: ctx.options
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
/** 车间 3:自动续写检测(ephemeralContinue 时,底部若有 prefix 消息则续写) */
|
|
715
|
+
async _stageAutoContinue(ctx) {
|
|
716
|
+
if (this.config.ephemeralContinue && !ctx.mergeToEntry) {
|
|
717
|
+
const msgs = ctx.messages.getAll();
|
|
718
|
+
const last = msgs[msgs.length - 1];
|
|
719
|
+
if (last && last.prefix && (last.role === "assistant" || last._ephemeral)) ctx.mergeToEntry = last.id;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/** 车间 4:构建请求体 */
|
|
723
|
+
async _stageBuildRequest(ctx) {
|
|
724
|
+
ctx.body = this._adapter.buildRequest(ctx.messages.getAll(), ctx.config, ctx.systemPrompts.getEnabled());
|
|
725
|
+
}
|
|
726
|
+
/** 车间 5:发送(流式/非流式 + 重试 + 占位消息 + _processResponse 工具钩子) */
|
|
727
|
+
async _stageSend(ctx) {
|
|
728
|
+
const retryCfg = this.config.retry || {};
|
|
729
|
+
ctx.result = await this._withRetry(ctx.body, {
|
|
730
|
+
isStream: ctx.isStream,
|
|
731
|
+
onProgress: ctx.onProgress,
|
|
732
|
+
onDone: ctx.onDone,
|
|
733
|
+
mergeToEntry: ctx.mergeToEntry,
|
|
734
|
+
config: ctx.config,
|
|
735
|
+
maxRetries: retryCfg.maxRetries ?? 0,
|
|
736
|
+
retryDelay: retryCfg.retryDelay ?? 1e3
|
|
737
|
+
});
|
|
738
|
+
}
|
|
446
739
|
_addUserMessage(userInput, addUser) {
|
|
447
740
|
if (addUser && userInput !== void 0) {
|
|
448
741
|
const msg = typeof userInput === "string" ? {
|
|
@@ -456,7 +749,7 @@ var ChatService = class extends EventEmitter {
|
|
|
456
749
|
}
|
|
457
750
|
}
|
|
458
751
|
/** retry 循环,占位消息只 push 一次 */
|
|
459
|
-
async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, maxRetries, retryDelay }) {
|
|
752
|
+
async _withRetry(body, { isStream, onProgress, onDone, mergeToEntry, config, maxRetries, retryDelay }) {
|
|
460
753
|
let lastError = null;
|
|
461
754
|
const adapterOptions = { signal: this._abortController?.signal };
|
|
462
755
|
let placeholder = null;
|
|
@@ -492,10 +785,11 @@ var ChatService = class extends EventEmitter {
|
|
|
492
785
|
base,
|
|
493
786
|
mergeToEntry,
|
|
494
787
|
onProgress,
|
|
495
|
-
onDone
|
|
788
|
+
onDone,
|
|
789
|
+
config
|
|
496
790
|
});
|
|
497
791
|
else {
|
|
498
|
-
const resp = await this._adapter.send(body,
|
|
792
|
+
const resp = await this._adapter.send(body, config, adapterOptions);
|
|
499
793
|
let result = this._handleResult(this._adapter.parseResponse(resp), mergeToEntry);
|
|
500
794
|
if (this._processResponse && !mergeToEntry) result = await this._processResponse(result, { isStream: false });
|
|
501
795
|
return result;
|
|
@@ -514,12 +808,21 @@ var ChatService = class extends EventEmitter {
|
|
|
514
808
|
this._isGenerating = false;
|
|
515
809
|
throw lastError;
|
|
516
810
|
}
|
|
517
|
-
/**
|
|
518
|
-
|
|
811
|
+
/**
|
|
812
|
+
* 流式处理:ChatService 维护占位消息,adapter 只负责解析 SSE
|
|
813
|
+
*
|
|
814
|
+
* 参数说明:
|
|
815
|
+
* - placeholder:流式期间实时更新的目标消息(续写时 = target 本身,普通发送时 = 新建的空消息)
|
|
816
|
+
* - base:续写时保存的旧内容快照,用于和 API 新内容拼接
|
|
817
|
+
* - mergeToEntry:续写目标的 id,有值时走续写逻辑
|
|
818
|
+
* - onProgress:外部回调,每收到一个 chunk 触发
|
|
819
|
+
* - onDone:流结束回调
|
|
820
|
+
*/
|
|
821
|
+
async _stream(body, adapterOptions, { placeholder, base, mergeToEntry, onProgress, onDone, config }) {
|
|
519
822
|
let finalMsg = null;
|
|
520
|
-
await this._adapter.stream(body,
|
|
521
|
-
placeholder.content = snap.content || "";
|
|
522
|
-
if (snap.reasoningContent) placeholder.reasoningContent = snap.reasoningContent;
|
|
823
|
+
await this._adapter.stream(body, config, (snap) => {
|
|
824
|
+
placeholder.content = mergeToEntry && base ? base.content + (snap.content || "") : snap.content || "";
|
|
825
|
+
if (snap.reasoningContent) placeholder.reasoningContent = mergeToEntry && base ? base.reasoning + snap.reasoningContent : snap.reasoningContent;
|
|
523
826
|
if (snap.toolCalls) placeholder.toolCalls = [...snap.toolCalls];
|
|
524
827
|
this.emit("stream-progress", snap);
|
|
525
828
|
if (onProgress) onProgress(snap);
|
|
@@ -533,7 +836,9 @@ var ChatService = class extends EventEmitter {
|
|
|
533
836
|
if (mergeToEntry && base) {
|
|
534
837
|
const changes = {
|
|
535
838
|
content: base.content + (final.content || ""),
|
|
536
|
-
_complete: true
|
|
839
|
+
_complete: true,
|
|
840
|
+
_ephemeral: false,
|
|
841
|
+
prefix: void 0
|
|
537
842
|
};
|
|
538
843
|
if (final.reasoningContent) changes.reasoningContent = base.reasoning + final.reasoningContent;
|
|
539
844
|
if (final.toolCalls) changes.toolCalls = final.toolCalls;
|
|
@@ -560,7 +865,9 @@ var ChatService = class extends EventEmitter {
|
|
|
560
865
|
}
|
|
561
866
|
const changes = {
|
|
562
867
|
content: (entry.content || "") + (assistantMsg.content || ""),
|
|
563
|
-
_complete: true
|
|
868
|
+
_complete: true,
|
|
869
|
+
_ephemeral: false,
|
|
870
|
+
prefix: void 0
|
|
564
871
|
};
|
|
565
872
|
if (assistantMsg.reasoningContent) changes.reasoningContent = (entry.reasoningContent || "") + assistantMsg.reasoningContent;
|
|
566
873
|
if (assistantMsg.toolCalls) changes.toolCalls = assistantMsg.toolCalls;
|
|
@@ -573,20 +880,44 @@ var ChatService = class extends EventEmitter {
|
|
|
573
880
|
this.emit("message", assistantMsg);
|
|
574
881
|
return assistantMsg;
|
|
575
882
|
}
|
|
576
|
-
|
|
883
|
+
/**
|
|
884
|
+
* 发送消息。
|
|
885
|
+
* @param {string|Object} userInput - 用户输入
|
|
886
|
+
* @param {Object} [params] - 请求级参数覆盖(model/temperature/modelParams 等,仅本次生效)
|
|
887
|
+
*/
|
|
888
|
+
async send(userInput, params) {
|
|
577
889
|
this._abortController = new AbortController();
|
|
578
890
|
try {
|
|
579
891
|
return await this._request({
|
|
580
892
|
userInput,
|
|
581
893
|
addUser: true,
|
|
582
|
-
isStream: false
|
|
894
|
+
isStream: false,
|
|
895
|
+
params
|
|
583
896
|
});
|
|
897
|
+
} catch (err) {
|
|
898
|
+
if (err.name === "AbortError") {
|
|
899
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
throw err;
|
|
584
903
|
} finally {
|
|
585
904
|
this._abortController = null;
|
|
586
905
|
this._isGenerating = false;
|
|
587
906
|
}
|
|
588
907
|
}
|
|
589
|
-
|
|
908
|
+
/**
|
|
909
|
+
* 流式发送。
|
|
910
|
+
* @param {string|Object} userInput - 用户输入
|
|
911
|
+
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
912
|
+
* @param {Function} [onProgress] - 流式进度回调
|
|
913
|
+
* @param {Function} [onDone] - 流式完成回调
|
|
914
|
+
*/
|
|
915
|
+
async stream(userInput, params, onProgress, onDone) {
|
|
916
|
+
if (typeof params === "function") {
|
|
917
|
+
onDone = onProgress;
|
|
918
|
+
onProgress = params;
|
|
919
|
+
params = void 0;
|
|
920
|
+
}
|
|
590
921
|
this._abortController = new AbortController();
|
|
591
922
|
try {
|
|
592
923
|
return await this._request({
|
|
@@ -594,40 +925,92 @@ var ChatService = class extends EventEmitter {
|
|
|
594
925
|
addUser: true,
|
|
595
926
|
isStream: true,
|
|
596
927
|
onProgress,
|
|
597
|
-
onDone
|
|
928
|
+
onDone,
|
|
929
|
+
params
|
|
598
930
|
});
|
|
931
|
+
} catch (err) {
|
|
932
|
+
if (err.name === "AbortError") {
|
|
933
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
throw err;
|
|
599
937
|
} finally {
|
|
600
938
|
this._abortController = null;
|
|
601
939
|
this._isGenerating = false;
|
|
602
940
|
}
|
|
603
941
|
}
|
|
604
|
-
|
|
942
|
+
/**
|
|
943
|
+
* 重发当前消息(不加用户消息)。
|
|
944
|
+
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
945
|
+
*/
|
|
946
|
+
async sendExisting(params) {
|
|
605
947
|
this._abortController = new AbortController();
|
|
606
948
|
try {
|
|
607
949
|
return await this._request({
|
|
608
950
|
addUser: false,
|
|
609
|
-
isStream: false
|
|
951
|
+
isStream: false,
|
|
952
|
+
params
|
|
610
953
|
});
|
|
954
|
+
} catch (err) {
|
|
955
|
+
if (err.name === "AbortError") {
|
|
956
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
throw err;
|
|
611
960
|
} finally {
|
|
612
961
|
this._abortController = null;
|
|
613
962
|
this._isGenerating = false;
|
|
614
963
|
}
|
|
615
964
|
}
|
|
616
|
-
|
|
965
|
+
/**
|
|
966
|
+
* 流式重发(不加用户消息)。
|
|
967
|
+
* @param {Object} [params] - 请求级参数覆盖(仅本次生效)
|
|
968
|
+
* @param {Function} [onProgress] - 流式进度回调
|
|
969
|
+
* @param {Function} [onDone] - 流式完成回调
|
|
970
|
+
*/
|
|
971
|
+
async sendExistingStream(params, onProgress, onDone) {
|
|
972
|
+
if (typeof params === "function") {
|
|
973
|
+
onDone = onProgress;
|
|
974
|
+
onProgress = params;
|
|
975
|
+
params = void 0;
|
|
976
|
+
}
|
|
617
977
|
this._abortController = new AbortController();
|
|
618
978
|
try {
|
|
619
979
|
return await this._request({
|
|
620
980
|
addUser: false,
|
|
621
981
|
isStream: true,
|
|
622
982
|
onProgress,
|
|
623
|
-
onDone
|
|
983
|
+
onDone,
|
|
984
|
+
params
|
|
624
985
|
});
|
|
986
|
+
} catch (err) {
|
|
987
|
+
if (err.name === "AbortError") {
|
|
988
|
+
this.emit("aborted", { timestamp: Date.now() });
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
throw err;
|
|
625
992
|
} finally {
|
|
626
993
|
this._abortController = null;
|
|
627
994
|
this._isGenerating = false;
|
|
628
995
|
}
|
|
629
996
|
}
|
|
630
997
|
};
|
|
998
|
+
/**
|
|
999
|
+
* 校验 adapter 是否满足协议(v3.0 · 阶段 3)。
|
|
1000
|
+
* 缺少必要方法时抛 ConfigurationError,并列出缺少的方法名——写"同级适配器"不再靠猜。
|
|
1001
|
+
* @param {*} adapter
|
|
1002
|
+
* @throws {ConfigurationError}
|
|
1003
|
+
*/
|
|
1004
|
+
function assertAdapter(adapter) {
|
|
1005
|
+
if (!adapter || typeof adapter !== "object") throw new ConfigurationError("setAdapter: adapter 必须是对象(如 openaiAdapter / createOpenAIAdapter() 实例)");
|
|
1006
|
+
const missing = [
|
|
1007
|
+
"buildRequest",
|
|
1008
|
+
"send",
|
|
1009
|
+
"stream",
|
|
1010
|
+
"parseResponse"
|
|
1011
|
+
].filter((k) => typeof adapter[k] !== "function");
|
|
1012
|
+
if (missing.length) throw new ConfigurationError(`setAdapter: adapter 缺少必要方法: ${missing.join(", ")}(协议见 ChatService.js 顶部的 @typedef ChatAdapter)`);
|
|
1013
|
+
}
|
|
631
1014
|
//#endregion
|
|
632
1015
|
//#region src/utils/url.js
|
|
633
1016
|
/**
|
|
@@ -805,17 +1188,48 @@ function assembleMessages(options = {}) {
|
|
|
805
1188
|
* OpenAI 兼容 API 适配器
|
|
806
1189
|
* 支持 DeepSeek 等完全兼容 OpenAI 接口的服务
|
|
807
1190
|
*/
|
|
1191
|
+
var TRANSPORT_KEYS = [
|
|
1192
|
+
"apiKey",
|
|
1193
|
+
"baseUrl",
|
|
1194
|
+
"apiUrl",
|
|
1195
|
+
"path",
|
|
1196
|
+
"headers"
|
|
1197
|
+
];
|
|
808
1198
|
var openaiAdapter = {
|
|
809
1199
|
name: "openai",
|
|
810
|
-
install(chatService) {
|
|
1200
|
+
install(chatService, options = {}) {
|
|
1201
|
+
if (this._transport) this._transport = {
|
|
1202
|
+
...this._transport,
|
|
1203
|
+
...options
|
|
1204
|
+
};
|
|
811
1205
|
chatService.setAdapter(this);
|
|
812
1206
|
},
|
|
1207
|
+
_resolveConfig(config) {
|
|
1208
|
+
const transport = this._transport || {};
|
|
1209
|
+
const resolved = {
|
|
1210
|
+
...config,
|
|
1211
|
+
...transport
|
|
1212
|
+
};
|
|
1213
|
+
if (transport.modelParams) resolved.modelParams = {
|
|
1214
|
+
...transport.modelParams,
|
|
1215
|
+
...config.modelParams || {}
|
|
1216
|
+
};
|
|
1217
|
+
return resolved;
|
|
1218
|
+
},
|
|
1219
|
+
getRequestDefaults() {
|
|
1220
|
+
if (!this._transport) return {};
|
|
1221
|
+
const defaults = {};
|
|
1222
|
+
for (const [key, value] of Object.entries(this._transport)) if (!TRANSPORT_KEYS.includes(key) && value !== void 0) defaults[key] = value;
|
|
1223
|
+
return defaults;
|
|
1224
|
+
},
|
|
813
1225
|
buildRequest(messages, config, systemPrompts = []) {
|
|
1226
|
+
config = this._resolveConfig(config);
|
|
814
1227
|
const model = config.model || config.modelParams?.model;
|
|
815
1228
|
if (!model) throw new Error("Missing required config: model (either at top level or in modelParams)");
|
|
816
|
-
const
|
|
817
|
-
const
|
|
818
|
-
const
|
|
1229
|
+
const mp = config.modelParams || {};
|
|
1230
|
+
const temperature = mp.temperature ?? config.temperature ?? .7;
|
|
1231
|
+
const maxTokens = mp.maxTokens ?? config.maxTokens ?? 2e3;
|
|
1232
|
+
const reasoningEffort = mp.reasoningEffort ?? config.reasoningEffort;
|
|
819
1233
|
const requestBody = {
|
|
820
1234
|
model,
|
|
821
1235
|
messages: MessageFormatter.format({
|
|
@@ -829,11 +1243,30 @@ var openaiAdapter = {
|
|
|
829
1243
|
max_tokens: maxTokens,
|
|
830
1244
|
stream: false
|
|
831
1245
|
};
|
|
1246
|
+
if (mp.topP !== void 0) requestBody.top_p = mp.topP;
|
|
1247
|
+
if (mp.frequencyPenalty !== void 0) requestBody.frequency_penalty = mp.frequencyPenalty;
|
|
1248
|
+
if (mp.presencePenalty !== void 0) requestBody.presence_penalty = mp.presencePenalty;
|
|
1249
|
+
if (mp.stop !== void 0) requestBody.stop = mp.stop;
|
|
1250
|
+
if (mp.responseFormat !== void 0) requestBody.response_format = mp.responseFormat;
|
|
1251
|
+
if (mp.seed !== void 0) requestBody.seed = mp.seed;
|
|
832
1252
|
if (config.tools && Array.isArray(config.tools) && config.tools.length > 0) {
|
|
833
1253
|
requestBody.tools = config.tools;
|
|
834
1254
|
requestBody.tool_choice = "auto";
|
|
835
1255
|
}
|
|
836
|
-
if (reasoningEffort
|
|
1256
|
+
if (reasoningEffort !== void 0) requestBody.reasoning_effort = reasoningEffort;
|
|
1257
|
+
const consumedKeys = new Set([
|
|
1258
|
+
"model",
|
|
1259
|
+
"temperature",
|
|
1260
|
+
"maxTokens",
|
|
1261
|
+
"reasoningEffort",
|
|
1262
|
+
"topP",
|
|
1263
|
+
"frequencyPenalty",
|
|
1264
|
+
"presencePenalty",
|
|
1265
|
+
"stop",
|
|
1266
|
+
"responseFormat",
|
|
1267
|
+
"seed"
|
|
1268
|
+
]);
|
|
1269
|
+
for (const [key, value] of Object.entries(mp)) if (!consumedKeys.has(key) && value !== void 0) requestBody[key] = value;
|
|
837
1270
|
return requestBody;
|
|
838
1271
|
},
|
|
839
1272
|
_getUrl(config) {
|
|
@@ -846,6 +1279,7 @@ var openaiAdapter = {
|
|
|
846
1279
|
},
|
|
847
1280
|
async send(requestBody, config, options = {}) {
|
|
848
1281
|
try {
|
|
1282
|
+
config = this._resolveConfig(config);
|
|
849
1283
|
const url = this._getUrl(config);
|
|
850
1284
|
const headers = {
|
|
851
1285
|
"Content-Type": "application/json",
|
|
@@ -880,6 +1314,7 @@ var openaiAdapter = {
|
|
|
880
1314
|
return internal;
|
|
881
1315
|
},
|
|
882
1316
|
async stream(requestBody, config, onProgress, onDone, options = {}) {
|
|
1317
|
+
config = this._resolveConfig(config);
|
|
883
1318
|
const streamBody = {
|
|
884
1319
|
...requestBody,
|
|
885
1320
|
stream: true
|
|
@@ -973,165 +1408,213 @@ var openaiAdapter = {
|
|
|
973
1408
|
throw new APIError(errorMessage, response.status, null, errorText);
|
|
974
1409
|
}
|
|
975
1410
|
};
|
|
1411
|
+
/**
|
|
1412
|
+
* 创建 OpenAI 兼容适配器实例(工厂)。
|
|
1413
|
+
*
|
|
1414
|
+
* 与单例 openaiAdapter 的区别:每个实例自持一份运输配置(apiKey/baseUrl/apiUrl/path/headers),
|
|
1415
|
+
* 互不干扰——解决"同一适配器装到多个 ChatService 互相覆盖"的单例陷阱。
|
|
1416
|
+
*
|
|
1417
|
+
* 用法:
|
|
1418
|
+
* const adapter = createOpenAIAdapter({ apiKey, baseUrl, modelParams: { temperature: 0.8 } });
|
|
1419
|
+
* const chat = new ChatService({ adapter, model: 'deepseek-chat' });
|
|
1420
|
+
* chat.use(adapter); // 或直接 new ChatService({ adapter })
|
|
1421
|
+
*
|
|
1422
|
+
* 非运输键(model/modelParams/messageFormat/resolveImage/capabilities 等)会作为
|
|
1423
|
+
* "请求默认参数"供会话层合并,单次请求仍可覆盖。
|
|
1424
|
+
*/
|
|
1425
|
+
function createOpenAIAdapter(options = {}) {
|
|
1426
|
+
return {
|
|
1427
|
+
...openaiAdapter,
|
|
1428
|
+
_transport: { ...options }
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
976
1431
|
//#endregion
|
|
977
1432
|
//#region src/plugins/tool-calling.js
|
|
978
1433
|
/**
|
|
979
1434
|
* 工具调用插件
|
|
980
1435
|
* 功能:拦截助手消息中的 tool_calls,执行对应的工具,将结果作为 tool 消息加入对话,
|
|
981
1436
|
* 然后自动继续对话(通过 sendExisting / sendExistingStream),直到没有新的工具调用。
|
|
982
|
-
*
|
|
1437
|
+
*
|
|
983
1438
|
* 设计要点:
|
|
984
1439
|
* - 支持普通请求和流式请求(通过 isStream 标志区分)
|
|
985
1440
|
* - 支持多次工具调用循环(maxIterations 防止无限循环)
|
|
986
|
-
* -
|
|
1441
|
+
* - 并行执行工具
|
|
987
1442
|
* - 工具执行失败时,仍然返回错误信息给 AI,而不是中断整个流程
|
|
988
1443
|
* - 触发 tool-error 事件,方便用户监听工具执行异常
|
|
1444
|
+
*
|
|
1445
|
+
* 注意:请使用 createToolCallingPlugin() 工厂创建实例。
|
|
1446
|
+
* 默认导出的 toolCallingPlugin 是兼容用的模块级单例,装到多个 ChatService
|
|
1447
|
+
* 实例会互相覆盖(executor 表、工具定义、配置),新代码不要直接用单例。
|
|
989
1448
|
*/
|
|
990
|
-
var toolCallingPlugin =
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1449
|
+
var toolCallingPlugin = createToolCallingPlugin();
|
|
1450
|
+
/**
|
|
1451
|
+
* 创建工具调用插件实例(工厂)。
|
|
1452
|
+
* @param {Object} [options] — { timeout, maxIterations }
|
|
1453
|
+
*/
|
|
1454
|
+
function createToolCallingPlugin(options = {}) {
|
|
1455
|
+
return {
|
|
1456
|
+
name: "tool-calling",
|
|
1457
|
+
maxIterations: options.maxIterations || 5,
|
|
1458
|
+
_options: { ...options },
|
|
1459
|
+
_tools: /* @__PURE__ */ new Map(),
|
|
1460
|
+
_toolDefs: [],
|
|
1461
|
+
chatService: null,
|
|
1462
|
+
install(chatService, options = {}) {
|
|
1463
|
+
this._options = {
|
|
1464
|
+
...this._options,
|
|
1465
|
+
...options
|
|
1466
|
+
};
|
|
1467
|
+
this.chatService = chatService;
|
|
1468
|
+
chatService.pipe({
|
|
1469
|
+
name: "tool-calling-inject",
|
|
1470
|
+
phase: "beforeSend",
|
|
1471
|
+
run: ({ config }) => {
|
|
1472
|
+
if (this._toolDefs.length) config.tools = this._toolDefs;
|
|
1473
|
+
}
|
|
1009
1474
|
});
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1475
|
+
/**
|
|
1476
|
+
* 注册工具
|
|
1477
|
+
* @param {string} name - 工具名称(唯一标识)
|
|
1478
|
+
* @param {string} description - 工具描述(告诉 AI 何时调用)
|
|
1479
|
+
* @param {Function} executor - 异步执行函数,接收参数对象,返回结果(字符串或对象)
|
|
1480
|
+
* @param {Object} parameters - JSON Schema 参数定义(可选,默认为空对象)
|
|
1481
|
+
* @returns {ChatService} 返回 chatService 实例,支持链式调用
|
|
1482
|
+
*/
|
|
1483
|
+
chatService.registerTool = (name, description, executor, parameters = {}) => {
|
|
1484
|
+
this._tools.set(name, {
|
|
1485
|
+
executor,
|
|
1486
|
+
description
|
|
1487
|
+
});
|
|
1488
|
+
const toolDefinition = {
|
|
1489
|
+
type: "function",
|
|
1490
|
+
function: {
|
|
1491
|
+
name,
|
|
1492
|
+
description,
|
|
1493
|
+
parameters: {
|
|
1494
|
+
type: "object",
|
|
1495
|
+
properties: parameters,
|
|
1496
|
+
required: Object.keys(parameters).filter((key) => parameters[key]?.required)
|
|
1497
|
+
}
|
|
1019
1498
|
}
|
|
1020
|
-
}
|
|
1499
|
+
};
|
|
1500
|
+
if (!this._toolDefs.find((t) => t.function.name === name)) this._toolDefs.push(toolDefinition);
|
|
1501
|
+
return chatService;
|
|
1021
1502
|
};
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const tool = self._tools.get(toolName);
|
|
1064
|
-
if (!tool) {
|
|
1065
|
-
chat.emit("tool-error", {
|
|
1066
|
-
toolName,
|
|
1067
|
-
error: /* @__PURE__ */ new Error(`Tool not registered: ${toolName}`),
|
|
1068
|
-
toolCallId: call.id,
|
|
1069
|
-
stage: "lookup",
|
|
1070
|
-
timestamp: Date.now()
|
|
1071
|
-
});
|
|
1072
|
-
return {
|
|
1073
|
-
tool_call_id: call.id,
|
|
1074
|
-
error: `工具 ${toolName} 未注册`,
|
|
1075
|
-
success: false
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
try {
|
|
1079
|
-
const toolTimeout = chat.config.toolTimeout;
|
|
1080
|
-
let execPromise = tool.executor(args);
|
|
1081
|
-
if (toolTimeout && typeof toolTimeout === "number" && toolTimeout > 0) {
|
|
1082
|
-
const timeoutErr = /* @__PURE__ */ new Error(`工具 ${toolName} 执行超时 (${toolTimeout}ms)`);
|
|
1083
|
-
timeoutErr.name = "ToolTimeoutError";
|
|
1084
|
-
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(timeoutErr), toolTimeout));
|
|
1085
|
-
execPromise = Promise.race([execPromise, timeoutPromise]);
|
|
1503
|
+
chatService.pipe({
|
|
1504
|
+
name: "tool-calling-loop",
|
|
1505
|
+
phase: "afterSend",
|
|
1506
|
+
run: async (ctx) => {
|
|
1507
|
+
ctx.result = await this._handleWithTools(ctx.result, ctx.isStream, ctx.onProgress || void 0, ctx.onDone || void 0);
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
},
|
|
1511
|
+
async _handleWithTools(initialResponse, isStream, onProgress, onDone) {
|
|
1512
|
+
const self = this;
|
|
1513
|
+
const chat = this.chatService;
|
|
1514
|
+
let iteration = 0;
|
|
1515
|
+
/**
|
|
1516
|
+
* 递归处理工具调用
|
|
1517
|
+
* @param {Object} initialResponse - 初始 AI 响应(可能是第一次请求的响应)
|
|
1518
|
+
* @returns {Promise<Object>} 最终 AI 响应(不含 tool_calls)
|
|
1519
|
+
*/
|
|
1520
|
+
async function processResponse(initialResponse) {
|
|
1521
|
+
let lastResponse = initialResponse;
|
|
1522
|
+
while (iteration < self.maxIterations) {
|
|
1523
|
+
const toolCalls = lastResponse?.toolCalls;
|
|
1524
|
+
if (!toolCalls || toolCalls.length === 0) break;
|
|
1525
|
+
iteration++;
|
|
1526
|
+
const toolResults = await Promise.all(toolCalls.map(async (call) => {
|
|
1527
|
+
const toolName = call.function?.name;
|
|
1528
|
+
let args;
|
|
1529
|
+
try {
|
|
1530
|
+
args = JSON.parse(call.function?.arguments || "{}");
|
|
1531
|
+
} catch (parseErr) {
|
|
1532
|
+
chat.emit("tool-error", {
|
|
1533
|
+
toolName: toolName || "unknown",
|
|
1534
|
+
error: parseErr,
|
|
1535
|
+
toolCallId: call.id,
|
|
1536
|
+
stage: "parse",
|
|
1537
|
+
timestamp: Date.now()
|
|
1538
|
+
});
|
|
1539
|
+
return {
|
|
1540
|
+
tool_call_id: call.id,
|
|
1541
|
+
error: `参数解析失败: ${parseErr.message}`,
|
|
1542
|
+
success: false
|
|
1543
|
+
};
|
|
1086
1544
|
}
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1545
|
+
const tool = self._tools.get(toolName);
|
|
1546
|
+
if (!tool) {
|
|
1547
|
+
chat.emit("tool-error", {
|
|
1548
|
+
toolName,
|
|
1549
|
+
error: /* @__PURE__ */ new Error(`Tool not registered: ${toolName}`),
|
|
1550
|
+
toolCallId: call.id,
|
|
1551
|
+
stage: "lookup",
|
|
1552
|
+
timestamp: Date.now()
|
|
1553
|
+
});
|
|
1554
|
+
return {
|
|
1555
|
+
tool_call_id: call.id,
|
|
1556
|
+
error: `工具 ${toolName} 未注册`,
|
|
1557
|
+
success: false
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
try {
|
|
1561
|
+
const toolTimeout = self._options.timeout ?? chat.config.toolTimeout;
|
|
1562
|
+
let execPromise = tool.executor(args);
|
|
1563
|
+
if (toolTimeout && typeof toolTimeout === "number" && toolTimeout > 0) {
|
|
1564
|
+
const timeoutErr = /* @__PURE__ */ new Error(`工具 ${toolName} 执行超时 (${toolTimeout}ms)`);
|
|
1565
|
+
timeoutErr.name = "ToolTimeoutError";
|
|
1566
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(timeoutErr), toolTimeout));
|
|
1567
|
+
execPromise = Promise.race([execPromise, timeoutPromise]);
|
|
1568
|
+
}
|
|
1569
|
+
const result = await execPromise;
|
|
1570
|
+
chat.emit("tool-success", {
|
|
1571
|
+
toolName,
|
|
1572
|
+
result,
|
|
1573
|
+
toolCallId: call.id,
|
|
1574
|
+
timestamp: Date.now()
|
|
1575
|
+
});
|
|
1576
|
+
return {
|
|
1577
|
+
tool_call_id: call.id,
|
|
1578
|
+
content: typeof result === "string" ? result : JSON.stringify(result),
|
|
1579
|
+
success: true
|
|
1580
|
+
};
|
|
1581
|
+
} catch (err) {
|
|
1582
|
+
const stage = err.name === "ToolTimeoutError" ? "timeout" : "execute";
|
|
1583
|
+
chat.emit("tool-error", {
|
|
1584
|
+
toolName,
|
|
1585
|
+
error: err,
|
|
1586
|
+
toolCallId: call.id,
|
|
1587
|
+
stage,
|
|
1588
|
+
timestamp: Date.now()
|
|
1589
|
+
});
|
|
1590
|
+
return {
|
|
1591
|
+
tool_call_id: call.id,
|
|
1592
|
+
error: err.message,
|
|
1593
|
+
success: false
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
}));
|
|
1597
|
+
for (const tr of toolResults) {
|
|
1598
|
+
const content = tr.success ? tr.content : tr.error || "工具执行失败";
|
|
1599
|
+
chat.messages.addTool(content, tr.tool_call_id);
|
|
1113
1600
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
lastResponse = chunk;
|
|
1123
|
-
}, (final) => {
|
|
1124
|
-
lastResponse = final;
|
|
1125
|
-
resolve();
|
|
1601
|
+
if (isStream) await new Promise((resolve) => {
|
|
1602
|
+
chat.sendExistingStream((chunk) => {
|
|
1603
|
+
if (onProgress) onProgress(chunk);
|
|
1604
|
+
lastResponse = chunk;
|
|
1605
|
+
}, (final) => {
|
|
1606
|
+
lastResponse = final;
|
|
1607
|
+
resolve();
|
|
1608
|
+
});
|
|
1126
1609
|
});
|
|
1127
|
-
|
|
1128
|
-
|
|
1610
|
+
else lastResponse = await chat.sendExisting();
|
|
1611
|
+
}
|
|
1612
|
+
return lastResponse;
|
|
1129
1613
|
}
|
|
1130
|
-
return
|
|
1614
|
+
return await processResponse(initialResponse);
|
|
1131
1615
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
};
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1135
1618
|
//#endregion
|
|
1136
1619
|
//#region src/plugins/model-registry.js
|
|
1137
1620
|
/**
|
|
@@ -1238,57 +1721,84 @@ var DEFAULT_CAPABILITIES = {
|
|
|
1238
1721
|
reasoning: false,
|
|
1239
1722
|
vision: false
|
|
1240
1723
|
};
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1724
|
+
/**
|
|
1725
|
+
* 注意:请使用 createModelRegistryPlugin() 工厂创建实例。
|
|
1726
|
+
* 默认导出的 modelRegistryPlugin 是兼容用的模块级单例,装到多个 ChatService
|
|
1727
|
+
* 实例会互相覆盖(注册表、实例引用),新代码不要直接用单例。
|
|
1728
|
+
*/
|
|
1729
|
+
var modelRegistryPlugin = createModelRegistryPlugin();
|
|
1730
|
+
/**
|
|
1731
|
+
* 创建模型能力注册表插件实例(工厂)。
|
|
1732
|
+
* @param {Object} [options] — { models: { '模型名': capabilities } }(合并进内置表)
|
|
1733
|
+
*/
|
|
1734
|
+
function createModelRegistryPlugin(options = {}) {
|
|
1735
|
+
return {
|
|
1736
|
+
name: "model-registry",
|
|
1737
|
+
_options: { ...options },
|
|
1738
|
+
_registry: new Map([...Object.entries(BUILTIN_MODELS), ...Object.entries(options.models || {})]),
|
|
1739
|
+
chat: null,
|
|
1740
|
+
install(chatService, options = {}) {
|
|
1741
|
+
this._options = {
|
|
1742
|
+
...this._options,
|
|
1743
|
+
...options
|
|
1257
1744
|
};
|
|
1258
|
-
this.
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
chatService.on("config-updated", ({ changes }) => {
|
|
1271
|
-
if ("model" in changes) this._syncCapabilities();
|
|
1272
|
-
if ("capabilities" in changes && !("model" in changes)) {
|
|
1273
|
-
const current = this._lookupCapabilities();
|
|
1274
|
-
if (current) chatService.config.capabilities = {
|
|
1275
|
-
...current,
|
|
1276
|
-
...changes.capabilities
|
|
1745
|
+
this.chat = chatService;
|
|
1746
|
+
/**
|
|
1747
|
+
* 注册/覆盖一个模型的能力标签
|
|
1748
|
+
* @param {string} name — 模型名称
|
|
1749
|
+
* @param {Object} capabilities — 能力标签对象(部分字段即可,未提供的取默认值)
|
|
1750
|
+
* @returns {ChatService}
|
|
1751
|
+
*/
|
|
1752
|
+
chatService.registerModel = (name, capabilities = {}) => {
|
|
1753
|
+
if (!name || typeof name !== "string" || !name.trim()) throw new Error("[model-registry] 模型名称必须是非空字符串");
|
|
1754
|
+
const merged = {
|
|
1755
|
+
...DEFAULT_CAPABILITIES,
|
|
1756
|
+
...capabilities
|
|
1277
1757
|
};
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1758
|
+
this._registry.set(name.trim(), merged);
|
|
1759
|
+
if (chatService.config.model === name.trim()) this._syncCapabilities();
|
|
1760
|
+
return chatService;
|
|
1761
|
+
};
|
|
1762
|
+
/**
|
|
1763
|
+
* 列出所有已注册的模型名称
|
|
1764
|
+
* @returns {Array<string>}
|
|
1765
|
+
*/
|
|
1766
|
+
chatService.listModels = () => {
|
|
1767
|
+
return [...this._registry.keys()];
|
|
1768
|
+
};
|
|
1769
|
+
this._syncCapabilities();
|
|
1770
|
+
chatService.pipe({
|
|
1771
|
+
name: "model-registry-caps",
|
|
1772
|
+
phase: "beforeSend",
|
|
1773
|
+
run: ({ config }) => {
|
|
1774
|
+
if (!config.model) return;
|
|
1775
|
+
const caps = this._registry.get(config.model);
|
|
1776
|
+
if (caps) config.capabilities = { ...caps };
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
chatService.on("config-updated", ({ changes }) => {
|
|
1780
|
+
if ("model" in changes) this._syncCapabilities();
|
|
1781
|
+
if ("capabilities" in changes && !("model" in changes)) {
|
|
1782
|
+
const current = this._lookupCapabilities();
|
|
1783
|
+
if (current) chatService.config.capabilities = {
|
|
1784
|
+
...current,
|
|
1785
|
+
...changes.capabilities
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
},
|
|
1790
|
+
_lookupCapabilities() {
|
|
1791
|
+
const model = this.chat.config.model;
|
|
1792
|
+
if (!model) return null;
|
|
1793
|
+
return this._registry.get(model) || null;
|
|
1794
|
+
},
|
|
1795
|
+
_syncCapabilities() {
|
|
1796
|
+
const caps = this._lookupCapabilities();
|
|
1797
|
+
if (caps) this.chat.config.capabilities = { ...caps };
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1291
1801
|
//#endregion
|
|
1292
|
-
export { APIError, ChatService, ConfigurationError, EventEmitter, MessageFormatter, MessageStore, NetworkError, ParsingError, SystemPromptStore, assembleMessages, modelRegistryPlugin, openaiAdapter, toolCallingPlugin };
|
|
1802
|
+
export { APIError, ChatService, ConfigurationError, EventEmitter, MessageFormatter, MessageStore, NetworkError, ParsingError, Pipeline, SystemPromptStore, assembleMessages, assertAdapter, createModelRegistryPlugin, createOpenAIAdapter, createToolCallingPlugin, modelRegistryPlugin, openaiAdapter, toolCallingPlugin };
|
|
1293
1803
|
|
|
1294
1804
|
//# sourceMappingURL=my-ai-chat-framework.browser.es.js.map
|