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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [3.0.0] - 2026-08-?
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **公开管道 API**:`chat.pipe({ name, phase?, run(ctx) })` / `chat.unpipe(name)` / `chat.pipelineStages`
|
|
9
|
+
- phase `beforeSend`(默认):内部 beforeRequest 之后、构建请求体之前
|
|
10
|
+
- phase `afterSend`:发送完成、结果落位之后(可改 ctx.result)
|
|
11
|
+
- 不注册任何车间 = 行为与 2.8.x 完全一致
|
|
12
|
+
- **协议固化**:`assertAdapter` 校验 buildRequest/send/stream/parseResponse,缺方法抛 ConfigurationError 并列出方法名;JSDoc @typedef ChatAdapter / PipelineContext / PipeStage
|
|
13
|
+
- **插件车间化**:tool-calling 经 afterSend 车间、model-registry 经 beforeSend 车间,不再占用 `_processResponse` 单座槽位
|
|
14
|
+
- 新增测试 `tests/pipeline.test.js`(8 例:pipe 顺序/冲突/unpipe/改ctx/SSE 冒烟/工具循环端到端)
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `ChatService._request` 固定线拆为内部管道:prepareInput → beforeSend → autoContinue → buildRequest → send(行为等价)
|
|
18
|
+
- `_hooks` beforeRequest 保留为兼容桥;`_processResponse` 保留为旧通道(新代码请用 afterSend 车间)
|
|
19
|
+
- 版本号 2.8.0 → 3.0.0(新增公共 API 面)
|
|
20
|
+
|
|
21
|
+
### Known delta(流式 tool-calling 时序)
|
|
22
|
+
- 流式工具调用时,初始回复的 `message` 事件在工具循环前发出(旧版循环后发)。非流式时序与旧版一致。
|
|
23
|
+
|
|
24
|
+
## [2.8.0] - 2026-08-08
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **配置归属系统**:配置按层归属(运输/请求/会话/插件),谁消费谁声明
|
|
28
|
+
- **工厂函数**:`createOpenAIAdapter(opts)` / `createToolCallingPlugin(opts)` / `createModelRegistryPlugin(opts)`——每个实例自持状态,规避模块级单例互踩陷阱
|
|
29
|
+
- **请求级参数覆盖**:`chat.send(input, params)` / `chat.stream(input, params, onProgress, onDone)` / `sendExisting(params)` / `sendExistingStream(params, ...)`,合并链 = 适配器默认 ← 会话配置 ← 本次覆盖(近者优先)
|
|
30
|
+
- **capabilities 按生效 model 解析**:model-registry 在 beforeRequest 钩子里按"本次请求的 model"(含请求级覆盖)查表
|
|
31
|
+
- `chat.use(plugin, options)`:options 透传 `plugin.install(chat, options)`
|
|
32
|
+
- `new ChatService({ adapter })`:构造时直接注入适配器
|
|
33
|
+
- 工具定义移入插件实例(不再写 `chat.config.tools`),工具超时改走 `createToolCallingPlugin({ timeout })`
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- `updateConfig` 收窄为会话层字段(model/temperature/maxTokens/modelParams/system/retry/ephemeralContinue),非白名单或非法值抛 `ConfigurationError`(修复绕过校验问题)
|
|
37
|
+
- openaiAdapter 的 transport(apiKey/baseUrl/headers)从实例自持,单例 `openaiAdapter` 保留为平铺兼容路径
|
|
38
|
+
- `buildRequest(messages, config, systemPrompts)` 的 config 现为合并后的请求参数
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- TODO.md 高危 #1:插件共享可变状态——工厂化后实例隔离(单例保留为兼容路径)
|
|
42
|
+
- TODO.md 高危 #3:updateConfig 绕过校验——现在白名单 + 校验
|
|
43
|
+
|
|
44
|
+
## [2.7.0] - 2026-05-25
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
- **Ephemeral 临时消息**:_ephemeral: true 标记,仅底部连续时参与请求,其余自动过滤
|
|
48
|
+
- **续写转正**:continueLast / continueLastStream 续写 ephemeral 消息后自动转正(delete _ephemeral)
|
|
49
|
+
- **EventEmitter.emitAsync**:异步触发事件,支持 beforeRequest 等钩子
|
|
50
|
+
- **beforeRequest 钩子**:chat._hooks.on('beforeRequest', ...) 在 buildRequest 前修改 messages
|
|
51
|
+
- **_processResponse 钩子**:tool-calling 改为注册此钩子,不再覆盖 send/stream
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
- MessageFormatter 过滤 ephemeral:识别底部连续段,中间 ephemeral 自动跳过
|
|
55
|
+
- ChatService._request 拆为 4 个私有方法(_addUserMessage / _withRetry / _stream / _handleResult)
|
|
56
|
+
- ool-calling.js 改为注册 _processResponse,不再替换 send/stream
|
|
57
|
+
- dapter.stream 不再接收/操作 streamEntry,_complete 由 ChatService 管理
|
|
58
|
+
- openaiAdapter.stream 约减 20 行(删除所有 entry 相关代码)
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
- retry 循环重复 push 占位消息(已修复:提到循环外)
|
|
62
|
+
- continueLastStream 被续写消息丢失(已修复:独立占位,ChatService 管理)
|
|
63
|
+
- prefix 续写空 content 被过滤(已修复:MessageFormatter 保留 prefix)
|
|
64
|
+
|
|
65
|
+
## [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 (
|
|
37
|
+
│ └── openai.js # OpenAI‑compatible API adapter (protocol via assertAdapter)
|
|
35
38
|
├── plugins/
|
|
36
|
-
│
|
|
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
|
-
→
|
|
48
|
-
→
|
|
49
|
-
→
|
|
50
|
-
→ adapter.send
|
|
51
|
-
→
|
|
52
|
-
→
|
|
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
|
|
@@ -148,6 +178,25 @@ Detects `tool_calls` in assistant responses, executes registered tools, feeds re
|
|
|
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)`:
|
|
@@ -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
|
-
|
|
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
|
|
package/README_ZH.md
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
|
|
2
|
+
# 🤖 我的AI聊天框架
|
|
3
|
+
|
|
4
|
+
一个轻量级、模块化的AI聊天框架,内置插件系统、统一消息格式和工具调用支持。
|
|
5
|
+
|
|
6
|
+
> **⚠️ 注意**:本项目用于**学习目的**,由 **AI 生成**,不适用于生产环境。不保证向后兼容性,请谨慎使用。
|
|
7
|
+
|
|
8
|
+
## ✨ 特性
|
|
9
|
+
|
|
10
|
+
- **轻量核心** – 仅约300行代码,易于理解和扩展。
|
|
11
|
+
- **插件系统** – 添加功能(工具调用、思维链、自定义适配器)无需修改核心。
|
|
12
|
+
- **统一消息格式** – 所有组件使用一致的数据结构。
|
|
13
|
+
- **多环境支持** – 构建为 ES 模块、UMD 和 CommonJS 格式,可用于浏览器和 Node.js。
|
|
14
|
+
- **无外部依赖** – 使用原生 `fetch`(Node 18+ 及现代浏览器)。
|
|
15
|
+
- **工具调用** – 内置插件处理 AI 的函数调用(自动多轮循环)。
|
|
16
|
+
- **流式传输** – 完整支持实时响应。
|
|
17
|
+
- **灵活配置** – 支持平铺和嵌套 `modelParams` 两种配置风格。
|
|
18
|
+
- **事件驱动** – 内置 EventEmitter,支持 `message`、`sending`、`error`、`stream-progress` 事件。
|
|
19
|
+
- **管道车间(v3.0)** – `chat.pipe()` 在任何阶段挂自定义功能(beforeSend/afterSend),核心代码不用改。
|
|
20
|
+
- **类型化错误** – `APIError`、`NetworkError`、`ConfigurationError`、`ParsingError`,方便分类处理。
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 🧱 架构概览
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
src/
|
|
28
|
+
├── index.js # 公共入口(统一导出)
|
|
29
|
+
├── core/
|
|
30
|
+
│ ├── ChatService.js # 核心:配置、管道调度、发送/流式、插件托管
|
|
31
|
+
│ ├── Pipeline.js # 顺序管道(v3.0:小车 ctx 依次过车间)
|
|
32
|
+
│ ├── MessageStore.js # 内存消息列表(增删改查)
|
|
33
|
+
│ ├── SystemPromptStore.js # 可开关的 system 提示词
|
|
34
|
+
│ ├── EventEmitter.js # 极简发布/订阅(on/off/emit)
|
|
35
|
+
│ └── Errors.js # 自定义错误类
|
|
36
|
+
├── adapters/
|
|
37
|
+
│ └── openai.js # OpenAI 兼容 API 适配器(协议见 assertAdapter)
|
|
38
|
+
├── plugins/
|
|
39
|
+
│ ├── tool-calling.js # 工具调用插件(afterSend 车间,自动检测&循环执行)
|
|
40
|
+
│ └── model-registry.js # 模型能力表(beforeSend 车间)
|
|
41
|
+
└── utils/
|
|
42
|
+
├── MessageFormatter.js # 消息格式转换(可注册)
|
|
43
|
+
├── typeCheck.js # 类型判断工具
|
|
44
|
+
└── url.js # URL 拼接工具
|
|
45
|
+
tests/ # node:test 单元测试(core / pipeline / integration)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**数据流**:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
用户调用 chat.send(input)
|
|
52
|
+
→ ChatService._request() → 小车 ctx 依次开过车间:
|
|
53
|
+
→ prepareInput(加用户消息、合并配置、发出 'sending')
|
|
54
|
+
→ beforeSend(内部钩子 + 用户 beforeSend 车间)
|
|
55
|
+
→ autoContinue / buildRequest(→ 请求体)
|
|
56
|
+
→ send(adapter.send / stream + 重试 + 占位消息)
|
|
57
|
+
→ afterSend(用户车间 + tool-calling 循环)
|
|
58
|
+
→ 返回结果,过程中发出 'message' 事件
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
使用 `toolCallingPlugin` 时,流程会循环:响应 → 检测 tool_calls → 执行工具 → 添加工具结果 → sendExisting → 重复(最多 5 轮)。
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 📦 安装
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm install my-ai-chat-framework
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 🚀 快速开始
|
|
74
|
+
|
|
75
|
+
### 基础用法(平铺配置)
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
import { ChatService, openaiAdapter, toolCallingPlugin } from 'my-ai-chat-framework';
|
|
79
|
+
|
|
80
|
+
const chat = new ChatService({
|
|
81
|
+
apiKey: 'your-api-key',
|
|
82
|
+
baseUrl: 'https://api.deepseek.com', // 可选,默认 OpenAI
|
|
83
|
+
model: 'deepseek-chat',
|
|
84
|
+
temperature: 0.7,
|
|
85
|
+
maxTokens: 2000
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
chat.use(openaiAdapter);
|
|
89
|
+
chat.use(toolCallingPlugin);
|
|
90
|
+
|
|
91
|
+
chat.on('message', msg => console.log(msg.content));
|
|
92
|
+
await chat.send('你好!');
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 工厂模式(配置归属更清晰,推荐)
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
import { ChatService, createOpenAIAdapter, createToolCallingPlugin } from 'my-ai-chat-framework';
|
|
99
|
+
|
|
100
|
+
// 运输层 + 请求默认参数归适配器
|
|
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, // 构造时注入(等价于 chat.use(adapter))
|
|
109
|
+
model: 'deepseek-chat',
|
|
110
|
+
system: '你是乐于助人的AI助手'
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
chat.use(createToolCallingPlugin({ timeout: 30000 })); // 插件自带配置
|
|
114
|
+
|
|
115
|
+
// 请求级覆盖:只对本次请求生效
|
|
116
|
+
await chat.send('你好', { temperature: 0.2 });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 使用 `modelParams`(参数较多时推荐)
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
const chat = new ChatService({
|
|
123
|
+
apiKey: 'your-api-key',
|
|
124
|
+
baseUrl: 'https://api.deepseek.com',
|
|
125
|
+
model: 'deepseek-chat', // 顶层仍保留 model 方便
|
|
126
|
+
modelParams: { // 可选参数分组
|
|
127
|
+
temperature: 0.8,
|
|
128
|
+
maxTokens: 1500,
|
|
129
|
+
reasoningEffort: 'medium' // 用于 deepseek-reasoner
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 注册工具
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
chat.registerTool('get_weather', '获取指定城市的天气',
|
|
138
|
+
async (args) => {
|
|
139
|
+
// args = { city: '北京' }
|
|
140
|
+
return `${args.city}天气:22°C,晴`;
|
|
141
|
+
},
|
|
142
|
+
{ // 参数 schema(可选但推荐)
|
|
143
|
+
city: { type: 'string', description: '城市名称', required: true }
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
await chat.send('北京今天天气怎么样?');
|
|
148
|
+
// → AI 调用 get_weather,框架执行,AI 返回天气信息
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 🔌 插件与适配器
|
|
154
|
+
|
|
155
|
+
### openaiAdapter
|
|
156
|
+
|
|
157
|
+
将内部消息转换为 OpenAI 兼容格式。支持:
|
|
158
|
+
- `apiUrl` – 完整 URL(最高优先级)
|
|
159
|
+
- `baseUrl` + `path` – 基础域名 + API 路径
|
|
160
|
+
- 默认:`https://api.openai.com/v1/chat/completions`
|
|
161
|
+
|
|
162
|
+
| 配置项 | 类型 | 默认值 | 描述 |
|
|
163
|
+
|--------|------|--------|------|
|
|
164
|
+
| `apiKey` | string | **必填** | Authorization 请求头中的 Bearer token |
|
|
165
|
+
| `apiUrl` | string | – | 完整请求 URL(优先级高于 baseUrl+path) |
|
|
166
|
+
| `baseUrl` | string | `https://api.openai.com` | API 基础域名 |
|
|
167
|
+
| `path` | string | `/v1/chat/completions` | API 端点路径 |
|
|
168
|
+
|
|
169
|
+
### toolCallingPlugin
|
|
170
|
+
|
|
171
|
+
检测助手响应中的 `tool_calls`,执行已注册的工具,将结果回传并继续对话(最多 `maxIterations` = 5 轮)。
|
|
172
|
+
|
|
173
|
+
- `chat.registerTool(name, description, executor, parameters?)` – 注册工具
|
|
174
|
+
- 自动向对话中插入 `role: 'tool'` 消息
|
|
175
|
+
- 工具执行出错时优雅降级(记录日志,将错误信息返回给模型)
|
|
176
|
+
- 配置:`createToolCallingPlugin({ timeout: 30000, maxIterations: 5 })`
|
|
177
|
+
|
|
178
|
+
> ⚠️ **多实例注意**:`openaiAdapter` / `toolCallingPlugin` / `modelRegistryPlugin` 默认导出是模块级单例,装到多个 ChatService 实例会互相覆盖。多实例场景请用工厂:`createOpenAIAdapter(opts)` / `createToolCallingPlugin(opts)` / `createModelRegistryPlugin(opts)`。
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 📡 事件系统(EventEmitter)
|
|
183
|
+
|
|
184
|
+
`ChatService` 继承自 `EventEmitter`。通过 `chat.on(event, handler)` 订阅:
|
|
185
|
+
|
|
186
|
+
| 事件 | 数据 | 触发时机 |
|
|
187
|
+
|------|------|----------|
|
|
188
|
+
| `sending` | `{ addUser, userInput, timestamp }` | 每次请求发送前 |
|
|
189
|
+
| `message` | `{ role, content, ... }` | 收到完整助手消息 |
|
|
190
|
+
| `stream-progress` | chunk 对象 | 每个流式块到达时 |
|
|
191
|
+
| `error` | `{ error, timestamp }` | 请求过程中发生错误 |
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
chat.on('sending', ({ userInput }) => console.log('发送中:', userInput));
|
|
195
|
+
chat.on('message', msg => console.log('收到:', msg.content));
|
|
196
|
+
chat.on('error', ({ error }) => console.error('错误:', error.message));
|
|
197
|
+
|
|
198
|
+
// on() 返回取消订阅函数
|
|
199
|
+
const unsubscribe = chat.on('message', handler);
|
|
200
|
+
unsubscribe(); // 停止监听
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 🧩 ChatService API
|
|
206
|
+
|
|
207
|
+
| 方法 | 返回值 | 描述 |
|
|
208
|
+
|------|--------|------|
|
|
209
|
+
| `chat.send(userInput)` | `Promise<Message>` | 发送消息,获取回复(非流式) |
|
|
210
|
+
| `chat.stream(userInput, onProgress, onDone)` | `Promise<Message>` | 发送消息,获取流式回复 |
|
|
211
|
+
| `chat.sendExisting()` | `Promise<Message>` | 用现有消息重新请求(不添加用户消息) |
|
|
212
|
+
| `chat.sendExistingStream(onProgress, onDone)` | `Promise<Message>` | 同上,流式 |
|
|
213
|
+
| `chat.use(plugin)` | `this` | 安装插件/适配器 |
|
|
214
|
+
| `chat.setAdapter(adapter)` | `void` | 手动设置适配器 |
|
|
215
|
+
| `chat.on(event, handler)` | `取消订阅函数` | 订阅事件 |
|
|
216
|
+
| `chat.registerTool(name, desc, fn, params?)` | `this` | 注册工具(需安装 toolCallingPlugin) |
|
|
217
|
+
| `chat.messages` | `MessageStore` | 直接访问消息存储 |
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## 🗄️ MessageStore API
|
|
222
|
+
|
|
223
|
+
| 方法 | 描述 |
|
|
224
|
+
|------|------|
|
|
225
|
+
| `add(message)` | 添加原始消息对象 |
|
|
226
|
+
| `addUser(content, meta?)` | 添加用户消息 |
|
|
227
|
+
| `addAssistant(content, meta?)` | 添加助手消息 |
|
|
228
|
+
| `addSystem(content, meta?)` | 添加系统消息 |
|
|
229
|
+
| `addTool(content, toolCallId, meta?)` | 添加工具结果消息 |
|
|
230
|
+
| `addOnceAssistant(content, options?)` | 添加一次性引导消息(默认 `_ephemeral: true` + `prefix: true`;options: `{ reasoningContent, prefix }`) |
|
|
231
|
+
| `getAll()` | 返回所有消息的浅拷贝 |
|
|
232
|
+
| `getLast()` | 返回最后一条消息(或 null) |
|
|
233
|
+
| `clear()` | 清空所有消息 |
|
|
234
|
+
| `undoToLastAssistant()` | 移除最后一条助手消息之后的所有消息 |
|
|
235
|
+
| `undoToPreviousUser(id)` | 撤回到指定消息之前的最近 user,删除该 user 之后到该消息(含)之间的消息(适用于重发) |
|
|
236
|
+
|
|
237
|
+
**消息格式**:
|
|
238
|
+
|
|
239
|
+
```javascript
|
|
240
|
+
{
|
|
241
|
+
id: string, // 未提供时自动生成
|
|
242
|
+
role: 'user' | 'assistant' | 'system' | 'tool',
|
|
243
|
+
content: string,
|
|
244
|
+
toolCalls?: Array, // 助手消息中的工具调用
|
|
245
|
+
toolCallId?: string, // 工具消息
|
|
246
|
+
reasoningContent?: string, // deepseek-reasoner 思维链
|
|
247
|
+
timestamp?: number,
|
|
248
|
+
metadata?: any
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## ❌ 错误处理
|
|
255
|
+
|
|
256
|
+
框架针对不同失败场景抛出类型化错误:
|
|
257
|
+
|
|
258
|
+
| 错误类 | `.name` | 触发场景 |
|
|
259
|
+
|--------|---------|----------|
|
|
260
|
+
| `APIError` | `'APIError'` | 非 2xx HTTP 响应(401, 429, 500 等) |
|
|
261
|
+
| `NetworkError` | `'NetworkError'` | `fetch` 失败、连接超时等 |
|
|
262
|
+
| `ConfigurationError` | `'ConfigurationError'` | 缺少必要配置项 |
|
|
263
|
+
| `ParsingError` | `'ParsingError'` | API 响应格式异常 |
|
|
264
|
+
|
|
265
|
+
```javascript
|
|
266
|
+
import { APIError, NetworkError, ConfigurationError, ParsingError } from 'my-ai-chat-framework';
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
await chat.send('你好');
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (error instanceof APIError) {
|
|
272
|
+
console.error(`API 错误 ${error.statusCode}: ${error.message}`);
|
|
273
|
+
} else if (error instanceof NetworkError) {
|
|
274
|
+
console.error('网络问题:', error.message);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## 📚 配置参考
|
|
282
|
+
|
|
283
|
+
`new ChatService(config)` 可接受以下选项:
|
|
284
|
+
|
|
285
|
+
| 选项 | 类型 | 默认值 | 描述 |
|
|
286
|
+
|------|------|--------|------|
|
|
287
|
+
| `apiKey` | string | **必填** | API 密钥 |
|
|
288
|
+
| `baseUrl` | string | `'https://api.openai.com'` | API 基础域名(与 `path` 配合) |
|
|
289
|
+
| `path` | string | `'/v1/chat/completions'` | API 路径(与 `baseUrl` 配合) |
|
|
290
|
+
| `apiUrl` | string | – | 完整 API URL(优先级高于 `baseUrl`+`path`) |
|
|
291
|
+
| `model` | string | **必填** | 模型名称(如 `deepseek-chat`) |
|
|
292
|
+
| `modelParams` | object | `{}` | 分组模型参数(见下) |
|
|
293
|
+
| `temperature` | number | `0.7` | 温度(0–2) |
|
|
294
|
+
| `maxTokens` | number | `2000` | 最大生成 token 数 |
|
|
295
|
+
| `reasoningEffort` | string | – | 仅 `deepseek-reasoner`:`'low'`、`'medium'`、`'high'` |
|
|
296
|
+
|
|
297
|
+
> 平铺和 `modelParams` 两种风格都支持。`modelParams` 中的值优先级高于顶层同名属性。
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## 🧪 测试
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
# 1. 创建 .env 文件,写入 API key
|
|
305
|
+
echo "DEEPSEEK_API_KEY=sk-xxxxx" > .env
|
|
306
|
+
|
|
307
|
+
# 2. 运行测试
|
|
308
|
+
npm test
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
单元测试:`npm test`(`tests/core.test.js` + `tests/pipeline.test.js`,无需 API Key)。集成测试:`npm run test:integration`(`tests/integration.test.js`,需要 .env 中的 `DEEPSEEK_API_KEY`)。
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## 🛠️ 开发
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
git clone https://github.com/your-username/my-ai-chat-framework.git
|
|
319
|
+
cd my-ai-chat-framework
|
|
320
|
+
npm install
|
|
321
|
+
|
|
322
|
+
# 开发模式(监听文件变化)
|
|
323
|
+
npm run dev
|
|
324
|
+
|
|
325
|
+
## 🧩 扩展:管道车间(v3.0)
|
|
326
|
+
|
|
327
|
+
核心流程是一辆小车(ctx)依次开过车间;加新功能 = 注册一个车间,核心代码不用改。
|
|
328
|
+
|
|
329
|
+
```javascript
|
|
330
|
+
// 发送前注入角色卡
|
|
331
|
+
chat.pipe({
|
|
332
|
+
name: '角色卡注入',
|
|
333
|
+
phase: 'beforeSend', // 默认
|
|
334
|
+
async run(ctx) {
|
|
335
|
+
ctx.messages.add({ role: 'system', content: '你是傲娇霸总' });
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// 发送后改写回复
|
|
340
|
+
chat.pipe({
|
|
341
|
+
name: '加时间戳',
|
|
342
|
+
phase: 'afterSend',
|
|
343
|
+
run(ctx) { if (ctx.result?.content) ctx.result.content += ' — ' + Date.now(); }
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
chat.unpipe('角色卡注入'); // 移除
|
|
347
|
+
console.log(chat.pipelineStages); // 查看车间顺序
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**写一个 adapter(同级服务商)**:实现 4 个方法即可,`assertAdapter` 会帮你在装错时点名哪个方法缺失:
|
|
351
|
+
|
|
352
|
+
```javascript
|
|
353
|
+
const myAdapter = {
|
|
354
|
+
buildRequest(messages, config) { /* 内部消息 → 请求体 */ },
|
|
355
|
+
async send(body, config, opts) { /* 非流式 → 原始响应 */ },
|
|
356
|
+
async stream(body, config, onProgress, onDone, opts) { /* 流式 */ },
|
|
357
|
+
parseResponse(resp) { /* 原始响应 → 内部消息格式 */ },
|
|
358
|
+
};
|
|
359
|
+
chat.setAdapter(myAdapter);
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
# 构建库(ES + UMD + CJS)
|
|
365
|
+
npm run build
|
|
366
|
+
|
|
367
|
+
# 运行测试
|
|
368
|
+
npm test
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## 📄 许可证
|
|
374
|
+
|
|
375
|
+
MIT
|