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
|
@@ -0,0 +1,479 @@
|
|
|
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.continueLast()` | 前缀续写:从最后一条 assistant/ephemeral 续写并拼回原消息,续写后自动转正。AbortError 内部捕获,返回 target |
|
|
115
|
+
| `chat.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.registerTool(name, desc, fn, params?)` | 注册工具(需 toolCallingPlugin) |
|
|
120
|
+
| `chat.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.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.continueLast()` 从最后一条 assistant 消息续写——
|
|
326
|
+
模型直接从原内容末尾接续,结果拼回原消息(不新增消息)。
|
|
327
|
+
|
|
328
|
+
> 部分服务商需 baseUrl 加 `/beta` 开启该功能(如 DeepSeek)。
|
|
329
|
+
|
|
330
|
+
```javascript
|
|
331
|
+
// 中断后直接续写(流式标记 _complete 为 false)
|
|
332
|
+
chat.abort();
|
|
333
|
+
const msg = await chat.continueLast();
|
|
334
|
+
// msg.content 已是原始 + 续写拼接,_complete 变为 true
|
|
335
|
+
|
|
336
|
+
// 已完成的对话也可以续写(但通常没必要)
|
|
337
|
+
await chat.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.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.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。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-ai-chat-framework",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.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
|
}
|
package/src/adapters/openai.js
CHANGED
|
@@ -8,29 +8,64 @@ import { isString } from "../utils/typeCheck.js";
|
|
|
8
8
|
import { APIError, NetworkError, ParsingError } from '../core/Errors.js';
|
|
9
9
|
import { MessageFormatter } from '../utils/MessageFormatter.js';
|
|
10
10
|
|
|
11
|
+
// 属于"运输层"的配置键:工厂实例持有,请求时覆盖 config 中同名项
|
|
12
|
+
const TRANSPORT_KEYS = ['apiKey', 'baseUrl', 'apiUrl', 'path', 'headers'];
|
|
13
|
+
|
|
11
14
|
export const openaiAdapter = {
|
|
12
15
|
name: 'openai',
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
/**
|
|
18
|
+
* 安装适配器。
|
|
19
|
+
* - 单例(openaiAdapter):options 无意义,配置从 chat.config 读取(平铺兼容路径)
|
|
20
|
+
* - 工厂实例(createOpenAIAdapter):options 并入实例自持的运输配置
|
|
21
|
+
*/
|
|
22
|
+
install(chatService, options = {}) {
|
|
23
|
+
if (this._transport) this._transport = { ...this._transport, ...options };
|
|
15
24
|
chatService.setAdapter(this);
|
|
16
25
|
},
|
|
17
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 实例自持配置(运输层)覆盖调用方传入的 config 同名项。
|
|
29
|
+
* modelParams 特殊处理:深度合并(config 优先,transport 兜底)——
|
|
30
|
+
* 避免工厂实例的默认参数覆盖掉会话/请求级覆盖。
|
|
31
|
+
*/
|
|
32
|
+
_resolveConfig(config) {
|
|
33
|
+
const transport = this._transport || {};
|
|
34
|
+
const resolved = { ...config, ...transport };
|
|
35
|
+
if (transport.modelParams) {
|
|
36
|
+
resolved.modelParams = { ...transport.modelParams, ...(config.modelParams || {}) };
|
|
37
|
+
}
|
|
38
|
+
return resolved;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/** 工厂实例可提供给会话层的"请求默认参数"(非运输键的部分) */
|
|
42
|
+
getRequestDefaults() {
|
|
43
|
+
if (!this._transport) return {};
|
|
44
|
+
const defaults = {};
|
|
45
|
+
for (const [key, value] of Object.entries(this._transport)) {
|
|
46
|
+
if (!TRANSPORT_KEYS.includes(key) && value !== undefined) defaults[key] = value;
|
|
47
|
+
}
|
|
48
|
+
return defaults;
|
|
49
|
+
},
|
|
50
|
+
|
|
18
51
|
/**
|
|
19
52
|
* 构建 OpenAI 格式的请求体
|
|
20
53
|
* @param {Array} messages - 内部消息列表
|
|
21
|
-
* @param {Object} config -
|
|
54
|
+
* @param {Object} config - 合并后的请求参数(含 model/temperature 等)
|
|
22
55
|
* @param {Array} [systemPrompts=[]] - 启用的 system prompt 列表
|
|
23
56
|
* @returns {Object} 请求体
|
|
24
57
|
*/
|
|
25
58
|
buildRequest(messages, config, systemPrompts = []) {
|
|
59
|
+
config = this._resolveConfig(config);
|
|
26
60
|
const model = config.model || config.modelParams?.model;
|
|
27
61
|
if (!model) {
|
|
28
62
|
throw new Error('Missing required config: model (either at top level or in modelParams)');
|
|
29
63
|
}
|
|
30
64
|
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
65
|
+
const mp = config.modelParams || {};
|
|
66
|
+
const temperature = mp.temperature ?? config.temperature ?? 0.7;
|
|
67
|
+
const maxTokens = mp.maxTokens ?? config.maxTokens ?? 2000;
|
|
68
|
+
const reasoningEffort = mp.reasoningEffort ?? config.reasoningEffort;
|
|
34
69
|
|
|
35
70
|
// 用 MessageFormatter 统一处理:system 置顶、消息过滤、字段映射、图片解析
|
|
36
71
|
// format 由 config.messageFormat 指定(默认 'openai')
|
|
@@ -50,15 +85,35 @@ export const openaiAdapter = {
|
|
|
50
85
|
stream: false
|
|
51
86
|
};
|
|
52
87
|
|
|
88
|
+
// 硬编码常用参数:驼峰命名,适配器负责转 API 格式
|
|
89
|
+
if (mp.topP !== undefined) requestBody.top_p = mp.topP;
|
|
90
|
+
if (mp.frequencyPenalty !== undefined) requestBody.frequency_penalty = mp.frequencyPenalty;
|
|
91
|
+
if (mp.presencePenalty !== undefined) requestBody.presence_penalty = mp.presencePenalty;
|
|
92
|
+
if (mp.stop !== undefined) requestBody.stop = mp.stop;
|
|
93
|
+
if (mp.responseFormat !== undefined) requestBody.response_format = mp.responseFormat;
|
|
94
|
+
if (mp.seed !== undefined) requestBody.seed = mp.seed;
|
|
95
|
+
|
|
53
96
|
if (config.tools && Array.isArray(config.tools) && config.tools.length > 0) {
|
|
54
97
|
requestBody.tools = config.tools;
|
|
55
98
|
requestBody.tool_choice = 'auto';
|
|
56
99
|
}
|
|
57
100
|
|
|
58
|
-
if (reasoningEffort
|
|
101
|
+
if (reasoningEffort !== undefined) {
|
|
59
102
|
requestBody.reasoning_effort = reasoningEffort;
|
|
60
103
|
}
|
|
61
104
|
|
|
105
|
+
// modelParams 中未被硬编码处理的字段,透传到请求体
|
|
106
|
+
const consumedKeys = new Set([
|
|
107
|
+
'model', 'temperature', 'maxTokens', 'reasoningEffort',
|
|
108
|
+
'topP', 'frequencyPenalty', 'presencePenalty',
|
|
109
|
+
'stop', 'responseFormat', 'seed'
|
|
110
|
+
]);
|
|
111
|
+
for (const [key, value] of Object.entries(mp)) {
|
|
112
|
+
if (!consumedKeys.has(key) && value !== undefined) {
|
|
113
|
+
requestBody[key] = value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
62
117
|
return requestBody;
|
|
63
118
|
},
|
|
64
119
|
|
|
@@ -93,6 +148,7 @@ export const openaiAdapter = {
|
|
|
93
148
|
*/
|
|
94
149
|
async send(requestBody, config, options = {}) {
|
|
95
150
|
try {
|
|
151
|
+
config = this._resolveConfig(config);
|
|
96
152
|
const url = this._getUrl(config);
|
|
97
153
|
|
|
98
154
|
const headers = {
|
|
@@ -156,6 +212,7 @@ export const openaiAdapter = {
|
|
|
156
212
|
* @param {Object} [options] — { signal, headers: {...} }
|
|
157
213
|
*/
|
|
158
214
|
async stream(requestBody, config, onProgress, onDone, options = {}) {
|
|
215
|
+
config = this._resolveConfig(config);
|
|
159
216
|
const streamBody = { ...requestBody, stream: true };
|
|
160
217
|
const url = this._getUrl(config);
|
|
161
218
|
let response;
|
|
@@ -277,4 +334,22 @@ export const openaiAdapter = {
|
|
|
277
334
|
throw new APIError(errorMessage, response.status, null, errorText);
|
|
278
335
|
}
|
|
279
336
|
|
|
280
|
-
};
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 创建 OpenAI 兼容适配器实例(工厂)。
|
|
341
|
+
*
|
|
342
|
+
* 与单例 openaiAdapter 的区别:每个实例自持一份运输配置(apiKey/baseUrl/apiUrl/path/headers),
|
|
343
|
+
* 互不干扰——解决"同一适配器装到多个 ChatService 互相覆盖"的单例陷阱。
|
|
344
|
+
*
|
|
345
|
+
* 用法:
|
|
346
|
+
* const adapter = createOpenAIAdapter({ apiKey, baseUrl, modelParams: { temperature: 0.8 } });
|
|
347
|
+
* const chat = new ChatService({ adapter, model: 'deepseek-chat' });
|
|
348
|
+
* chat.use(adapter); // 或直接 new ChatService({ adapter })
|
|
349
|
+
*
|
|
350
|
+
* 非运输键(model/modelParams/messageFormat/resolveImage/capabilities 等)会作为
|
|
351
|
+
* "请求默认参数"供会话层合并,单次请求仍可覆盖。
|
|
352
|
+
*/
|
|
353
|
+
export function createOpenAIAdapter(options = {}) {
|
|
354
|
+
return { ...openaiAdapter, _transport: { ...options } };
|
|
355
|
+
}
|