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/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.0.0",
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",
@@ -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 Error('Missing required config: model (either at top level or in modelParams)');
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
- const temperature = mp.temperature ?? config.temperature ?? 0.7;
67
- const maxTokens = mp.maxTokens ?? config.maxTokens ?? 2000;
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
- for (const line of lines) {
269
- const dataLine = line.replace(/^data: /, '').trim();
270
- if (!dataLine) continue;
271
- if (dataLine === '[DONE]') { reachedDone = true; break; }
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(dataLine);
286
+ const chunk = JSON.parse(dataPayload);
274
287
  const delta = chunk.choices?.[0]?.delta;
275
288
  if (!delta) continue;
276
289
 
277
- if (delta.content) accumulated.content += delta.content;
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
- // 传出累积快照,ChatService 自行处理占位消息更新
302
- onProgress({ ...accumulated });
326
+ // onProgress(snap, delta):snap = 全量累积快照;delta = 本次新增片段
327
+ onProgress({ ...accumulated }, deltaInfo);
303
328
  } catch (e) {
304
- console.warn('stream parsing failed :', e, dataLine);
329
+ console.warn('stream parsing failed :', e, dataPayload);
305
330
  }
306
331
  }
332
+ if (reachedDone) break;
307
333
  }
308
334
 
309
335
  const finalMessage = {