my-ai-chat-framework 2.0.0 → 2.7.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/README.md +242 -45
- package/dist/my-ai-chat-framework.browser.es.js +1048 -184
- package/dist/my-ai-chat-framework.browser.es.js.map +1 -1
- package/dist/my-ai-chat-framework.browser.umd.js +1056 -188
- package/dist/my-ai-chat-framework.browser.umd.js.map +1 -1
- package/dist/my-ai-chat-framework.node.cjs.js +1056 -188
- package/dist/my-ai-chat-framework.node.cjs.js.map +1 -1
- package/package.json +7 -7
- package/src/adapters/openai.js +214 -64
- package/src/core/ChatService.js +298 -110
- package/src/core/Errors.js +60 -0
- package/src/core/EventEmitter.js +19 -0
- package/src/core/MessageStore.js +39 -0
- package/src/core/SystemPromptStore.js +118 -0
- package/src/index.js +10 -14
- package/src/plugins/model-registry.js +187 -0
- package/src/plugins/tool-calling.js +172 -78
- package/src/utils/MessageFormatter.js +204 -0
- package/src/utils/typeCheck.js +12 -0
- package/src/utils/url.js +18 -0
- package/.env +0 -15
- package/test.js +0 -107
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageFormatter — 可注册的消息格式转换器
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 内置 OpenA I兼容格式的转换逻辑
|
|
6
|
+
* 2. 支持 register(name, fn) 注册自定义格式(如 Anthropic、Gemini 等)
|
|
7
|
+
* 3. 统一处理 capabilities 过滤(reasoning、vision 等)
|
|
8
|
+
* 4. 所有适配器通过此工具获取 API 消息数组,消除重复代码
|
|
9
|
+
*
|
|
10
|
+
* 用法:
|
|
11
|
+
* import { MessageFormatter } from 'my-ai-chat-framework';
|
|
12
|
+
*
|
|
13
|
+
* // 使用内置格式
|
|
14
|
+
* const msgs = MessageFormatter.format({
|
|
15
|
+
* messages, systemPrompts, capabilities, resolveImage
|
|
16
|
+
* }); // 默认 'openai'
|
|
17
|
+
*
|
|
18
|
+
* // 注册自定义格式
|
|
19
|
+
* MessageFormatter.register('anthropic', ({ messages, systemPrompts, capabilities }) => {
|
|
20
|
+
* // 返回 Anthropic 格式的消息数组
|
|
21
|
+
* });
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { isString } from './typeCheck.js';
|
|
25
|
+
|
|
26
|
+
// ====== 内置格式 ======
|
|
27
|
+
|
|
28
|
+
const IS_URL = /^https?:\/\//i;
|
|
29
|
+
|
|
30
|
+
function defaultResolveImage(imageId) {
|
|
31
|
+
if (IS_URL.test(imageId)) return { url: imageId };
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildMultimodalContent(textContent, images, resolveImage) {
|
|
36
|
+
const content = [{ type: 'text', text: textContent }];
|
|
37
|
+
for (const ref of images) {
|
|
38
|
+
const resolved = resolveImage(ref);
|
|
39
|
+
if (!resolved) continue;
|
|
40
|
+
if (resolved.url) {
|
|
41
|
+
content.push({ type: 'image_url', image_url: { url: resolved.url } });
|
|
42
|
+
} else if (resolved.data) {
|
|
43
|
+
const mime = resolved.mimeType || 'image/png';
|
|
44
|
+
content.push({ type: 'image_url', image_url: { url: `data:${mime};base64,${resolved.data}` } });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return content;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** OpenAI 兼容格式 */
|
|
51
|
+
function toOpenAI({ messages, systemPrompts, capabilities, resolveImage }) {
|
|
52
|
+
const rImg = typeof resolveImage === 'function' ? resolveImage : defaultResolveImage;
|
|
53
|
+
const result = [];
|
|
54
|
+
|
|
55
|
+
// 1) system 置顶
|
|
56
|
+
for (const sp of systemPrompts) {
|
|
57
|
+
if (sp?.content && isString(sp.content) && sp.content.trim()) {
|
|
58
|
+
result.push({ role: 'system', content: sp.content.trim() });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2) 识别底部连续 ephemeral(只保留这些临时消息,其他 ephemeral 忽略)
|
|
63
|
+
let ephemEnd = messages.length;
|
|
64
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
65
|
+
if (messages[i]._ephemeral) ephemEnd = i;
|
|
66
|
+
else break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3) 过滤 + 字段映射
|
|
70
|
+
for (let i = 0; i < messages.length; i++) {
|
|
71
|
+
const msg = messages[i];
|
|
72
|
+
|
|
73
|
+
// ephemeral 消息:仅底部连续的保留,其余跳过
|
|
74
|
+
if (msg._ephemeral) {
|
|
75
|
+
if (i < ephemEnd) continue;
|
|
76
|
+
// 底部 ephemeral 按原始 role 处理,不加 _ephemeral 标记
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 非底部 ephemeral 的正常消息仍保留原有过滤
|
|
80
|
+
if (msg.role === 'system' && !msg._ephemeral) continue;
|
|
81
|
+
|
|
82
|
+
if (msg.role === 'tool') {
|
|
83
|
+
result.push({ role: 'tool', content: msg.content || '', tool_call_id: msg.toolCallId });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (msg.role === 'assistant' && msg.toolCalls?.length) {
|
|
88
|
+
const entry = { role: 'assistant', content: msg.content || '', tool_calls: msg.toolCalls };
|
|
89
|
+
if (msg.prefix) entry.prefix = true;
|
|
90
|
+
if (capabilities?.reasoning && msg.reasoningContent) {
|
|
91
|
+
entry.reasoning_content = msg.reasoningContent;
|
|
92
|
+
}
|
|
93
|
+
result.push(entry);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 常规消息
|
|
98
|
+
const hasText = msg.content && isString(msg.content) && msg.content.trim();
|
|
99
|
+
const hasImages = msg.images && Array.isArray(msg.images) && msg.images.length > 0;
|
|
100
|
+
if (!hasText && !hasImages) {
|
|
101
|
+
if (msg.role === 'assistant' && msg.prefix) {
|
|
102
|
+
const prefixEntry = { role: 'assistant', content: '', prefix: true };
|
|
103
|
+
if (capabilities?.reasoning && msg.reasoningContent) {
|
|
104
|
+
prefixEntry.reasoning_content = msg.reasoningContent;
|
|
105
|
+
}
|
|
106
|
+
result.push(prefixEntry);
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const entry = { role: msg.role };
|
|
112
|
+
|
|
113
|
+
if (hasImages) {
|
|
114
|
+
if (!capabilities?.vision) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
'[MessageFormatter] 消息包含图片但模型不支持视觉(capabilities.vision=false)。' +
|
|
117
|
+
'请切换模型或移除图片。'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
entry.content = buildMultimodalContent(msg.content || '', msg.images, rImg);
|
|
121
|
+
} else {
|
|
122
|
+
entry.content = msg.content;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (msg.prefix) entry.prefix = true;
|
|
126
|
+
// 常规 assistant 不回传 reasoningContent——API 自己会重新推理
|
|
127
|
+
result.push(entry);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ====== 格式化器 ======
|
|
134
|
+
|
|
135
|
+
export const MessageFormatter = {
|
|
136
|
+
_formats: new Map([
|
|
137
|
+
['openai', toOpenAI]
|
|
138
|
+
]),
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 注册自定义格式
|
|
142
|
+
* @param {string} name — 格式名称(如 'anthropic', 'gemini')
|
|
143
|
+
* @param {Function} fn — 转换函数,接收 { messages, systemPrompts, capabilities, resolveImage },返回消息数组
|
|
144
|
+
*/
|
|
145
|
+
register(name, fn) {
|
|
146
|
+
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
147
|
+
throw new Error('[MessageFormatter] 格式名称必须是非空字符串');
|
|
148
|
+
}
|
|
149
|
+
if (typeof fn !== 'function') {
|
|
150
|
+
throw new Error('[MessageFormatter] 转换函数必须是 function');
|
|
151
|
+
}
|
|
152
|
+
this._formats.set(name.trim(), fn);
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 移除自定义格式(内置格式不可移除)
|
|
157
|
+
* @param {string} name
|
|
158
|
+
*/
|
|
159
|
+
unregister(name) {
|
|
160
|
+
if (name === 'openai') {
|
|
161
|
+
throw new Error('[MessageFormatter] 内置格式 "openai" 不可移除');
|
|
162
|
+
}
|
|
163
|
+
this._formats.delete(name);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 列出所有已注册的格式名称
|
|
168
|
+
* @returns {Array<string>}
|
|
169
|
+
*/
|
|
170
|
+
listFormats() {
|
|
171
|
+
return [...this._formats.keys()];
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 按指定格式转换消息
|
|
176
|
+
* @param {Object} options
|
|
177
|
+
* @param {Array} options.messages — 内部消息列表
|
|
178
|
+
* @param {Array} [options.systemPrompts=[]] — 启用的 system prompt
|
|
179
|
+
* @param {Object} [options.capabilities={}] — 模型能力标签
|
|
180
|
+
* @param {Function} [options.resolveImage] — 图片解析钩子
|
|
181
|
+
* @param {string} [options.format='openai'] — 目标格式名称
|
|
182
|
+
* @returns {Array<Object>}
|
|
183
|
+
*/
|
|
184
|
+
format(options = {}) {
|
|
185
|
+
const formatName = options.format || 'openai';
|
|
186
|
+
const fn = this._formats.get(formatName);
|
|
187
|
+
if (!fn) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`[MessageFormatter] 未知格式 "${formatName}"。可用格式: ${[...this._formats.keys()].join(', ')}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return fn({
|
|
193
|
+
messages: options.messages || [],
|
|
194
|
+
systemPrompts: options.systemPrompts || [],
|
|
195
|
+
capabilities: options.capabilities || {},
|
|
196
|
+
resolveImage: options.resolveImage
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// 向后兼容:保留 assembleMessages 函数
|
|
202
|
+
export function assembleMessages(options = {}) {
|
|
203
|
+
return MessageFormatter.format({ ...options, format: 'openai' });
|
|
204
|
+
}
|
package/src/utils/url.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL 工具函数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 拼接 baseUrl 和 path
|
|
7
|
+
* 注意:path 应以 '/' 开头,否则会替换 baseUrl 的最后一段
|
|
8
|
+
* @param {string} baseUrl - 基础 URL(如 https://api.deepseek.com)
|
|
9
|
+
* @param {string} path - 路径(如 /v1/chat/completions)
|
|
10
|
+
* @returns {string} 完整的 URL
|
|
11
|
+
*/
|
|
12
|
+
export function joinUrl(baseUrl, path) {
|
|
13
|
+
// 移除 baseUrl 末尾的斜杠(如果有)
|
|
14
|
+
const base = baseUrl.replace(/\/$/, '');
|
|
15
|
+
// 确保 path 以斜杠开头
|
|
16
|
+
const p = path.startsWith('/') ? path : '/' + path;
|
|
17
|
+
return base + p;
|
|
18
|
+
}
|
package/.env
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# AI Chat Framework 环境变量配置
|
|
2
|
-
|
|
3
|
-
# DeepSeek API 配置
|
|
4
|
-
DEEPSEEK_API_KEY=sk-3b396e7fd64b463390d4ff033c6e7e0b
|
|
5
|
-
DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions
|
|
6
|
-
|
|
7
|
-
# 测试配置
|
|
8
|
-
TEST_API_KEY=sk-3b396e7fd64b463390d4ff033c6e7e0b
|
|
9
|
-
TEST_MODEL=deepseek-chat
|
|
10
|
-
TEST_TEMPERATURE=0.7
|
|
11
|
-
TEST_MAX_TOKENS=2000
|
|
12
|
-
|
|
13
|
-
# 调试配置
|
|
14
|
-
DEBUG=false
|
|
15
|
-
LOG_LEVEL=info
|
package/test.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
// test.js - 测试新框架核心功能
|
|
2
|
-
import 'dotenv/config'; // 如果已安装 dotenv,否则手动读取 .env
|
|
3
|
-
import { ChatService, openaiAdapter, toolCallingPlugin } from './src/index.js';
|
|
4
|
-
|
|
5
|
-
// 从环境变量读取配置(确保 .env 文件存在)
|
|
6
|
-
const apiKey = process.env.DEEPSEEK_API_KEY;
|
|
7
|
-
const apiUrl = process.env.DEEPSEEK_API_URL || 'https://api.deepseek.com/v1/chat/completions';
|
|
8
|
-
const model = process.env.TEST_MODEL || 'deepseek-chat';
|
|
9
|
-
|
|
10
|
-
if (!apiKey) {
|
|
11
|
-
console.error('❌ 未找到 API Key,请设置 DEEPSEEK_API_KEY 环境变量或创建 .env 文件');
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
console.log('🚀 开始测试新框架...\n');
|
|
16
|
-
|
|
17
|
-
// 创建聊天服务实例
|
|
18
|
-
const chat = new ChatService({
|
|
19
|
-
apiKey,
|
|
20
|
-
model,
|
|
21
|
-
apiUrl,
|
|
22
|
-
temperature: 0.7,
|
|
23
|
-
maxTokens: 2000
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// 加载适配器和插件
|
|
27
|
-
chat.use(openaiAdapter);
|
|
28
|
-
chat.use(toolCallingPlugin);
|
|
29
|
-
|
|
30
|
-
// 监听事件(便于观察)
|
|
31
|
-
chat.on('sending', msg => {
|
|
32
|
-
console.log('📤 发送消息:', msg.content?.substring(0, 50) || msg);
|
|
33
|
-
});
|
|
34
|
-
chat.on('message', msg => {
|
|
35
|
-
console.log('📥 收到回复:', msg.content?.substring(0, 100) || msg);
|
|
36
|
-
});
|
|
37
|
-
chat.on('stream-progress', chunk => {
|
|
38
|
-
console.log('🌊 流式数据块:', chunk.content?.substring(0, 30) || '');
|
|
39
|
-
});
|
|
40
|
-
chat.on('error', err => {
|
|
41
|
-
console.error('❌ 错误:', err.message);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// 注册一个简单测试工具(不实际调用,只验证注册和工具调用流程)
|
|
45
|
-
chat.registerTool('greet', '打招呼工具', async (args) => {
|
|
46
|
-
console.log('🔧 工具被调用:', args);
|
|
47
|
-
return `你好,${args.name || '朋友'}!`;
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// 辅助函数:等待指定毫秒
|
|
51
|
-
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
52
|
-
|
|
53
|
-
// 测试 1:基础发送消息(非流式)
|
|
54
|
-
console.log('===== 测试1:基础发送消息 =====');
|
|
55
|
-
const response = await chat.send('请简单说一句:你好,世界!');
|
|
56
|
-
console.log('最终回复:', response.content);
|
|
57
|
-
console.log();
|
|
58
|
-
|
|
59
|
-
// 等待一下,避免请求过快
|
|
60
|
-
await sleep(1000);
|
|
61
|
-
|
|
62
|
-
// 测试 2:流式发送消息
|
|
63
|
-
console.log('===== 测试2:流式发送消息 =====');
|
|
64
|
-
let streamContent = '';
|
|
65
|
-
await chat.stream('请用流式方式回答:告诉我一个好消息。',
|
|
66
|
-
(chunk) => {
|
|
67
|
-
streamContent += chunk.content;
|
|
68
|
-
// 实时打印已接收内容(可选)
|
|
69
|
-
// process.stdout.write(chunk.content);
|
|
70
|
-
},
|
|
71
|
-
(final) => {
|
|
72
|
-
console.log('流式完成,完整内容:', final.content);
|
|
73
|
-
}
|
|
74
|
-
);
|
|
75
|
-
console.log();
|
|
76
|
-
|
|
77
|
-
await sleep(1000);
|
|
78
|
-
|
|
79
|
-
// 测试 3:工具调用
|
|
80
|
-
console.log('===== 测试3:工具调用 =====');
|
|
81
|
-
// 注意:需要让 AI 主动调用 greet 工具。由于 deepseek-chat 支持函数调用,我们可以在系统提示或用户消息中引导。
|
|
82
|
-
// 我们在用户消息中明确要求调用工具,但 AI 是否调用取决于模型。这里做一个简单引导。
|
|
83
|
-
const toolResponse = await chat.send('请使用 greet 工具,向 "小明" 打招呼。');
|
|
84
|
-
console.log('工具调用后的回复:', toolResponse.content);
|
|
85
|
-
console.log();
|
|
86
|
-
|
|
87
|
-
await sleep(1000);
|
|
88
|
-
|
|
89
|
-
// 测试 4:验证消息存储
|
|
90
|
-
console.log('===== 测试4:消息存储验证 =====');
|
|
91
|
-
const allMessages = chat.messages.getAll();
|
|
92
|
-
console.log(`当前消息总数: ${allMessages.length}`);
|
|
93
|
-
allMessages.forEach((msg, i) => {
|
|
94
|
-
console.log(`[${i}] ${msg.role}: ${msg.content?.substring(0, 40)}${msg.content?.length > 40 ? '...' : ''}`);
|
|
95
|
-
if (msg.toolCalls) console.log(' 工具调用:', msg.toolCalls);
|
|
96
|
-
if (msg.toolCallId) console.log(' 工具调用ID:', msg.toolCallId);
|
|
97
|
-
});
|
|
98
|
-
console.log();
|
|
99
|
-
|
|
100
|
-
// 测试 5:撤回功能(可选)
|
|
101
|
-
console.log('===== 测试5:撤回最后一条助手消息 =====');
|
|
102
|
-
const removed = chat.messages.undoToLastAssistant();
|
|
103
|
-
console.log(`撤回了 ${removed.length} 条消息`);
|
|
104
|
-
const afterUndo = chat.messages.getAll();
|
|
105
|
-
console.log(`剩余消息数: ${afterUndo.length}`);
|
|
106
|
-
|
|
107
|
-
console.log('\n✅ 所有测试完成!');
|