@zhin.js/ai 0.0.1
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 +564 -0
- package/TOOLS.md +294 -0
- package/package.json +66 -0
- package/src/agent.ts +471 -0
- package/src/context-manager.ts +439 -0
- package/src/index.ts +1432 -0
- package/src/providers/anthropic.ts +375 -0
- package/src/providers/base.ts +173 -0
- package/src/providers/index.ts +13 -0
- package/src/providers/ollama.ts +283 -0
- package/src/providers/openai.ts +167 -0
- package/src/session.ts +537 -0
- package/src/tools.ts +205 -0
- package/src/types.ts +274 -0
- package/tests/agent.test.ts +484 -0
- package/tests/ai-trigger.test.ts +369 -0
- package/tests/context-manager.test.ts +387 -0
- package/tests/integration.test.ts +596 -0
- package/tests/providers.integration.test.ts +227 -0
- package/tests/session.test.ts +243 -0
- package/tests/setup.ts +304 -0
- package/tests/tool.test.ts +800 -0
- package/tests/tools-builtin.test.ts +346 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Providers 集成测试(真实 API 调用)
|
|
3
|
+
*
|
|
4
|
+
* 使用 test-bot 的环境配置进行真实 API 测试
|
|
5
|
+
*
|
|
6
|
+
* 运行方式(需要网络权限):
|
|
7
|
+
* pnpm test packages/ai/tests/providers.integration.test.ts
|
|
8
|
+
*
|
|
9
|
+
* 注意:此测试需要网络访问,在 sandbox 中会自动跳过
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
12
|
+
import { OllamaProvider } from '../src/providers/ollama.js';
|
|
13
|
+
import type { ChatMessage } from '@zhin.js/core';
|
|
14
|
+
|
|
15
|
+
// Ollama 配置
|
|
16
|
+
const OLLAMA_CONFIG = {
|
|
17
|
+
baseUrl: 'https://ollama.l2cl.link',
|
|
18
|
+
models: ['qwen2.5:7b', 'qwen3:8b'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// 全局状态
|
|
22
|
+
let canRun = false;
|
|
23
|
+
let provider: OllamaProvider | null = null;
|
|
24
|
+
|
|
25
|
+
// 跳过检查
|
|
26
|
+
const skipIfNoNetwork = (ctx: any) => {
|
|
27
|
+
if (!canRun) {
|
|
28
|
+
ctx.skip();
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
try {
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
38
|
+
const response = await globalThis.fetch(`${OLLAMA_CONFIG.baseUrl}/api/tags`, {
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
});
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
canRun = response.ok;
|
|
43
|
+
|
|
44
|
+
if (canRun) {
|
|
45
|
+
provider = new OllamaProvider({
|
|
46
|
+
baseUrl: OLLAMA_CONFIG.baseUrl,
|
|
47
|
+
models: OLLAMA_CONFIG.models,
|
|
48
|
+
});
|
|
49
|
+
console.log('✅ Ollama 服务可用');
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
canRun = false;
|
|
53
|
+
console.log('⚠️ 网络不可用,跳过集成测试');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('Ollama Provider 集成测试', () => {
|
|
58
|
+
describe('基本聊天', () => {
|
|
59
|
+
it('应该能进行简单对话', async (ctx) => {
|
|
60
|
+
if (skipIfNoNetwork(ctx)) return;
|
|
61
|
+
|
|
62
|
+
const response = await provider!.chat({
|
|
63
|
+
model: OLLAMA_CONFIG.models[0],
|
|
64
|
+
messages: [{ role: 'user', content: '你好,一句话介绍自己' }],
|
|
65
|
+
max_tokens: 100,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(response.choices[0].message.content).toBeTruthy();
|
|
69
|
+
console.log('📝 AI 回复:', response.choices[0].message.content);
|
|
70
|
+
}, 30000);
|
|
71
|
+
|
|
72
|
+
it('应该能处理多轮对话', async (ctx) => {
|
|
73
|
+
if (skipIfNoNetwork(ctx)) return;
|
|
74
|
+
|
|
75
|
+
const messages: ChatMessage[] = [
|
|
76
|
+
{ role: 'user', content: '记住:我叫小明。简短回复。' },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const r1 = await provider!.chat({
|
|
80
|
+
model: OLLAMA_CONFIG.models[0],
|
|
81
|
+
messages,
|
|
82
|
+
max_tokens: 30,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
messages.push({ role: 'assistant', content: r1.choices[0].message.content as string });
|
|
86
|
+
messages.push({ role: 'user', content: '我叫什么?' });
|
|
87
|
+
|
|
88
|
+
const r2 = await provider!.chat({
|
|
89
|
+
model: OLLAMA_CONFIG.models[0],
|
|
90
|
+
messages,
|
|
91
|
+
max_tokens: 30,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const reply = r2.choices[0].message.content as string;
|
|
95
|
+
console.log('📝 多轮对话:', reply);
|
|
96
|
+
expect(reply.toLowerCase()).toContain('小明');
|
|
97
|
+
}, 120000);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('工具调用', () => {
|
|
101
|
+
it('应该能调用计算器工具', async (ctx) => {
|
|
102
|
+
if (skipIfNoNetwork(ctx)) return;
|
|
103
|
+
|
|
104
|
+
const response = await provider!.chat({
|
|
105
|
+
model: OLLAMA_CONFIG.models[0],
|
|
106
|
+
messages: [
|
|
107
|
+
{ role: 'system', content: '使用 calculator 工具计算。' },
|
|
108
|
+
{ role: 'user', content: '计算 15 * 8' },
|
|
109
|
+
],
|
|
110
|
+
tools: [{
|
|
111
|
+
type: 'function',
|
|
112
|
+
function: {
|
|
113
|
+
name: 'calculator',
|
|
114
|
+
description: '计算数学表达式',
|
|
115
|
+
parameters: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: { expression: { type: 'string' } },
|
|
118
|
+
required: ['expression'],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
}],
|
|
122
|
+
tool_choice: 'auto',
|
|
123
|
+
max_tokens: 200,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
console.log('📝 工具调用:', JSON.stringify(response.choices[0].message, null, 2));
|
|
127
|
+
expect(response.choices[0].message).toBeDefined();
|
|
128
|
+
}, 30000);
|
|
129
|
+
|
|
130
|
+
it('应该能调用天气工具', async (ctx) => {
|
|
131
|
+
if (skipIfNoNetwork(ctx)) return;
|
|
132
|
+
|
|
133
|
+
const response = await provider!.chat({
|
|
134
|
+
model: OLLAMA_CONFIG.models[0],
|
|
135
|
+
messages: [
|
|
136
|
+
{ role: 'system', content: '使用 get_weather 工具查天气。' },
|
|
137
|
+
{ role: 'user', content: '上海天气?' },
|
|
138
|
+
],
|
|
139
|
+
tools: [{
|
|
140
|
+
type: 'function',
|
|
141
|
+
function: {
|
|
142
|
+
name: 'get_weather',
|
|
143
|
+
description: '查询天气',
|
|
144
|
+
parameters: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: { city: { type: 'string' } },
|
|
147
|
+
required: ['city'],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
}],
|
|
151
|
+
tool_choice: 'auto',
|
|
152
|
+
max_tokens: 200,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
console.log('📝 天气工具测试');
|
|
156
|
+
expect(response.choices[0].message).toBeDefined();
|
|
157
|
+
}, 30000);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('流式输出', () => {
|
|
161
|
+
it('应该能进行流式对话', async (ctx) => {
|
|
162
|
+
if (skipIfNoNetwork(ctx)) return;
|
|
163
|
+
|
|
164
|
+
const stream = await provider!.chatStream({
|
|
165
|
+
model: OLLAMA_CONFIG.models[0],
|
|
166
|
+
messages: [{ role: 'user', content: '写4行诗' }],
|
|
167
|
+
max_tokens: 100,
|
|
168
|
+
stream: true,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
let content = '';
|
|
172
|
+
let chunks = 0;
|
|
173
|
+
|
|
174
|
+
console.log('📝 流式输出:');
|
|
175
|
+
for await (const chunk of stream) {
|
|
176
|
+
const c = chunk.choices?.[0]?.delta?.content;
|
|
177
|
+
if (c) {
|
|
178
|
+
content += c;
|
|
179
|
+
chunks++;
|
|
180
|
+
process.stdout.write(c);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
console.log(`\n (${chunks} chunks)`);
|
|
184
|
+
|
|
185
|
+
expect(content.length).toBeGreaterThan(0);
|
|
186
|
+
}, 60000);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('错误处理', () => {
|
|
190
|
+
it('应该处理无效模型', async (ctx) => {
|
|
191
|
+
if (skipIfNoNetwork(ctx)) return;
|
|
192
|
+
|
|
193
|
+
await expect(
|
|
194
|
+
provider!.chat({
|
|
195
|
+
model: 'invalid-model-xyz',
|
|
196
|
+
messages: [{ role: 'user', content: 'test' }],
|
|
197
|
+
})
|
|
198
|
+
).rejects.toThrow();
|
|
199
|
+
}, 60000); // Ollama 拉取模型可能需要较长时间才会失败
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('健康检查', () => {
|
|
203
|
+
it('应该返回健康状态', async (ctx) => {
|
|
204
|
+
if (skipIfNoNetwork(ctx)) return;
|
|
205
|
+
|
|
206
|
+
const healthy = await provider!.healthCheck();
|
|
207
|
+
console.log('📝 健康:', healthy ? '✅' : '❌');
|
|
208
|
+
expect(typeof healthy).toBe('boolean');
|
|
209
|
+
}, 10000);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('性能测试', () => {
|
|
214
|
+
it('应该快速响应', async (ctx) => {
|
|
215
|
+
if (skipIfNoNetwork(ctx)) return;
|
|
216
|
+
|
|
217
|
+
const start = Date.now();
|
|
218
|
+
await provider!.chat({
|
|
219
|
+
model: OLLAMA_CONFIG.models[0],
|
|
220
|
+
messages: [{ role: 'user', content: '1+1' }],
|
|
221
|
+
max_tokens: 10,
|
|
222
|
+
});
|
|
223
|
+
const ms = Date.now() - start;
|
|
224
|
+
console.log(`📝 响应: ${ms}ms`);
|
|
225
|
+
expect(ms).toBeLessThan(30000);
|
|
226
|
+
}, 35000);
|
|
227
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager 测试
|
|
3
|
+
*
|
|
4
|
+
* 测试会话管理功能:
|
|
5
|
+
* - 会话创建和获取
|
|
6
|
+
* - 消息历史管理
|
|
7
|
+
* - 会话超时清理
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
|
|
11
|
+
// Mock Logger
|
|
12
|
+
vi.mock('@zhin.js/core', async (importOriginal) => {
|
|
13
|
+
const original = await importOriginal() as any;
|
|
14
|
+
return {
|
|
15
|
+
...original,
|
|
16
|
+
Logger: class {
|
|
17
|
+
debug = vi.fn();
|
|
18
|
+
info = vi.fn();
|
|
19
|
+
warn = vi.fn();
|
|
20
|
+
error = vi.fn();
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
MemorySessionManager,
|
|
27
|
+
SessionManager,
|
|
28
|
+
createMemorySessionManager
|
|
29
|
+
} from '../src/session.js';
|
|
30
|
+
|
|
31
|
+
describe('MemorySessionManager', () => {
|
|
32
|
+
let manager: MemorySessionManager;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.useFakeTimers();
|
|
36
|
+
manager = new MemorySessionManager({
|
|
37
|
+
maxHistory: 10,
|
|
38
|
+
expireMs: 60000, // 1 分钟
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
manager.dispose();
|
|
44
|
+
vi.useRealTimers();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('会话创建', () => {
|
|
48
|
+
it('应该创建新会话', () => {
|
|
49
|
+
const session = manager.get('user-1');
|
|
50
|
+
|
|
51
|
+
expect(session).toBeDefined();
|
|
52
|
+
expect(session.id).toBe('user-1');
|
|
53
|
+
expect(session.messages).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('应该返回已存在的会话', () => {
|
|
57
|
+
const session1 = manager.get('user-1');
|
|
58
|
+
manager.addMessage('user-1', { role: 'user', content: 'test' });
|
|
59
|
+
|
|
60
|
+
const session2 = manager.get('user-1');
|
|
61
|
+
|
|
62
|
+
expect(session2.messages).toHaveLength(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('不同用户应该有不同的会话', () => {
|
|
66
|
+
const session1 = manager.get('user-1');
|
|
67
|
+
const session2 = manager.get('user-2');
|
|
68
|
+
|
|
69
|
+
expect(session1.id).not.toBe(session2.id);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('消息历史', () => {
|
|
74
|
+
it('应该添加消息到历史', () => {
|
|
75
|
+
manager.addMessage('user-1', { role: 'user', content: '你好' });
|
|
76
|
+
manager.addMessage('user-1', { role: 'assistant', content: '你好!有什么可以帮你的?' });
|
|
77
|
+
|
|
78
|
+
const messages = manager.getMessages('user-1');
|
|
79
|
+
expect(messages).toHaveLength(2);
|
|
80
|
+
expect(messages[0].role).toBe('user');
|
|
81
|
+
expect(messages[1].role).toBe('assistant');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('应该限制历史记录数量', () => {
|
|
85
|
+
// 添加超过限制的消息
|
|
86
|
+
for (let i = 0; i < 15; i++) {
|
|
87
|
+
manager.addMessage('user-1', { role: 'user', content: `消息 ${i}` });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const messages = manager.getMessages('user-1');
|
|
91
|
+
expect(messages.length).toBeLessThanOrEqual(10);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('系统消息应该保留', () => {
|
|
95
|
+
manager.setSystemPrompt('user-1', '你是一个助手');
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < 15; i++) {
|
|
98
|
+
manager.addMessage('user-1', { role: 'user', content: `消息 ${i}` });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const messages = manager.getMessages('user-1');
|
|
102
|
+
const systemMessages = messages.filter(m => m.role === 'system');
|
|
103
|
+
expect(systemMessages.length).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('系统提示', () => {
|
|
108
|
+
it('应该设置系统提示', () => {
|
|
109
|
+
manager.setSystemPrompt('user-1', '你是一个助手');
|
|
110
|
+
|
|
111
|
+
const messages = manager.getMessages('user-1');
|
|
112
|
+
expect(messages[0].role).toBe('system');
|
|
113
|
+
expect(messages[0].content).toBe('你是一个助手');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('应该替换旧的系统提示', () => {
|
|
117
|
+
manager.setSystemPrompt('user-1', '旧提示');
|
|
118
|
+
manager.setSystemPrompt('user-1', '新提示');
|
|
119
|
+
|
|
120
|
+
const messages = manager.getMessages('user-1');
|
|
121
|
+
const systemMessages = messages.filter(m => m.role === 'system');
|
|
122
|
+
expect(systemMessages.length).toBe(1);
|
|
123
|
+
expect(systemMessages[0].content).toBe('新提示');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('会话清理', () => {
|
|
128
|
+
it('应该清除指定会话', () => {
|
|
129
|
+
manager.get('user-1');
|
|
130
|
+
manager.addMessage('user-1', { role: 'user', content: 'test' });
|
|
131
|
+
|
|
132
|
+
expect(manager.clear('user-1')).toBe(true);
|
|
133
|
+
expect(manager.has('user-1')).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('清除不存在的会话应返回 false', () => {
|
|
137
|
+
expect(manager.clear('nonexistent')).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('reset 应该保留系统消息', () => {
|
|
141
|
+
manager.setSystemPrompt('user-1', '系统提示');
|
|
142
|
+
manager.addMessage('user-1', { role: 'user', content: 'test' });
|
|
143
|
+
|
|
144
|
+
manager.reset('user-1');
|
|
145
|
+
|
|
146
|
+
const messages = manager.getMessages('user-1');
|
|
147
|
+
expect(messages.length).toBe(1);
|
|
148
|
+
expect(messages[0].role).toBe('system');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('会话超时', () => {
|
|
153
|
+
it('应该在超时后清理会话', () => {
|
|
154
|
+
manager.get('user-1');
|
|
155
|
+
|
|
156
|
+
expect(manager.has('user-1')).toBe(true);
|
|
157
|
+
|
|
158
|
+
// 前进时间超过超时时间
|
|
159
|
+
vi.advanceTimersByTime(61000);
|
|
160
|
+
|
|
161
|
+
// 触发清理
|
|
162
|
+
manager.cleanup();
|
|
163
|
+
|
|
164
|
+
expect(manager.has('user-1')).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('活动会话不应该被清理', () => {
|
|
168
|
+
manager.get('user-1');
|
|
169
|
+
|
|
170
|
+
// 前进一半时间
|
|
171
|
+
vi.advanceTimersByTime(30000);
|
|
172
|
+
|
|
173
|
+
// 添加新消息(刷新活动时间)
|
|
174
|
+
manager.addMessage('user-1', { role: 'user', content: '新消息' });
|
|
175
|
+
|
|
176
|
+
// 再前进一半时间
|
|
177
|
+
vi.advanceTimersByTime(30000);
|
|
178
|
+
|
|
179
|
+
manager.cleanup();
|
|
180
|
+
|
|
181
|
+
// 会话应该还在
|
|
182
|
+
expect(manager.has('user-1')).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('统计信息', () => {
|
|
187
|
+
it('应该返回正确的统计信息', () => {
|
|
188
|
+
manager.get('user-1');
|
|
189
|
+
manager.get('user-2');
|
|
190
|
+
|
|
191
|
+
const stats = manager.getStats();
|
|
192
|
+
expect(stats.total).toBe(2);
|
|
193
|
+
expect(stats.active).toBe(2);
|
|
194
|
+
expect(stats.expired).toBe(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('应该返回会话列表', () => {
|
|
198
|
+
manager.get('user-1');
|
|
199
|
+
manager.get('user-2');
|
|
200
|
+
manager.get('user-3');
|
|
201
|
+
|
|
202
|
+
const sessions = manager.listSessions();
|
|
203
|
+
expect(sessions).toContain('user-1');
|
|
204
|
+
expect(sessions).toContain('user-2');
|
|
205
|
+
expect(sessions).toContain('user-3');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('dispose', () => {
|
|
210
|
+
it('应该清理所有会话', () => {
|
|
211
|
+
manager.get('user-1');
|
|
212
|
+
manager.get('user-2');
|
|
213
|
+
|
|
214
|
+
manager.dispose();
|
|
215
|
+
|
|
216
|
+
expect(manager.has('user-1')).toBe(false);
|
|
217
|
+
expect(manager.has('user-2')).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('SessionManager', () => {
|
|
223
|
+
describe('generateId', () => {
|
|
224
|
+
it('应该生成正确的会话 ID(带频道)', () => {
|
|
225
|
+
const id = SessionManager.generateId('qq', 'user123', 'channel456');
|
|
226
|
+
expect(id).toBe('qq:channel456:user123');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('应该生成正确的会话 ID(无频道)', () => {
|
|
230
|
+
const id = SessionManager.generateId('telegram', 'user123');
|
|
231
|
+
expect(id).toBe('telegram:user123');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('createMemorySessionManager', () => {
|
|
237
|
+
it('应该创建 SessionManager 实例', () => {
|
|
238
|
+
const manager = createMemorySessionManager({ maxHistory: 50 });
|
|
239
|
+
expect(manager).toBeDefined();
|
|
240
|
+
expect(manager.get).toBeDefined();
|
|
241
|
+
manager.dispose();
|
|
242
|
+
});
|
|
243
|
+
});
|