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 +117 -0
- package/LICENSE +20 -20
- package/README.md +103 -16
- package/README_ZH.md +392 -0
- package/dist/my-ai-chat-framework.browser.es.js +1238 -421
- package/dist/my-ai-chat-framework.browser.es.js.map +1 -1
- package/dist/my-ai-chat-framework.browser.umd.js +1245 -420
- package/dist/my-ai-chat-framework.browser.umd.js.map +1 -1
- package/dist/my-ai-chat-framework.node.cjs.js +1245 -420
- package/dist/my-ai-chat-framework.node.cjs.js.map +1 -1
- package/docs/DEVELOPER.md +569 -0
- package/docs/README.md +33 -0
- package/package.json +31 -8
- package/src/adapters/openai.js +124 -23
- package/src/core/ChatService.js +596 -133
- package/src/core/Errors.js +72 -59
- package/src/core/MessageStore.js +27 -0
- package/src/core/Pipeline.js +48 -0
- package/src/core/SystemPromptStore.js +120 -118
- package/src/index.js +7 -4
- package/src/plugins/continuation.js +156 -0
- package/src/plugins/model-registry.js +236 -187
- package/src/plugins/tool-calling.js +245 -187
- package/src/utils/MessageFormatter.js +296 -204
- package/src/utils/typeCheck.js +11 -11
- package/src/utils/url.js +17 -17
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
# 开发者文档
|
|
2
|
+
|
|
3
|
+
> 版本 2.7.0 | 2026-06-05
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 架构
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
ChatService (核心)
|
|
11
|
+
├── config — 全部配置(model、apiKey、capabilities…)
|
|
12
|
+
├── messages — MessageStore 实例(对话历史)
|
|
13
|
+
├── systemPrompts — SystemPromptStore 实例(可开关的 system 提示词)
|
|
14
|
+
├── _adapter — 当前适配器(openaiAdapter 等)
|
|
15
|
+
├── _abortController — 当前请求的中断控制器
|
|
16
|
+
└── _isGenerating — 是否正在生成中
|
|
17
|
+
|
|
18
|
+
插件(通过 chat.use() 安装)
|
|
19
|
+
├── openaiAdapter — HTTP 请求 + 流式解析
|
|
20
|
+
├── toolCallingPlugin — 工具调用循环
|
|
21
|
+
└── modelRegistryPlugin — 模型能力表自动同步
|
|
22
|
+
|
|
23
|
+
工具层
|
|
24
|
+
├── MessageFormatter — 消息格式转换(内置 openai,可注册自定义)
|
|
25
|
+
├── EventEmitter — 发布/订阅(ChatService 继承它)
|
|
26
|
+
└── Errors — APIError / NetworkError / ConfigurationError / ParsingError
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 配置
|
|
32
|
+
|
|
33
|
+
配置按**归属分层**:谁消费,谁声明。内置与自定义插件走同一条路径。
|
|
34
|
+
|
|
35
|
+
| 层 | 内容 | 归谁管 |
|
|
36
|
+
|----|------|--------|
|
|
37
|
+
| 运输层 | `apiKey` / `baseUrl` / `apiUrl` / `path` / `headers` | 适配器工厂 `createOpenAIAdapter(opts)` |
|
|
38
|
+
| 请求层 | `model` / `temperature` / `maxTokens` / `modelParams.*` / `messageFormat` / `resolveImage` | 会话默认 + 请求级覆盖 |
|
|
39
|
+
| 会话层 | `system` / `retry` / `ephemeralContinue` | `new ChatService({...})` + `updateConfig` |
|
|
40
|
+
| 插件自有 | `toolTimeout` / `maxIterations` / `models` 等 | 各插件工厂 options |
|
|
41
|
+
|
|
42
|
+
### 归属模式(自由)
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
import { ChatService, createOpenAIAdapter, createToolCallingPlugin, createModelRegistryPlugin } from 'my-ai-chat-framework';
|
|
46
|
+
|
|
47
|
+
const adapter = createOpenAIAdapter({
|
|
48
|
+
apiKey: 'sk-xxxx',
|
|
49
|
+
baseUrl: 'https://api.deepseek.com',
|
|
50
|
+
modelParams: { temperature: 0.8, maxTokens: 2000 } // 请求默认参数
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const chat = new ChatService({
|
|
54
|
+
adapter, // 构造时注入适配器(等价于 chat.use(adapter))
|
|
55
|
+
model: 'deepseek-chat', // 会话默认 model
|
|
56
|
+
system: '你是猫娘', // 自动同步到 systemPrompts
|
|
57
|
+
retry: { maxRetries: 2, retryDelay: 1000 }
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
chat.use(createToolCallingPlugin({ timeout: 30000 })); // 工具插件自带配置
|
|
61
|
+
chat.use(createModelRegistryPlugin({ models: { 'my-model': { reasoning: true } } }));
|
|
62
|
+
|
|
63
|
+
// 请求级覆盖:只对本次请求生效
|
|
64
|
+
await chat.send('你好', { temperature: 0.2, model: 'deepseek-reasoner' });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 平铺兼容写法(简单模式)
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
// 运输层/请求层字段在构造时平铺传入,内部路由给内置 openaiAdapter
|
|
71
|
+
const chat = new ChatService({
|
|
72
|
+
apiKey: 'sk-xxxx',
|
|
73
|
+
baseUrl: 'https://api.deepseek.com',
|
|
74
|
+
model: 'deepseek-chat',
|
|
75
|
+
modelParams: { temperature: 0.8, maxTokens: 2000 }
|
|
76
|
+
});
|
|
77
|
+
chat.use(openaiAdapter); // 无状态单例,配置从 chat.config 读取
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 合并优先级
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
适配器默认(getRequestDefaults) ← 会话配置(this.config) ← 本次覆盖(params)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`modelParams` 深层合并;平铺便捷键(`temperature` / `maxTokens` / `reasoningEffort`)自动折叠进 `modelParams`。
|
|
87
|
+
|
|
88
|
+
### 插件配置归属
|
|
89
|
+
|
|
90
|
+
- 插件 options 在**工厂创建时**传入:`createXxxPlugin(opts)`
|
|
91
|
+
- 也支持安装时传入:`chat.use(plugin, opts)`,合并进 `_options`
|
|
92
|
+
- **推荐用工厂**:模块级单例(`openaiAdapter` / `toolCallingPlugin` / `modelRegistryPlugin`)装到多个 ChatService 实例会互相覆盖,仅作兼容保留
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 配置校验
|
|
97
|
+
|
|
98
|
+
- 构造器:`model` 必填(`config.model` 或 `config.modelParams.model`);`temperature` 0~2;`maxTokens` 正整数
|
|
99
|
+
- `updateConfig`:只接受会话层字段(`model` / `temperature` / `maxTokens` / `modelParams` / `system` / `retry` / `ephemeralContinue`),非白名单字段或非法值抛 `ConfigurationError`,与构造器校验一致
|
|
100
|
+
- 请求级覆盖(`chat.send(x, params)`)不做运行时校验——API 会返回错误,适合调试
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## API
|
|
105
|
+
|
|
106
|
+
### ChatService
|
|
107
|
+
|
|
108
|
+
| 方法 | 说明 |
|
|
109
|
+
|------|------|
|
|
110
|
+
| `chat.send(input, params?)` | 发送消息,返回助手消息。`params` 为请求级覆盖(仅本次生效)。AbortError 内部捕获,不向外抛 |
|
|
111
|
+
| `chat.stream(input, params?, onProgress, onDone)` | 流式发送。兼容旧签名 `stream(input, onProgress, onDone)`。AbortError 内部捕获 |
|
|
112
|
+
| `chat.sendExisting(params?)` | 重发当前消息(不加用户消息)。AbortError 内部捕获 |
|
|
113
|
+
| `chat.sendExistingStream(params?, onProgress, onDone)` | 流式重发。兼容旧签名。AbortError 内部捕获 |
|
|
114
|
+
| `chat.plugins['continuation'].continueLast()` | 前缀续写:从最后一条 assistant/ephemeral 续写并拼回原消息,续写后自动转正。AbortError 内部捕获,返回 target |
|
|
115
|
+
| `chat.plugins['continuation'].continueLastStream(onProgress, onDone)` | 流式前缀续写。AbortError 内部捕获,返回 target |
|
|
116
|
+
| `chat.abort()` | 中断当前请求,触发 `aborted` 事件 |
|
|
117
|
+
| `chat.use(plugin, options?)` | 安装插件,options 透传给 `plugin.install(chat, options)` |
|
|
118
|
+
| `chat.updateConfig(partial)` | 更新会话层配置(白名单+校验),触发 `config-updated` |
|
|
119
|
+
| `chat.plugins['tool-calling'].registerTool(name, desc, fn, params?)` | 注册工具(需 toolCallingPlugin) |
|
|
120
|
+
| `chat.plugins['model-registry'].registerModel(name, caps)` | 注册/覆盖模型能力(需 modelRegistryPlugin) |
|
|
121
|
+
|
|
122
|
+
### 属性
|
|
123
|
+
|
|
124
|
+
| 属性 | 说明 |
|
|
125
|
+
|------|------|
|
|
126
|
+
| `chat.messages` | MessageStore 实例 |
|
|
127
|
+
| `chat.systemPrompts` | SystemPromptStore 实例 |
|
|
128
|
+
| `chat.config` | 当前配置对象(可直接读) |
|
|
129
|
+
| `chat.isGenerating` | 是否正在生成(只读) |
|
|
130
|
+
|
|
131
|
+
### SystemPromptStore
|
|
132
|
+
|
|
133
|
+
| 方法 | 说明 |
|
|
134
|
+
|------|------|
|
|
135
|
+
| `add(content, enabled?)` | 添加 system prompt |
|
|
136
|
+
| `remove(index)` | 删除 |
|
|
137
|
+
| `toggle(index)` | 切换启用/禁用 |
|
|
138
|
+
| `update(index, content)` | 修改内容 |
|
|
139
|
+
| `set(content)` | 清空并用一条替换 |
|
|
140
|
+
| `getEnabled()` | 返回启用的 `[{role:'system',content}]` |
|
|
141
|
+
| `getAll()` | 返回全部(含 enabled 状态) |
|
|
142
|
+
|
|
143
|
+
### MessageStore
|
|
144
|
+
|
|
145
|
+
| 方法 | 说明 |
|
|
146
|
+
|------|------|
|
|
147
|
+
| `add(msg)` | 添加消息 |
|
|
148
|
+
| `addUser(content)` / `addAssistant(content)` / `addSystem(content)` / `addTool(content, toolCallId)` | 快捷方法 |
|
|
149
|
+
| `addOnceAssistant(content, options?)` | 一次性引导消息:默认 `_ephemeral: true` + `prefix: true`,options 可传 `{ reasoningContent, prefix }` |
|
|
150
|
+
| `getAll()` | 返回浅拷贝 |
|
|
151
|
+
| `getLast()` | 最后一条(或 null) |
|
|
152
|
+
| `clear()` | 清空 |
|
|
153
|
+
| `undoToLastAssistant()` | 撤回最后 assistant 之后的消息 |
|
|
154
|
+
| `undoToPreviousUser(id)` | 撤回到指定消息之前的最近 user:删除该 user 之后到该消息(含)之间的消息(适用于重发) |
|
|
155
|
+
|
|
156
|
+
消息格式:
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
{
|
|
160
|
+
id: 'msg_1716300000_1',
|
|
161
|
+
role: 'user' | 'assistant' | 'system' | 'tool',
|
|
162
|
+
content: string,
|
|
163
|
+
images?: ['https://...'],
|
|
164
|
+
toolCalls?: [{ id, type:'function', function:{name,arguments} }],
|
|
165
|
+
toolCallId?: string,
|
|
166
|
+
reasoningContent?: string,
|
|
167
|
+
// 流式标记(自动添加)
|
|
168
|
+
_streaming?: true, // 正在生成
|
|
169
|
+
prefix?: boolean, // 前缀续写标记
|
|
170
|
+
_ephemeral?: boolean, // 临时消息:仅底部连续时参与请求,续写后转正
|
|
171
|
+
_complete?: boolean, // 流式是否完成(false=未完成/中断,true=正常结束),非流式无此字段
|
|
172
|
+
timestamp: 1716300000,
|
|
173
|
+
metadata?: any
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 事件
|
|
180
|
+
|
|
181
|
+
| 事件 | 数据 | 触发时机 |
|
|
182
|
+
|------|------|---------|
|
|
183
|
+
| `sending` | `{ addUser, userInput, timestamp }` | 请求发送前 |
|
|
184
|
+
| `stream-progress` | chunk 对象 | 每个流式块 |
|
|
185
|
+
| `message` | 助手消息对象 | 完整消息收到 |
|
|
186
|
+
| `aborted` | `{ timestamp }` | 用户调用 abort() 中断请求 |
|
|
187
|
+
| `error` | `{ error, timestamp }` | 请求失败(AbortError 不触发此事件) |
|
|
188
|
+
| `retry` | `{ attempt, maxRetries, lastError }` | 网络错误重试 |
|
|
189
|
+
| `config-updated` | `{ changes, timestamp }` | config 更新 |
|
|
190
|
+
| `tool-success` | `{ toolName, result, toolCallId }` | 工具执行成功 |
|
|
191
|
+
| `tool-error` | `{ toolName, error, toolCallId, stage }` | 工具执行失败(stage: parse / lookup / execute / timeout) |
|
|
192
|
+
| `beforeRequest` | `{ messages, config, options }` | 请求发送前,可修改 messages/config(见下方钩子说明) |
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
// 状态管理示例:用事件驱动 UI,不需要手动 catch
|
|
196
|
+
chat.on('sending', () => { /* 按钮切发送中 */ });
|
|
197
|
+
chat.on('message', () => { /* 按钮恢复 idle */ });
|
|
198
|
+
chat.on('aborted', () => { /* 按钮恢复 idle */ });
|
|
199
|
+
chat.on('error', ({ error }) => { /* 按钮恢复 idle + 显示错误 */ });
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
chat.on('error', ({ error }) => {
|
|
204
|
+
if (error instanceof APIError) console.error('API错误', error.statusCode);
|
|
205
|
+
if (error instanceof NetworkError) console.error('网络错误');
|
|
206
|
+
if (error instanceof ConfigurationError) console.error('配置错误');
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### beforeRequest 钩子
|
|
211
|
+
|
|
212
|
+
在 adapter 构建请求体之前触发,可修改 messages 或 config。所有请求(send / stream / continueLast / continueLastStream)都会触发。
|
|
213
|
+
|
|
214
|
+
```javascript
|
|
215
|
+
chat._hooks.on('beforeRequest', async ({ messages, config, options }) => {
|
|
216
|
+
// messages — MessageStore 实例,可直接 add/update/clear
|
|
217
|
+
// config — 当前配置对象,可修改(仅本次请求生效)
|
|
218
|
+
// options — 本次请求的参数
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`options` 字段:
|
|
223
|
+
|
|
224
|
+
| 字段 | 类型 | 说明 |
|
|
225
|
+
|------|------|------|
|
|
226
|
+
| `addUser` | boolean | `true` = 普通发送(send/stream),`false` = 续写/重发 |
|
|
227
|
+
| `isStream` | boolean | 是否流式 |
|
|
228
|
+
| `userInput` | string\|object | 用户输入(续写时为 undefined) |
|
|
229
|
+
| `mergeToEntry` | string\|undefined | 续写目标 id,有值 = 续写模式 |
|
|
230
|
+
| `onProgress` | function | 流式进度回调 |
|
|
231
|
+
| `onDone` | function | 流式结束回调 |
|
|
232
|
+
|
|
233
|
+
**区分普通发送和续写**:
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
chat._hooks.on('beforeRequest', async ({ messages, options }) => {
|
|
237
|
+
// 续写时不注入,避免重复
|
|
238
|
+
if (options.mergeToEntry) return;
|
|
239
|
+
|
|
240
|
+
// 普通发送时注入 ephemeral 引导
|
|
241
|
+
messages.add({
|
|
242
|
+
role: 'assistant', content: '',
|
|
243
|
+
_ephemeral: true, prefix: true
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 临时消息(Ephemeral)
|
|
251
|
+
|
|
252
|
+
标记 `_ephemeral: true` 的消息**仅底部连续时参与请求**,其余位置自动被 MessageFormatter 过滤。
|
|
253
|
+
续写后自动转正(`delete _ephemeral`),变为永久消息。
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
// 注入临时引导消息(仅本次请求生效)
|
|
257
|
+
chat._hooks.on('beforeRequest', async ({ messages }) => {
|
|
258
|
+
messages.add({
|
|
259
|
+
role: 'assistant', content: '让我分析一下:',
|
|
260
|
+
reasoningContent: '我需要逐步推理',
|
|
261
|
+
_ephemeral: true, prefix: true
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// 续写后自动转正
|
|
266
|
+
await chat.plugins['continuation'].continueLast();
|
|
267
|
+
// 该消息的 _ephemeral 已被清除,成为永久 assistant
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### 自动续写(ephemeralContinue)
|
|
271
|
+
|
|
272
|
+
`config.ephemeralContinue: true` 启用后,当底部消息带有 `prefix: true` 且角色为 `assistant` 或 `_ephemeral` 时,
|
|
273
|
+
框架自动将其设为续写目标(`mergeToEntry`),结果拼回原消息而非追加为新消息。
|
|
274
|
+
|
|
275
|
+
适用于 ephemeral 注入引导场景——钩子注入一条带 prefix 的引导消息后,API 的回复直接合并回该消息:
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
const chat = new ChatService({
|
|
279
|
+
apiKey: 'sk-xxx',
|
|
280
|
+
model: 'deepseek-chat',
|
|
281
|
+
ephemeralContinue: true
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
chat._hooks.on('beforeRequest', async ({ messages }) => {
|
|
285
|
+
messages.addOnceAssistant('让我分析一下', { reasoningContent: '逐步推理' });
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
启用后每次正常请求(send/stream)都会检测底部 prefix 消息,不影响已有语义。
|
|
290
|
+
不启用时 `prefix` 仅透传给 API,结果作为新消息追加。
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## MessageFormatter
|
|
295
|
+
|
|
296
|
+
```javascript
|
|
297
|
+
import { MessageFormatter } from 'my-ai-chat-framework';
|
|
298
|
+
|
|
299
|
+
// 注册 Anthropic 格式
|
|
300
|
+
MessageFormatter.register('anthropic', ({ messages, systemPrompts, capabilities }) => {
|
|
301
|
+
const result = [];
|
|
302
|
+
// Anthropic 格式:user/assistant 交替
|
|
303
|
+
for (const msg of messages) {
|
|
304
|
+
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
305
|
+
result.push({ role: msg.role, content: msg.content });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// 使用
|
|
312
|
+
const chat = new ChatService({
|
|
313
|
+
messageFormat: 'anthropic', // 指定格式
|
|
314
|
+
// ...
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// 列出所有格式
|
|
318
|
+
console.log(MessageFormatter.listFormats()); // ['openai', 'anthropic']
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 前缀续写
|
|
324
|
+
|
|
325
|
+
调用 `chat.plugins['continuation'].continueLast()` 从最后一条 assistant 消息续写——
|
|
326
|
+
模型直接从原内容末尾接续,结果拼回原消息(不新增消息)。
|
|
327
|
+
|
|
328
|
+
> 部分服务商需 baseUrl 加 `/beta` 开启该功能(如 DeepSeek)。
|
|
329
|
+
|
|
330
|
+
```javascript
|
|
331
|
+
// 中断后直接续写(流式标记 _complete 为 false)
|
|
332
|
+
chat.abort();
|
|
333
|
+
const msg = await chat.plugins['continuation'].continueLast();
|
|
334
|
+
// msg.content 已是原始 + 续写拼接,_complete 变为 true
|
|
335
|
+
|
|
336
|
+
// 已完成的对话也可以续写(但通常没必要)
|
|
337
|
+
await chat.plugins['continuation'].continueLast();
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## 流式消息与 Vue 渲染
|
|
343
|
+
|
|
344
|
+
框架自动在流式开始时 push 占位消息到 `messages`,并实时更新 content:
|
|
345
|
+
|
|
346
|
+
```vue
|
|
347
|
+
<template>
|
|
348
|
+
<div v-for="msg in messages" :key="msg.id">
|
|
349
|
+
<div v-if="msg.role === 'user'">{{ msg.content }}</div>
|
|
350
|
+
<div v-else-if="msg.role === 'assistant'" :class="{ streaming: msg._streaming }">
|
|
351
|
+
{{ msg.content }}
|
|
352
|
+
<span v-if="msg._complete === false">▊</span>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
</template>
|
|
356
|
+
|
|
357
|
+
<script setup>
|
|
358
|
+
import { ref } from 'vue';
|
|
359
|
+
const messages = ref([]);
|
|
360
|
+
|
|
361
|
+
chat.on('stream-progress', (chunk) => {
|
|
362
|
+
// 不需要手动 push,messages 已实时更新
|
|
363
|
+
// 只需触发 Vue 响应
|
|
364
|
+
});
|
|
365
|
+
</script>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## 自定义适配器
|
|
371
|
+
|
|
372
|
+
适配器契约(`config` 为合并后的请求参数,运输层可从实例自持或 `config` 读取):
|
|
373
|
+
|
|
374
|
+
```javascript
|
|
375
|
+
export const myAdapter = {
|
|
376
|
+
name: 'custom',
|
|
377
|
+
install(chat, options = {}) { chat.setAdapter(this); },
|
|
378
|
+
|
|
379
|
+
// 可选:工厂实例可提供的"请求默认参数"(运输键除外),
|
|
380
|
+
// 会话层会按 适配器默认 ← 会话配置 ← 本次覆盖 合并
|
|
381
|
+
getRequestDefaults() { return { modelParams: { temperature: 0.7 } }; },
|
|
382
|
+
|
|
383
|
+
buildRequest(messages, config, systemPrompts) {
|
|
384
|
+
return MessageFormatter.format({
|
|
385
|
+
messages, systemPrompts,
|
|
386
|
+
capabilities: config.capabilities,
|
|
387
|
+
format: config.messageFormat || 'openai'
|
|
388
|
+
});
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
async send(requestBody, config, options) { /* ... */ },
|
|
392
|
+
|
|
393
|
+
// stream 只负责解析 SSE:onProgress 传累积快照 { content, reasoningContent, toolCalls },
|
|
394
|
+
// onDone 传最终消息。占位消息的创建/更新由 ChatService 管理,adapter 不碰
|
|
395
|
+
async stream(requestBody, config, onProgress, onDone, options) {
|
|
396
|
+
// ...解析 chunk,accumulated 累积后调用 onProgress({ ...accumulated })
|
|
397
|
+
// 结束时调用 onDone(finalMessage)
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
parseResponse(apiResponse) { /* ... */ },
|
|
401
|
+
};
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## 完整示例
|
|
407
|
+
|
|
408
|
+
```javascript
|
|
409
|
+
import {
|
|
410
|
+
ChatService, openaiAdapter, toolCallingPlugin, modelRegistryPlugin,
|
|
411
|
+
APIError, NetworkError
|
|
412
|
+
} from 'my-ai-chat-framework';
|
|
413
|
+
|
|
414
|
+
const chat = new ChatService({
|
|
415
|
+
apiKey: 'sk-xxxx',
|
|
416
|
+
model: 'deepseek-chat',
|
|
417
|
+
system: '你是乐于助人的AI助手',
|
|
418
|
+
retry: { maxRetries: 2 },
|
|
419
|
+
toolTimeout: 10000,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
chat.use(openaiAdapter);
|
|
423
|
+
chat.use(modelRegistryPlugin);
|
|
424
|
+
chat.use(toolCallingPlugin);
|
|
425
|
+
|
|
426
|
+
chat.plugins['tool-calling'].registerTool('get_weather', '查询天气', async (args) => {
|
|
427
|
+
return `${args.city}: 22°C, 晴`;
|
|
428
|
+
}, { city: { type: 'string', description: '城市', required: true } });
|
|
429
|
+
|
|
430
|
+
chat.on('message', msg => console.log('AI:', msg.content));
|
|
431
|
+
chat.on('error', ({ error }) => console.error(error.message));
|
|
432
|
+
|
|
433
|
+
// 基础对话
|
|
434
|
+
await chat.send('你好');
|
|
435
|
+
|
|
436
|
+
// 工具调用
|
|
437
|
+
await chat.send('北京天气?');
|
|
438
|
+
|
|
439
|
+
// 手动中断
|
|
440
|
+
setTimeout(() => chat.abort(), 3000);
|
|
441
|
+
await chat.stream('讲个长故事', ...);
|
|
442
|
+
|
|
443
|
+
// 前缀续写(从最后一条 assistant 续写)
|
|
444
|
+
await chat.plugins['continuation'].continueLast();
|
|
445
|
+
// 结果拼回原消息,不新增消息
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## 扩展:写一个"车间"(v3.0)
|
|
451
|
+
|
|
452
|
+
核心流程已是一辆小车(ctx)依次开过车间。想加一个新功能 = 注册一个车间,核心代码一行不用改。
|
|
453
|
+
|
|
454
|
+
```javascript
|
|
455
|
+
const chat = new ChatService({ adapter, model: 'deepseek-chat' });
|
|
456
|
+
|
|
457
|
+
// 发送前:给每句话注入角色卡(ctx.messages 是 MessageStore,可用 add / addUser …)
|
|
458
|
+
chat.pipe({
|
|
459
|
+
name: '角色卡注入',
|
|
460
|
+
phase: 'beforeSend', // 默认即 beforeSend
|
|
461
|
+
async run(ctx) {
|
|
462
|
+
ctx.messages.add({ role: 'system', content: '你是傲娇霸总' });
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// 发送后:把回复打上时间戳(ctx.result 可改,send() 的返回值会跟着变)
|
|
467
|
+
chat.pipe({
|
|
468
|
+
name: '回复打标',
|
|
469
|
+
phase: 'afterSend',
|
|
470
|
+
run(ctx) {
|
|
471
|
+
if (ctx.result?.content) ctx.result.content += '(来自' + new Date().toLocaleTimeString() + ')';
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// 移除车间
|
|
476
|
+
chat.unpipe('角色卡注入');
|
|
477
|
+
```
|
|
478
|
+
|
|
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",
|
|
@@ -13,6 +13,35 @@
|
|
|
13
13
|
"browser": "./dist/my-ai-chat-framework.browser.umd.js"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "vite build",
|
|
18
|
+
"dev": "vite",
|
|
19
|
+
"preview": "vite preview",
|
|
20
|
+
"test": "node tests/core.test.js && node tests/pipeline.test.js",
|
|
21
|
+
"test:integration": "node tests/integration.test.js",
|
|
22
|
+
"selfcheck": "node examples/selfcheck.js",
|
|
23
|
+
"prepublishOnly": "npm test && npm run build"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist/",
|
|
27
|
+
"src/",
|
|
28
|
+
"README.md",
|
|
29
|
+
"README_ZH.md",
|
|
30
|
+
"CHANGELOG.md",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"docs/DEVELOPER.md"
|
|
33
|
+
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/233zrl/my-chat-test.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/233zrl/my-chat-test#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/233zrl/my-chat-test/issues"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
16
45
|
"keywords": [
|
|
17
46
|
"ai",
|
|
18
47
|
"chat",
|
|
@@ -22,16 +51,10 @@
|
|
|
22
51
|
"streaming",
|
|
23
52
|
"plugin"
|
|
24
53
|
],
|
|
25
|
-
"author": "
|
|
54
|
+
"author": "233zrl <zrl233333@outlook.com>",
|
|
26
55
|
"license": "MIT",
|
|
27
56
|
"devDependencies": {
|
|
28
57
|
"dotenv": "^17.3.1",
|
|
29
58
|
"vite": "^8.0.2"
|
|
30
|
-
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"build": "vite build",
|
|
33
|
-
"dev": "vite",
|
|
34
|
-
"preview": "vite preview",
|
|
35
|
-
"test": "node test.js"
|
|
36
59
|
}
|
|
37
60
|
}
|