my-ai-chat-framework 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +3 -3
- package/README_ZH.md +22 -5
- package/dist/my-ai-chat-framework.browser.es.js +599 -292
- package/dist/my-ai-chat-framework.browser.es.js.map +1 -1
- package/dist/my-ai-chat-framework.browser.umd.js +601 -291
- package/dist/my-ai-chat-framework.browser.umd.js.map +1 -1
- package/dist/my-ai-chat-framework.node.cjs.js +601 -291
- package/dist/my-ai-chat-framework.node.cjs.js.map +1 -1
- package/docs/DEVELOPER.md +100 -10
- package/docs/README.md +33 -0
- package/package.json +1 -1
- package/src/adapters/openai.js +44 -18
- package/src/core/ChatService.js +250 -141
- package/src/core/Errors.js +13 -0
- package/src/core/SystemPromptStore.js +7 -5
- package/src/index.js +2 -1
- package/src/plugins/continuation.js +156 -0
- package/src/plugins/model-registry.js +16 -3
- package/src/plugins/tool-calling.js +46 -18
- package/src/utils/MessageFormatter.js +138 -46
package/docs/DEVELOPER.md
CHANGED
|
@@ -111,13 +111,13 @@ chat.use(openaiAdapter); // 无状态单例,配置从 chat.config 读取
|
|
|
111
111
|
| `chat.stream(input, params?, onProgress, onDone)` | 流式发送。兼容旧签名 `stream(input, onProgress, onDone)`。AbortError 内部捕获 |
|
|
112
112
|
| `chat.sendExisting(params?)` | 重发当前消息(不加用户消息)。AbortError 内部捕获 |
|
|
113
113
|
| `chat.sendExistingStream(params?, onProgress, onDone)` | 流式重发。兼容旧签名。AbortError 内部捕获 |
|
|
114
|
-
| `chat.continueLast()` | 前缀续写:从最后一条 assistant/ephemeral 续写并拼回原消息,续写后自动转正。AbortError 内部捕获,返回 target |
|
|
115
|
-
| `chat.continueLastStream(onProgress, onDone)` | 流式前缀续写。AbortError 内部捕获,返回 target |
|
|
114
|
+
| `chat.plugins['continuation'].continueLast()` | 前缀续写:从最后一条 assistant/ephemeral 续写并拼回原消息,续写后自动转正。AbortError 内部捕获,返回 target |
|
|
115
|
+
| `chat.plugins['continuation'].continueLastStream(onProgress, onDone)` | 流式前缀续写。AbortError 内部捕获,返回 target |
|
|
116
116
|
| `chat.abort()` | 中断当前请求,触发 `aborted` 事件 |
|
|
117
117
|
| `chat.use(plugin, options?)` | 安装插件,options 透传给 `plugin.install(chat, options)` |
|
|
118
118
|
| `chat.updateConfig(partial)` | 更新会话层配置(白名单+校验),触发 `config-updated` |
|
|
119
|
-
| `chat.registerTool(name, desc, fn, params?)` | 注册工具(需 toolCallingPlugin) |
|
|
120
|
-
| `chat.registerModel(name, caps)` | 注册/覆盖模型能力(需 modelRegistryPlugin) |
|
|
119
|
+
| `chat.plugins['tool-calling'].registerTool(name, desc, fn, params?)` | 注册工具(需 toolCallingPlugin) |
|
|
120
|
+
| `chat.plugins['model-registry'].registerModel(name, caps)` | 注册/覆盖模型能力(需 modelRegistryPlugin) |
|
|
121
121
|
|
|
122
122
|
### 属性
|
|
123
123
|
|
|
@@ -263,7 +263,7 @@ chat._hooks.on('beforeRequest', async ({ messages }) => {
|
|
|
263
263
|
});
|
|
264
264
|
|
|
265
265
|
// 续写后自动转正
|
|
266
|
-
await chat.continueLast();
|
|
266
|
+
await chat.plugins['continuation'].continueLast();
|
|
267
267
|
// 该消息的 _ephemeral 已被清除,成为永久 assistant
|
|
268
268
|
```
|
|
269
269
|
|
|
@@ -322,7 +322,7 @@ console.log(MessageFormatter.listFormats()); // ['openai', 'anthropic']
|
|
|
322
322
|
|
|
323
323
|
## 前缀续写
|
|
324
324
|
|
|
325
|
-
调用 `chat.continueLast()` 从最后一条 assistant 消息续写——
|
|
325
|
+
调用 `chat.plugins['continuation'].continueLast()` 从最后一条 assistant 消息续写——
|
|
326
326
|
模型直接从原内容末尾接续,结果拼回原消息(不新增消息)。
|
|
327
327
|
|
|
328
328
|
> 部分服务商需 baseUrl 加 `/beta` 开启该功能(如 DeepSeek)。
|
|
@@ -330,11 +330,11 @@ console.log(MessageFormatter.listFormats()); // ['openai', 'anthropic']
|
|
|
330
330
|
```javascript
|
|
331
331
|
// 中断后直接续写(流式标记 _complete 为 false)
|
|
332
332
|
chat.abort();
|
|
333
|
-
const msg = await chat.continueLast();
|
|
333
|
+
const msg = await chat.plugins['continuation'].continueLast();
|
|
334
334
|
// msg.content 已是原始 + 续写拼接,_complete 变为 true
|
|
335
335
|
|
|
336
336
|
// 已完成的对话也可以续写(但通常没必要)
|
|
337
|
-
await chat.continueLast();
|
|
337
|
+
await chat.plugins['continuation'].continueLast();
|
|
338
338
|
```
|
|
339
339
|
|
|
340
340
|
---
|
|
@@ -423,7 +423,7 @@ chat.use(openaiAdapter);
|
|
|
423
423
|
chat.use(modelRegistryPlugin);
|
|
424
424
|
chat.use(toolCallingPlugin);
|
|
425
425
|
|
|
426
|
-
chat.registerTool('get_weather', '查询天气', async (args) => {
|
|
426
|
+
chat.plugins['tool-calling'].registerTool('get_weather', '查询天气', async (args) => {
|
|
427
427
|
return `${args.city}: 22°C, 晴`;
|
|
428
428
|
}, { city: { type: 'string', description: '城市', required: true } });
|
|
429
429
|
|
|
@@ -441,7 +441,7 @@ setTimeout(() => chat.abort(), 3000);
|
|
|
441
441
|
await chat.stream('讲个长故事', ...);
|
|
442
442
|
|
|
443
443
|
// 前缀续写(从最后一条 assistant 续写)
|
|
444
|
-
await chat.continueLast();
|
|
444
|
+
await chat.plugins['continuation'].continueLast();
|
|
445
445
|
// 结果拼回原消息,不新增消息
|
|
446
446
|
```
|
|
447
447
|
|
|
@@ -477,3 +477,93 @@ chat.unpipe('角色卡注入');
|
|
|
477
477
|
```
|
|
478
478
|
|
|
479
479
|
车间即"同级功能":tool-calling / model-registry 本身现在就是车间(见 src/plugins/)。ctx 的全部字段见 ChatService.js 顶部 @typedef PipelineContext。
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## 出口:替换 / 关闭内部步骤(v3.1)
|
|
484
|
+
|
|
485
|
+
框架的 5 个内部步骤都能被替换、关闭、包装——"加功能只能改源码"成为历史。
|
|
486
|
+
|
|
487
|
+
```js
|
|
488
|
+
chat.internalStages // ['prepareInput','beforeSend','autoContinue','buildRequest','send']
|
|
489
|
+
|
|
490
|
+
// 1) 完全接管"发送"这一步(框架不再插手)
|
|
491
|
+
chat.replaceStage('send', async (ctx) => {
|
|
492
|
+
ctx.result = await 我自己的发送逻辑(ctx.body);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// 2) 包一层:先做自己的事,再交回默认实现
|
|
496
|
+
const 默认发送 = chat.getStage('send');
|
|
497
|
+
chat.replaceStage('send', async (ctx) => { 记录日志(ctx); await 默认发送(ctx); });
|
|
498
|
+
|
|
499
|
+
// 3) 彻底关掉某一步
|
|
500
|
+
chat.unstage('autoContinue');
|
|
501
|
+
|
|
502
|
+
// 4) 反悔
|
|
503
|
+
chat.restoreStage('send'); // 恢复默认实现
|
|
504
|
+
chat.restage('autoContinue'); // 重新启用
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## 临时消息:用业务规则实现(v4.0)
|
|
510
|
+
|
|
511
|
+
> v4.0 起,**核心不认识 ephemeral(临时消息)与续写** —— 它们是玩法,住在 `src/plugins/continuation.js`。
|
|
512
|
+
> 两条路,按需选。
|
|
513
|
+
|
|
514
|
+
### 场景 A:不装插件 —— 一次性注入(最轻)
|
|
515
|
+
|
|
516
|
+
```js
|
|
517
|
+
// 让一段内容"只影响本次请求、不进历史"
|
|
518
|
+
chat.pipe({
|
|
519
|
+
name: '临时引导',
|
|
520
|
+
phase: 'beforeSend',
|
|
521
|
+
run(ctx) { ctx.config.guideText = '接下来用三句话总结'; }
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// 在你自己写的 adapter.buildRequest 里,把 config.guideText 变成请求里的一条消息
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### 场景 B:装插件 —— 模拟思维链 / 前缀续写
|
|
528
|
+
|
|
529
|
+
```js
|
|
530
|
+
import { createContinuationPlugin } from 'my-ai-chat-framework';
|
|
531
|
+
|
|
532
|
+
chat.use(createContinuationPlugin({ autoContinue: true }));
|
|
533
|
+
|
|
534
|
+
// 1) 模拟思维链:注入临时引导,结果并回上一条 assistant,不留痕
|
|
535
|
+
chat.messages.addOnceAssistant('(内心:他肯定又要熬夜)');
|
|
536
|
+
await chat.stream('在吗');
|
|
537
|
+
|
|
538
|
+
// 2) 前缀续写:让 AI 接着最后一条 assistant 往下写
|
|
539
|
+
await chat.plugins['continuation'].continueLast();
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### 插件到底装了什么(可整体卸载)
|
|
543
|
+
|
|
544
|
+
| 装的东西 | 用到的机制 |
|
|
545
|
+
|---|---|
|
|
546
|
+
| 续写检测(`autoContinue` 步骤) | `chat.replaceStage('autoContinue', …)`(v3.1 出口) |
|
|
547
|
+
| ephemeral 过滤规则 | `MessageFormatter.registerFilter('ephemeral', …)`(v4.0 过滤器注册点) |
|
|
548
|
+
| `chat.plugins['continuation'].continueLast` / `chat.plugins['continuation'].continueLastStream` | 挂到实例上 |
|
|
549
|
+
| `plugin.uninstall(chat)` | 全部还原(步骤 / 过滤器 / 方法) |
|
|
550
|
+
|
|
551
|
+
### 只要规则、不要插件
|
|
552
|
+
|
|
553
|
+
```js
|
|
554
|
+
import { ephemeralFilterRule } from 'my-ai-chat-framework';
|
|
555
|
+
MessageFormatter.registerFilter('ephemeral', ephemeralFilterRule, { priority: 100 });
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### 自定义消息过滤(决定哪些消息进入请求)
|
|
559
|
+
|
|
560
|
+
```js
|
|
561
|
+
MessageFormatter.registerFilter('我的规则', (msg, { index, messages, capabilities }) => {
|
|
562
|
+
if (msg.metadata?.hidden) return null; // 丢弃
|
|
563
|
+
return msg; // 保留(也可返回改写后的新对象)
|
|
564
|
+
}, { priority: 100 }); // 数字越小越先执行
|
|
565
|
+
|
|
566
|
+
MessageFormatter.listFilters(); // 查看当前过滤器链
|
|
567
|
+
MessageFormatter.unregisterFilter('我的规则'); // 移除
|
|
568
|
+
```
|
|
569
|
+
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# 文档索引
|
|
2
|
+
|
|
3
|
+
> 按"你现在想干什么"挑一份看。
|
|
4
|
+
|
|
5
|
+
| 我想…… | 看这份 |
|
|
6
|
+
|---|---|
|
|
7
|
+
| **快速用起来**(抄代码) | [COOKBOOK.md](./COOKBOOK.md) —— 16 个常见任务配方 |
|
|
8
|
+
| **查某个方法/事件/错误** | [API-REFERENCE.md](./API-REFERENCE.md) —— 完整 API 参考 |
|
|
9
|
+
| **搞懂它为什么这么设计** | [ARCHITECTURE.md](./ARCHITECTURE.md) —— 架构、数据流、五种扩展方式 |
|
|
10
|
+
| **确认哪些能改、哪些不能改** | [API-STABILITY.md](./API-STABILITY.md) —— 稳定性契约(Stable/Experimental/Internal) |
|
|
11
|
+
| **理解"框架 vs 库"、责任边界** | [DESIGN-boundaries.md](./DESIGN-boundaries.md) —— 责任边界与出口缺口 |
|
|
12
|
+
| **上手时的注意点 / 试用结论** | [FRAMEWORK-REVIEW.md](./FRAMEWORK-REVIEW.md) —— 试用报告(5 个易踩点) |
|
|
13
|
+
| **发布到 npm** | [PUBLISHING.md](./PUBLISHING.md) —— 发布流程(含 2027 变化) |
|
|
14
|
+
| **看还有什么没做** | [TODO.md](./TODO.md) —— 路线图与已知问题 |
|
|
15
|
+
| **开发这个框架本身** | [DEVELOPER.md](./DEVELOPER.md) —— 扩展教程(写车间/出口/临时消息) |
|
|
16
|
+
| **让 AI 帮你改这个仓库** | [../AGENTS.md](../AGENTS.md) · [../CLAUDE.md](../CLAUDE.md) —— 给 AI 的说明书 |
|
|
17
|
+
|
|
18
|
+
## 推荐阅读顺序(第一次接触)
|
|
19
|
+
|
|
20
|
+
1. 仓库根目录 [README_ZH.md](../README_ZH.md) —— 5 分钟了解是什么
|
|
21
|
+
2. [COOKBOOK.md](./COOKBOOK.md) 的配方 1、3、4 —— 能跑起来、能流式、能注入角色卡
|
|
22
|
+
3. [ARCHITECTURE.md](./ARCHITECTURE.md) 的第三节(生命周期)+ 第四节(五种扩展方式)
|
|
23
|
+
4. [API-STABILITY.md](./API-STABILITY.md) —— 记住"只加不改",然后放心写业务
|
|
24
|
+
5. 剩下按需查
|
|
25
|
+
|
|
26
|
+
## 可运行的示例
|
|
27
|
+
|
|
28
|
+
| 文件 | 用途 |
|
|
29
|
+
|---|---|
|
|
30
|
+
| [../examples/selfcheck.js](../examples/selfcheck.js) | 无网络自检 21 项:`node examples/selfcheck.js` |
|
|
31
|
+
| [../examples/demo.html](../examples/demo.html) | 浏览器真实对话(含车间开关、插件开关) |
|
|
32
|
+
| [../tests/pipeline.test.js](../tests/pipeline.test.js) | **测试即文档**:36 个用例,每个演示一种用法 |
|
|
33
|
+
| [../tests/core.test.js](../tests/core.test.js) | 48 个基础用例(存储/配置/适配器/过滤器) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-ai-chat-framework",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "A lightweight AI chat framework with plugin system, unified message format, and tool calling support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/my-ai-chat-framework.node.cjs.js",
|
package/src/adapters/openai.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { joinUrl } from "../utils/url.js";
|
|
7
7
|
import { isString } from "../utils/typeCheck.js";
|
|
8
|
-
import { APIError, NetworkError, ParsingError } from '../core/Errors.js';
|
|
8
|
+
import { APIError, NetworkError, ParsingError, ConfigurationError } from '../core/Errors.js';
|
|
9
9
|
import { MessageFormatter } from '../utils/MessageFormatter.js';
|
|
10
10
|
|
|
11
11
|
// 属于"运输层"的配置键:工厂实例持有,请求时覆盖 config 中同名项
|
|
@@ -59,12 +59,13 @@ export const openaiAdapter = {
|
|
|
59
59
|
config = this._resolveConfig(config);
|
|
60
60
|
const model = config.model || config.modelParams?.model;
|
|
61
61
|
if (!model) {
|
|
62
|
-
throw new
|
|
62
|
+
throw new ConfigurationError('Missing required config: model (either at top level or in modelParams)');
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
const mp = config.modelParams || {};
|
|
66
|
-
|
|
67
|
-
const
|
|
66
|
+
// v4.0:**不再提供默认值** —— 没传就不放进请求体,由 API 用自己的默认
|
|
67
|
+
const temperature = mp.temperature ?? config.temperature;
|
|
68
|
+
const maxTokens = mp.maxTokens ?? config.maxTokens;
|
|
68
69
|
const reasoningEffort = mp.reasoningEffort ?? config.reasoningEffort;
|
|
69
70
|
|
|
70
71
|
// 用 MessageFormatter 统一处理:system 置顶、消息过滤、字段映射、图片解析
|
|
@@ -80,11 +81,13 @@ export const openaiAdapter = {
|
|
|
80
81
|
const requestBody = {
|
|
81
82
|
model,
|
|
82
83
|
messages: apiMessages,
|
|
83
|
-
temperature,
|
|
84
|
-
max_tokens: maxTokens,
|
|
85
84
|
stream: false
|
|
86
85
|
};
|
|
87
86
|
|
|
87
|
+
// v4.0:可选参数只在"显式传了"时才放进请求体(不替使用者决定要什么默认值)
|
|
88
|
+
if (temperature !== undefined) requestBody.temperature = temperature;
|
|
89
|
+
if (maxTokens !== undefined) requestBody.max_tokens = maxTokens;
|
|
90
|
+
|
|
88
91
|
// 硬编码常用参数:驼峰命名,适配器负责转 API 格式
|
|
89
92
|
if (mp.topP !== undefined) requestBody.top_p = mp.topP;
|
|
90
93
|
if (mp.frequencyPenalty !== undefined) requestBody.frequency_penalty = mp.frequencyPenalty;
|
|
@@ -262,20 +265,37 @@ export const openaiAdapter = {
|
|
|
262
265
|
const { done, value } = await reader.read();
|
|
263
266
|
if (done) break;
|
|
264
267
|
buffer += decoder.decode(value, { stream: true });
|
|
265
|
-
const lines = buffer.split(/\n\n/);
|
|
266
|
-
buffer = lines.pop();
|
|
267
268
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
269
|
+
// ===== v4.0:稳健的 SSE 事件解析 =====
|
|
270
|
+
// 支持:\r\n 换行、`data:`(无空格)、多行 data(按 SSE 规范用 \n 连接)、注释行
|
|
271
|
+
let sepMatch;
|
|
272
|
+
const EVENT_SEP = /\r?\n\r?\n/;
|
|
273
|
+
while ((sepMatch = EVENT_SEP.exec(buffer)) !== null) {
|
|
274
|
+
const rawEvent = buffer.slice(0, sepMatch.index);
|
|
275
|
+
buffer = buffer.slice(sepMatch.index + sepMatch[0].length);
|
|
276
|
+
|
|
277
|
+
const dataPayload = rawEvent
|
|
278
|
+
.split(/\r?\n/)
|
|
279
|
+
.filter(l => l.startsWith('data:'))
|
|
280
|
+
.map(l => l.slice(5).replace(/^ /, ''))
|
|
281
|
+
.join('\n');
|
|
282
|
+
if (!dataPayload) continue;
|
|
283
|
+
if (dataPayload.trim() === '[DONE]') { reachedDone = true; break; }
|
|
284
|
+
|
|
272
285
|
try {
|
|
273
|
-
const chunk = JSON.parse(
|
|
286
|
+
const chunk = JSON.parse(dataPayload);
|
|
274
287
|
const delta = chunk.choices?.[0]?.delta;
|
|
275
288
|
if (!delta) continue;
|
|
276
289
|
|
|
277
|
-
|
|
290
|
+
// v4.0:本次 chunk 的"增量"(onProgress 第二个参数)
|
|
291
|
+
const deltaInfo = { content: '', reasoningContent: '', toolCalls: null };
|
|
292
|
+
|
|
293
|
+
if (delta.content) {
|
|
294
|
+
accumulated.content += delta.content;
|
|
295
|
+
deltaInfo.content = delta.content;
|
|
296
|
+
}
|
|
278
297
|
if (delta.tool_calls) {
|
|
298
|
+
const newCalls = [];
|
|
279
299
|
if (!accumulated.toolCalls) accumulated.toolCalls = [];
|
|
280
300
|
for (const toolCallDelta of delta.tool_calls) {
|
|
281
301
|
const index = toolCallDelta.index;
|
|
@@ -294,16 +314,22 @@ export const openaiAdapter = {
|
|
|
294
314
|
if (toolCallDelta.function?.arguments) {
|
|
295
315
|
accumulated.toolCalls[index].function.arguments += toolCallDelta.function.arguments;
|
|
296
316
|
}
|
|
317
|
+
newCalls.push({ index, delta: toolCallDelta });
|
|
297
318
|
}
|
|
319
|
+
deltaInfo.toolCalls = newCalls;
|
|
320
|
+
}
|
|
321
|
+
if (delta.reasoning_content) {
|
|
322
|
+
accumulated.reasoningContent += delta.reasoning_content;
|
|
323
|
+
deltaInfo.reasoningContent = delta.reasoning_content;
|
|
298
324
|
}
|
|
299
|
-
if (delta.reasoning_content) accumulated.reasoningContent += delta.reasoning_content;
|
|
300
325
|
|
|
301
|
-
//
|
|
302
|
-
onProgress({ ...accumulated });
|
|
326
|
+
// onProgress(snap, delta):snap = 全量累积快照;delta = 本次新增片段
|
|
327
|
+
onProgress({ ...accumulated }, deltaInfo);
|
|
303
328
|
} catch (e) {
|
|
304
|
-
console.warn('stream parsing failed :', e,
|
|
329
|
+
console.warn('stream parsing failed :', e, dataPayload);
|
|
305
330
|
}
|
|
306
331
|
}
|
|
332
|
+
if (reachedDone) break;
|
|
307
333
|
}
|
|
308
334
|
|
|
309
335
|
const finalMessage = {
|