my-ai-chat-framework 2.7.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,117 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [4.0.0] - 2026-09
6
+
7
+ > 主题:**把"玩法"从核心搬出去,并给核心补上"出口"**。
8
+ > 一句话:核心只保留"怎么合并、怎么发、怎么映射字段";"续写 / 临时消息"变成可选的 continuation 插件。
9
+
10
+ ### Added(清债 · A 组)
11
+ - **A1 删旧通道**:`_hooks` / `beforeRequest` 钩子 / `_processResponse` 全部移除(改用 `pipe` 车间 / `replaceStage`);`beforeSend` 步骤改空实现
12
+ - **A2 可扩展配置白名单**:`registerConfigKeys(keys)` / `unregisterConfigKeys` / `configKeys`
13
+ —— 插件自带配置(如 continuation 的 `autoContinue`)可运行时修改,**不必回头改核心**
14
+ - **A3 错误类型统一**:新增 `ValidationError extends ConfigurationError`,15 处裸 `Error` 全部类型化
15
+ - **A5 流式增量**:`onProgress(snap, delta)` —— `snap` 全量、`delta` 本次新增
16
+ - **A6 SSE 健壮化**:支持 `\r\n`、`data:`(无空格)、多行 `data` 拼接
17
+
18
+ ### Changed(架构 · B 组)
19
+ - **B1 每请求独立中断**:`abort()` 中断全部 / `abort(id)` 精确中断 / `activeRequests`;多请求互不干扰(旧版共享单个 controller,会中断错的那个)
20
+ - **B3 消息存储可注入**:`new ChatService({ store })`(为将来的对话树/自定义存储留口)
21
+ - **B4 插件命名空间**:插件 API 挂到 `chat.plugins.<插件名>`,不再直接挂 `chat`(撞名从根上消失);三个插件都提供 `uninstall`
22
+ - **A4 工具去限制 / 参数不默认**(BREAKING):
23
+ - 工具循环**默认不限轮数**(旧版硬编码 5 轮);`timeout` 只在显式传入时生效,并修掉超时泄漏(clearTimeout + AbortController 通知工具)
24
+ - `registerTool` 生成标准 JSON Schema(必填项进顶层 `required`,properties 不再残留)
25
+ - `temperature` / `maxTokens` **不再有默认值** —— 不传就不放进请求体(由 API 用自己的默认)
26
+
27
+ ### Docs
28
+ - **docs/API-STABILITY.md**:稳定性契约(Stable 清单 / 四个协议 / 事件 payload 形状 / 兼容纪律 / v3→v4 迁移记录)
29
+
30
+ ### Added
31
+ - **出口 API**:`chat.replaceStage(name, fn)` / `restoreStage` / `unstage` / `restage` / `getStage` / `internalStages`
32
+ —— 5 个内部步骤(prepareInput / beforeSend / autoContinue / buildRequest / send)可替换、可关闭、可包装
33
+ - **消息过滤器注册点**:`MessageFormatter.registerFilter(name, fn, { priority })` / `unregisterFilter` / `listFilters`
34
+ —— 决定"哪些消息进入请求"的规则变成可插拔链(同名覆盖,返回旧函数)
35
+ - **continuation 插件**(`src/plugins/continuation.js`,随框架发布,非独立包):
36
+ - 续写检测(`replaceStage('autoContinue', …)`)、ephemeral 过滤规则、`chat.continueLast` / `continueLastStream`
37
+ - `createContinuationPlugin({ autoContinue = false })` + `plugin.uninstall(chat)` 完整还原
38
+ - 导出 `ephemeralFilterRule` / `prepareContinue`(只要规则不要插件时可用)
39
+ - `sendExisting(params, { mergeToEntry })` / `sendExistingStream(params, { mergeToEntry }, onProgress, onDone)`
40
+ —— "把结果并回指定消息"成为公开能力(兼容旧签名)
41
+
42
+ ### Changed(BREAKING)
43
+ - **`new ChatService()` 不再自带续写与临时消息**:核心删除了 `continueLast` / `continueLastStream` / `_prepareContinue`,
44
+ `autoContinue` 步骤改为空实现,`MessageFormatter` 不再内置 ephemeral 过滤规则
45
+ → 需要这些能力的:`chat.use(createContinuationPlugin({ autoContinue: true }))`
46
+ - `config.ephemeralContinue` 移除(`updateConfig` 白名单同步收窄);改用插件选项 `autoContinue`
47
+ - 消息"保留/丢弃"规则改为过滤器实现(行为与 v3.x 一致:ephemeral 底部连续、system 分流、空内容清理)
48
+
49
+ ### 迁移(从 3.x 升到 4.0)
50
+ ```js
51
+ // 旧:什么都不用做,续写开箱可用
52
+ // 新:一行装配
53
+ import { createContinuationPlugin } from 'my-ai-chat-framework';
54
+ chat.use(createContinuationPlugin({ autoContinue: true }));
55
+ ```
56
+
57
+ ## [3.0.0] - 2026-08-?
58
+
59
+ ### Added
60
+ - **公开管道 API**:`chat.pipe({ name, phase?, run(ctx) })` / `chat.unpipe(name)` / `chat.pipelineStages`
61
+ - phase `beforeSend`(默认):内部 beforeRequest 之后、构建请求体之前
62
+ - phase `afterSend`:发送完成、结果落位之后(可改 ctx.result)
63
+ - 不注册任何车间 = 行为与 2.8.x 完全一致
64
+ - **协议固化**:`assertAdapter` 校验 buildRequest/send/stream/parseResponse,缺方法抛 ConfigurationError 并列出方法名;JSDoc @typedef ChatAdapter / PipelineContext / PipeStage
65
+ - **插件车间化**:tool-calling 经 afterSend 车间、model-registry 经 beforeSend 车间,不再占用 `_processResponse` 单座槽位
66
+ - 新增测试 `tests/pipeline.test.js`(8 例:pipe 顺序/冲突/unpipe/改ctx/SSE 冒烟/工具循环端到端)
67
+
68
+ ### Changed
69
+ - `ChatService._request` 固定线拆为内部管道:prepareInput → beforeSend → autoContinue → buildRequest → send(行为等价)
70
+ - `_hooks` beforeRequest 保留为兼容桥;`_processResponse` 保留为旧通道(新代码请用 afterSend 车间)
71
+ - 版本号 2.8.0 → 3.0.0(新增公共 API 面)
72
+
73
+ ### Known delta(流式 tool-calling 时序)
74
+ - 流式工具调用时,初始回复的 `message` 事件在工具循环前发出(旧版循环后发)。非流式时序与旧版一致。
75
+
76
+ ## [2.8.0] - 2026-08-08
77
+
78
+ ### Added
79
+ - **配置归属系统**:配置按层归属(运输/请求/会话/插件),谁消费谁声明
80
+ - **工厂函数**:`createOpenAIAdapter(opts)` / `createToolCallingPlugin(opts)` / `createModelRegistryPlugin(opts)`——每个实例自持状态,规避模块级单例互踩陷阱
81
+ - **请求级参数覆盖**:`chat.send(input, params)` / `chat.stream(input, params, onProgress, onDone)` / `sendExisting(params)` / `sendExistingStream(params, ...)`,合并链 = 适配器默认 ← 会话配置 ← 本次覆盖(近者优先)
82
+ - **capabilities 按生效 model 解析**:model-registry 在 beforeRequest 钩子里按"本次请求的 model"(含请求级覆盖)查表
83
+ - `chat.use(plugin, options)`:options 透传 `plugin.install(chat, options)`
84
+ - `new ChatService({ adapter })`:构造时直接注入适配器
85
+ - 工具定义移入插件实例(不再写 `chat.config.tools`),工具超时改走 `createToolCallingPlugin({ timeout })`
86
+
87
+ ### Changed
88
+ - `updateConfig` 收窄为会话层字段(model/temperature/maxTokens/modelParams/system/retry/ephemeralContinue),非白名单或非法值抛 `ConfigurationError`(修复绕过校验问题)
89
+ - openaiAdapter 的 transport(apiKey/baseUrl/headers)从实例自持,单例 `openaiAdapter` 保留为平铺兼容路径
90
+ - `buildRequest(messages, config, systemPrompts)` 的 config 现为合并后的请求参数
91
+
92
+ ### Fixed
93
+ - TODO.md 高危 #1:插件共享可变状态——工厂化后实例隔离(单例保留为兼容路径)
94
+ - TODO.md 高危 #3:updateConfig 绕过校验——现在白名单 + 校验
95
+
96
+ ## [2.7.0] - 2026-05-25
97
+
98
+ ### Added
99
+ - **Ephemeral 临时消息**:_ephemeral: true 标记,仅底部连续时参与请求,其余自动过滤
100
+ - **续写转正**:continueLast / continueLastStream 续写 ephemeral 消息后自动转正(delete _ephemeral)
101
+ - **EventEmitter.emitAsync**:异步触发事件,支持 beforeRequest 等钩子
102
+ - **beforeRequest 钩子**:chat._hooks.on('beforeRequest', ...) 在 buildRequest 前修改 messages
103
+ - **_processResponse 钩子**:tool-calling 改为注册此钩子,不再覆盖 send/stream
104
+
105
+ ### Changed
106
+ - MessageFormatter 过滤 ephemeral:识别底部连续段,中间 ephemeral 自动跳过
107
+ - ChatService._request 拆为 4 个私有方法(_addUserMessage / _withRetry / _stream / _handleResult)
108
+ - ool-calling.js 改为注册 _processResponse,不再替换 send/stream
109
+ - dapter.stream 不再接收/操作 streamEntry,_complete 由 ChatService 管理
110
+ - openaiAdapter.stream 约减 20 行(删除所有 entry 相关代码)
111
+
112
+ ### Fixed
113
+ - retry 循环重复 push 占位消息(已修复:提到循环外)
114
+ - continueLastStream 被续写消息丢失(已修复:独立占位,ChatService 管理)
115
+ - prefix 续写空 content 被过滤(已修复:MessageFormatter 保留 prefix)
116
+
117
+ ## [2.6.0] - 2026-05-23
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 My Name
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1
+ MIT License
2
+
3
+ Copyright (c) 2024 My Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
package/README.md CHANGED
@@ -16,6 +16,7 @@ A lightweight, modular AI chat framework with plugin system, unified message for
16
16
  - **Streaming** – Full support for real‑time responses.
17
17
  - **Flexible Configuration** – Supports both flat and nested `modelParams` structure.
18
18
  - **Event‑Driven** – Built‑in EventEmitter for `message`, `sending`, `error`, `stream-progress` events.
19
+ - **Pipeline Stages (v3.0)** – `chat.pipe()` to hook custom logic at `beforeSend` / `afterSend` without touching the core.
19
20
  - **Custom Error Classes** – `APIError`, `NetworkError`, `ConfigurationError`, `ParsingError` for fine‑grained error handling.
20
21
 
21
22
  ---
@@ -26,32 +27,35 @@ A lightweight, modular AI chat framework with plugin system, unified message for
26
27
  src/
27
28
  ├── index.js # Public entry point (re-exports)
28
29
  ├── core/
29
- │ ├── ChatService.js # Main service: config, send/stream, plugin hosting
30
+ │ ├── ChatService.js # Main service: config, pipeline, send/stream, plugin hosting
31
+ │ ├── Pipeline.js # Sequential pipeline (v3.0: ctx flows through stages)
30
32
  │ ├── MessageStore.js # In-memory message list with CRUD helpers
33
+ │ ├── SystemPromptStore.js # Toggleable system prompts
31
34
  │ ├── EventEmitter.js # Minimal pub/sub (on/off/emit)
32
35
  │ └── Errors.js # Custom error classes
33
36
  ├── adapters/
34
- │ └── openai.js # OpenAI‑compatible API adapter (plugin pattern)
37
+ │ └── openai.js # OpenAI‑compatible API adapter (protocol via assertAdapter)
35
38
  ├── plugins/
36
- └── tool-calling.js # Tool calling plugin (auto‑detect & loop)
39
+ ├── tool-calling.js # Tool calling plugin (afterSend stage, auto‑detect & loop)
40
+ │ └── model-registry.js # Model capability table (beforeSend stage)
37
41
  └── utils/
42
+ ├── MessageFormatter.js # Message format conversion (registrable)
38
43
  ├── typeCheck.js # Type checking helpers
39
44
  └── url.js # URL joining utility
45
+ tests/ # node:test suites (core / pipeline / integration)
40
46
  ```
41
47
 
42
48
  **Data flow**:
43
49
 
44
50
  ```
45
51
  User calls chat.send(input)
46
- → ChatService._request()
47
- emits 'sending'
48
- MessageStore.add(userMsg)
49
- adapter.buildRequest(messages, config) ← formats request body
50
- → adapter.send(requestBody, config) ← HTTP call (fetch)
51
- adapter.parseResponse(response) ← normalize response
52
- MessageStore.add(assistantMsg)
53
- → emits 'message'
54
- → returns assistantMsg
52
+ → ChatService._request() → ctx flows through stages:
53
+ prepareInput (add user msg, merge config, emit 'sending')
54
+ beforeSend (internal hook + user beforeSend stages)
55
+ autoContinue / buildRequest ( request body)
56
+ send (adapter.send / stream + retry + placeholder)
57
+ afterSend (user stages + tool-calling loop)
58
+ returns result, emitting 'message' along the way
55
59
  ```
56
60
 
57
61
  With `toolCallingPlugin`, the flow loops: response → detect tool_calls → execute tools → add tool results → sendExisting → repeat (max 5 iterations).
@@ -88,6 +92,32 @@ chat.on('message', msg => console.log(msg.content));
88
92
  await chat.send('Hello!');
89
93
  ```
90
94
 
95
+ ### Factory Mode (Owned Configuration, Recommended)
96
+
97
+ ```javascript
98
+ import { ChatService, createOpenAIAdapter, createToolCallingPlugin } from 'my-ai-chat-framework';
99
+
100
+ // transport + request defaults belong to the adapter
101
+ const adapter = createOpenAIAdapter({
102
+ apiKey: 'your-api-key',
103
+ baseUrl: 'https://api.deepseek.com',
104
+ modelParams: { temperature: 0.8, maxTokens: 2000 }
105
+ });
106
+
107
+ const chat = new ChatService({
108
+ adapter, // injected at construction (same as chat.use(adapter))
109
+ model: 'deepseek-chat',
110
+ system: 'You are a helpful assistant'
111
+ });
112
+
113
+ chat.use(createToolCallingPlugin({ timeout: 30000 })); // plugin options
114
+
115
+ // per-request override (this request only)
116
+ await chat.send('Hello', { temperature: 0.2 });
117
+ ```
118
+
119
+ > ⚠️ The default exports `openaiAdapter` / `toolCallingPlugin` / `modelRegistryPlugin` are module-level singletons — installing one on multiple `ChatService` instances will clobber shared state. Use the factories (`createXxx`) for multiple instances.
120
+
91
121
  ### Using `modelParams` (Recommended for Many Parameters)
92
122
 
93
123
  ```javascript
@@ -106,7 +136,7 @@ const chat = new ChatService({
106
136
  ### Registering a Tool
107
137
 
108
138
  ```javascript
109
- chat.registerTool('get_weather', 'Get current weather for a city',
139
+ chat.plugins['tool-calling'].registerTool('get_weather', 'Get current weather for a city',
110
140
  async (args) => {
111
141
  // args = { city: 'Beijing' }
112
142
  return `Weather in ${args.city}: 22°C, sunny`;
@@ -142,12 +172,31 @@ Converts internal messages to OpenAI‑compatible format. Supports:
142
172
 
143
173
  Detects `tool_calls` in assistant responses, executes registered tools, feeds results back, and continues the conversation (up to `maxIterations` = 5).
144
174
 
145
- - `chat.registerTool(name, description, executor, parameters?)` – register a tool
175
+ - `chat.plugins['tool-calling'].registerTool(name, description, executor, parameters?)` – register a tool
146
176
  - Automatically injects `tool` role messages into the conversation
147
177
  - Recovers from tool execution errors gracefully (logs error, returns error message to model)
148
178
 
149
179
  ---
150
180
 
181
+ ## 🔧 Extending with Pipeline (v3.0)
182
+
183
+ ```javascript
184
+ // Inject a role card before every request
185
+ chat.pipe({ name: 'role-card', phase: 'beforeSend', async run(ctx) {
186
+ ctx.messages.add({ role: 'system', content: 'You are a tsundere CEO.' });
187
+ }});
188
+
189
+ // Post-process the reply
190
+ chat.pipe({ name: 'stamp', phase: 'afterSend', run(ctx) {
191
+ if (ctx.result?.content) ctx.result.content += ' — ' + Date.now();
192
+ }});
193
+
194
+ chat.unpipe('role-card'); // remove
195
+ console.log(chat.pipelineStages); // inspect stage order
196
+ ```
197
+
198
+ See `docs/DEVELOPER.md` \u201cWrite a Stage\u201d for the full guide.
199
+
151
200
  ## 📡 Events (EventEmitter)
152
201
 
153
202
  `ChatService` extends `EventEmitter`. Subscribe with `chat.on(event, handler)`:
@@ -182,7 +231,7 @@ unsubscribe(); // stop listening
182
231
  | `chat.use(plugin)` | `this` | Install a plugin/adapter |
183
232
  | `chat.setAdapter(adapter)` | `void` | Manually set the adapter |
184
233
  | `chat.on(event, handler)` | `unsubscribe function` | Subscribe to events |
185
- | `chat.registerTool(name, desc, fn, params?)` | `this` | Register a tool (requires toolCallingPlugin) |
234
+ | `chat.plugins['tool-calling'].registerTool(name, desc, fn, params?)` | `this` | Register a tool (requires toolCallingPlugin) |
186
235
  | `chat.messages` | `MessageStore` | Access the message store directly |
187
236
 
188
237
  ---
@@ -196,10 +245,12 @@ unsubscribe(); // stop listening
196
245
  | `addAssistant(content, meta?)` | Add an assistant message |
197
246
  | `addSystem(content, meta?)` | Add a system message |
198
247
  | `addTool(content, toolCallId, meta?)` | Add a tool result message |
248
+ | `addOnceAssistant(content, options?)` | Add a one-shot assistant guide message (defaults to `_ephemeral: true` + `prefix: true`; options: `{ reasoningContent, prefix }`) |
199
249
  | `getAll()` | Return a shallow copy of all messages |
200
250
  | `getLast()` | Return the last message (or null) |
201
251
  | `clear()` | Remove all messages |
202
252
  | `undoToLastAssistant()` | Remove messages after the last assistant message |
253
+ | `undoToPreviousUser(id)` | Remove messages from the message with `id` (inclusive) back to, but excluding, the previous user message — useful before resending |
203
254
 
204
255
  **Message format**:
205
256
 
@@ -263,6 +314,42 @@ try {
263
314
 
264
315
  > Both flat and `modelParams` styles work. `modelParams` takes precedence over top‑level values.
265
316
 
317
+ ### modelParams
318
+
319
+ `modelParams` groups all model-related parameters. Supported fields:
320
+
321
+ | Field | Type | Default | Description |
322
+ |-------|------|---------|-------------|
323
+ | `model` | string | – | Model name (overrides top-level `model`) |
324
+ | `temperature` | number | `0.7` | Sampling temperature (0–2) |
325
+ | `maxTokens` | number | `2000` | Max tokens to generate |
326
+ | `reasoningEffort` | string | – | For `deepseek-reasoner`: `'low'`, `'medium'`, `'high'` |
327
+ | `topP` | number | – | Nucleus sampling (0–1) |
328
+ | `frequencyPenalty` | number | – | Penalize frequent tokens (-2.0–2.0) |
329
+ | `presencePenalty` | number | – | Penalize repeated tokens (-2.0–2.0) |
330
+ | `stop` | string \| string[] | – | Stop sequences |
331
+ | `responseFormat` | object | – | e.g., `{ type: 'json_object' }` |
332
+ | `seed` | number | – | For reproducible results |
333
+
334
+ **Naming convention**: CamelCase fields (`maxTokens`, `topP`) are managed by the adapter (converted to API format). Any unknown field is passed through as‑is—use the provider's native naming (e.g., `snake_case` for OpenAI).
335
+
336
+ ```javascript
337
+ const chat = new ChatService({
338
+ apiKey: 'your-api-key',
339
+ baseUrl: 'https://api.deepseek.com',
340
+ model: 'deepseek-chat',
341
+ modelParams: {
342
+ temperature: 0.8,
343
+ maxTokens: 1500,
344
+ topP: 0.9,
345
+ frequencyPenalty: 0.5,
346
+ // unknown fields pass through directly (use API's native names):
347
+ logprobs: true,
348
+ top_logprobs: 5
349
+ }
350
+ });
351
+ ```
352
+
266
353
  ---
267
354
 
268
355
  ## 🧪 Testing
@@ -275,7 +362,7 @@ echo "DEEPSEEK_API_KEY=sk-xxxxx" > .env
275
362
  npm test
276
363
  ```
277
364
 
278
- The test script (`test.js`) sends a simple message and logs the response.
365
+ Unit tests: `npm test` (`tests/core.test.js` + `tests/pipeline.test.js`, no API key needed). Integration test: `npm run test:integration` (`tests/integration.test.js`, requires `DEEPSEEK_API_KEY` in `.env`).
279
366
 
280
367
  ---
281
368