@zhin.js/agent 0.0.2 → 0.0.3

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/lib/agent.d.ts +4 -129
  3. package/lib/agent.d.ts.map +1 -1
  4. package/lib/agent.js +3 -733
  5. package/lib/agent.js.map +1 -1
  6. package/lib/compaction.d.ts +3 -129
  7. package/lib/compaction.d.ts.map +1 -1
  8. package/lib/compaction.js +2 -367
  9. package/lib/compaction.js.map +1 -1
  10. package/lib/context-manager.d.ts +3 -210
  11. package/lib/context-manager.d.ts.map +1 -1
  12. package/lib/context-manager.js +2 -310
  13. package/lib/context-manager.js.map +1 -1
  14. package/lib/conversation-memory.d.ts +3 -189
  15. package/lib/conversation-memory.d.ts.map +1 -1
  16. package/lib/conversation-memory.js +2 -616
  17. package/lib/conversation-memory.js.map +1 -1
  18. package/lib/init/create-zhin-agent.d.ts.map +1 -1
  19. package/lib/init/create-zhin-agent.js +1 -3
  20. package/lib/init/create-zhin-agent.js.map +1 -1
  21. package/lib/output.d.ts +3 -90
  22. package/lib/output.d.ts.map +1 -1
  23. package/lib/output.js +2 -173
  24. package/lib/output.js.map +1 -1
  25. package/lib/rate-limiter.d.ts +3 -35
  26. package/lib/rate-limiter.d.ts.map +1 -1
  27. package/lib/rate-limiter.js +2 -83
  28. package/lib/rate-limiter.js.map +1 -1
  29. package/lib/session.d.ts +3 -190
  30. package/lib/session.d.ts.map +1 -1
  31. package/lib/session.js +2 -462
  32. package/lib/session.js.map +1 -1
  33. package/lib/storage.d.ts +3 -65
  34. package/lib/storage.d.ts.map +1 -1
  35. package/lib/storage.js +2 -102
  36. package/lib/storage.js.map +1 -1
  37. package/lib/tone-detector.d.ts +3 -16
  38. package/lib/tone-detector.d.ts.map +1 -1
  39. package/lib/tone-detector.js +2 -69
  40. package/lib/tone-detector.js.map +1 -1
  41. package/package.json +3 -2
  42. package/src/agent.ts +4 -852
  43. package/src/compaction.ts +27 -528
  44. package/src/context-manager.ts +14 -439
  45. package/src/conversation-memory.ts +3 -814
  46. package/src/init/create-zhin-agent.ts +1 -3
  47. package/src/output.ts +14 -260
  48. package/src/rate-limiter.ts +3 -127
  49. package/src/session.ts +12 -565
  50. package/src/storage.ts +8 -134
  51. package/src/tone-detector.ts +3 -87
  52. package/tests/ai/setup.ts +20 -84
  53. package/tests/ai/agent.test.ts +0 -565
  54. package/tests/ai/context-manager.test.ts +0 -413
  55. package/tests/ai/conversation-memory.test.ts +0 -128
  56. package/tests/ai/output.test.ts +0 -128
  57. package/tests/ai/rate-limiter.test.ts +0 -108
  58. package/tests/ai/session.test.ts +0 -334
  59. package/tests/ai/tone-detector.test.ts +0 -80
@@ -1,108 +0,0 @@
1
- /**
2
- * RateLimiter 测试
3
- */
4
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
5
- import { RateLimiter } from '@zhin.js/agent';
6
-
7
- describe('RateLimiter', () => {
8
- let limiter: RateLimiter;
9
-
10
- beforeEach(() => {
11
- vi.useFakeTimers();
12
- });
13
-
14
- afterEach(() => {
15
- limiter?.dispose();
16
- vi.useRealTimers();
17
- });
18
-
19
- it('应允许正常请求', () => {
20
- limiter = new RateLimiter({ maxRequestsPerMinute: 5 });
21
- const result = limiter.check('user1');
22
- expect(result.allowed).toBe(true);
23
- expect(result.message).toBeUndefined();
24
- });
25
-
26
- it('应在超过限制后拒绝请求', () => {
27
- limiter = new RateLimiter({ maxRequestsPerMinute: 3, cooldownSeconds: 5 });
28
-
29
- // 发送 3 个请求(达到限制)
30
- limiter.check('user1');
31
- limiter.check('user1');
32
- limiter.check('user1');
33
-
34
- // 第 4 个请求应被拒绝
35
- const result = limiter.check('user1');
36
- expect(result.allowed).toBe(false);
37
- expect(result.message).toBeDefined();
38
- expect(result.retryAfterSeconds).toBe(5);
39
- });
40
-
41
- it('冷却期结束后应允许请求', () => {
42
- limiter = new RateLimiter({ maxRequestsPerMinute: 2, cooldownSeconds: 5 });
43
-
44
- limiter.check('user1');
45
- limiter.check('user1');
46
- limiter.check('user1'); // 触发冷却
47
-
48
- // 推进 6 秒
49
- vi.advanceTimersByTime(6000);
50
-
51
- // 推进超过 1 分钟让滑动窗口清空旧记录
52
- vi.advanceTimersByTime(60000);
53
-
54
- const result = limiter.check('user1');
55
- expect(result.allowed).toBe(true);
56
- });
57
-
58
- it('不同用户应独立计数', () => {
59
- limiter = new RateLimiter({ maxRequestsPerMinute: 2 });
60
-
61
- limiter.check('user1');
62
- limiter.check('user1');
63
- const user1Result = limiter.check('user1'); // 超限
64
-
65
- const user2Result = limiter.check('user2'); // 新用户
66
- expect(user1Result.allowed).toBe(false);
67
- expect(user2Result.allowed).toBe(true);
68
- });
69
-
70
- it('滑动窗口应在 1 分钟后清理旧记录', () => {
71
- limiter = new RateLimiter({ maxRequestsPerMinute: 2 });
72
-
73
- limiter.check('user1');
74
- limiter.check('user1');
75
-
76
- // 推进 61 秒
77
- vi.advanceTimersByTime(61000);
78
-
79
- // 旧记录清理后应允许
80
- const result = limiter.check('user1');
81
- expect(result.allowed).toBe(true);
82
- });
83
-
84
- it('dispose 应清理所有资源', () => {
85
- limiter = new RateLimiter();
86
- limiter.check('user1');
87
- limiter.dispose();
88
-
89
- // dispose 后内部 buckets 应清空
90
- const result = limiter.check('user1');
91
- expect(result.allowed).toBe(true); // 重新开始计数
92
- });
93
-
94
- it('在冷却期内请求应返回等待秒数', () => {
95
- limiter = new RateLimiter({ maxRequestsPerMinute: 1, cooldownSeconds: 10 });
96
-
97
- limiter.check('user1');
98
- limiter.check('user1'); // 触发冷却
99
-
100
- // 推进 3 秒
101
- vi.advanceTimersByTime(3000);
102
-
103
- const result = limiter.check('user1');
104
- expect(result.allowed).toBe(false);
105
- expect(result.retryAfterSeconds).toBeLessThanOrEqual(10);
106
- expect(result.retryAfterSeconds).toBeGreaterThan(0);
107
- });
108
- });
@@ -1,334 +0,0 @@
1
- /**
2
- * Session Manager 测试
3
- */
4
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
5
-
6
- vi.mock('@zhin.js/core', async (importOriginal) => {
7
- const original = await importOriginal() as any;
8
- return {
9
- ...original,
10
- Logger: class {
11
- debug = vi.fn();
12
- info = vi.fn();
13
- warn = vi.fn();
14
- error = vi.fn();
15
- },
16
- };
17
- });
18
-
19
- import {
20
- MemorySessionManager,
21
- DatabaseSessionManager,
22
- SessionManager,
23
- createMemorySessionManager,
24
- } from '@zhin.js/agent';
25
-
26
- describe('MemorySessionManager', () => {
27
- let manager: MemorySessionManager;
28
-
29
- beforeEach(() => {
30
- vi.useFakeTimers();
31
- manager = new MemorySessionManager({
32
- maxHistory: 10,
33
- expireMs: 60000,
34
- });
35
- });
36
-
37
- afterEach(() => {
38
- manager.dispose();
39
- vi.useRealTimers();
40
- });
41
-
42
- describe('会话创建', () => {
43
- it('应该创建新会话', () => {
44
- const session = manager.get('user-1');
45
- expect(session).toBeDefined();
46
- expect(session.id).toBe('user-1');
47
- expect(session.messages).toEqual([]);
48
- });
49
-
50
- it('应该返回已存在的会话', () => {
51
- const session1 = manager.get('user-1');
52
- manager.addMessage('user-1', { role: 'user', content: 'test' });
53
- const session2 = manager.get('user-1');
54
- expect(session2.messages).toHaveLength(1);
55
- });
56
-
57
- it('不同用户应该有不同的会话', () => {
58
- const session1 = manager.get('user-1');
59
- const session2 = manager.get('user-2');
60
- expect(session1.id).not.toBe(session2.id);
61
- });
62
- });
63
-
64
- describe('消息历史', () => {
65
- it('应该添加消息到历史', () => {
66
- manager.addMessage('user-1', { role: 'user', content: '你好' });
67
- manager.addMessage('user-1', { role: 'assistant', content: '你好!有什么可以帮你的?' });
68
- const messages = manager.getMessages('user-1');
69
- expect(messages).toHaveLength(2);
70
- expect(messages[0].role).toBe('user');
71
- expect(messages[1].role).toBe('assistant');
72
- });
73
-
74
- it('应该限制历史记录数量', () => {
75
- for (let i = 0; i < 15; i++) {
76
- manager.addMessage('user-1', { role: 'user', content: `消息 ${i}` });
77
- }
78
- const messages = manager.getMessages('user-1');
79
- expect(messages.length).toBeLessThanOrEqual(10);
80
- });
81
-
82
- it('系统消息应该保留', () => {
83
- manager.setSystemPrompt('user-1', '你是一个助手');
84
- for (let i = 0; i < 15; i++) {
85
- manager.addMessage('user-1', { role: 'user', content: `消息 ${i}` });
86
- }
87
- const messages = manager.getMessages('user-1');
88
- const systemMessages = messages.filter((m: { role: string }) => m.role === 'system');
89
- expect(systemMessages.length).toBe(1);
90
- });
91
- });
92
-
93
- describe('系统提示', () => {
94
- it('应该设置系统提示', () => {
95
- manager.setSystemPrompt('user-1', '你是一个助手');
96
- const messages = manager.getMessages('user-1');
97
- expect(messages[0].role).toBe('system');
98
- expect(messages[0].content).toBe('你是一个助手');
99
- });
100
-
101
- it('应该替换旧的系统提示', () => {
102
- manager.setSystemPrompt('user-1', '旧提示');
103
- manager.setSystemPrompt('user-1', '新提示');
104
- const messages = manager.getMessages('user-1');
105
- const systemMessages = messages.filter((m: { role: string }) => m.role === 'system');
106
- expect(systemMessages.length).toBe(1);
107
- expect(systemMessages[0].content).toBe('新提示');
108
- });
109
- });
110
-
111
- describe('会话清理', () => {
112
- it('应该清除指定会话', () => {
113
- manager.get('user-1');
114
- manager.addMessage('user-1', { role: 'user', content: 'test' });
115
- expect(manager.clear('user-1')).toBe(true);
116
- expect(manager.has('user-1')).toBe(false);
117
- });
118
-
119
- it('清除不存在的会话应返回 false', () => {
120
- expect(manager.clear('nonexistent')).toBe(false);
121
- });
122
-
123
- it('reset 应该保留系统消息', () => {
124
- manager.setSystemPrompt('user-1', '系统提示');
125
- manager.addMessage('user-1', { role: 'user', content: 'test' });
126
- manager.reset('user-1');
127
- const messages = manager.getMessages('user-1');
128
- expect(messages.length).toBe(1);
129
- expect(messages[0].role).toBe('system');
130
- });
131
- });
132
-
133
- describe('会话超时', () => {
134
- it('应该在超时后清理会话', () => {
135
- manager.get('user-1');
136
- expect(manager.has('user-1')).toBe(true);
137
- vi.advanceTimersByTime(61000);
138
- manager.cleanup();
139
- expect(manager.has('user-1')).toBe(false);
140
- });
141
-
142
- it('活动会话不应该被清理', () => {
143
- manager.get('user-1');
144
- vi.advanceTimersByTime(30000);
145
- manager.addMessage('user-1', { role: 'user', content: '新消息' });
146
- vi.advanceTimersByTime(30000);
147
- manager.cleanup();
148
- expect(manager.has('user-1')).toBe(true);
149
- });
150
- });
151
-
152
- describe('统计信息', () => {
153
- it('应该返回正确的统计信息', () => {
154
- manager.get('user-1');
155
- manager.get('user-2');
156
- const stats = manager.getStats();
157
- expect(stats.total).toBe(2);
158
- expect(stats.active).toBe(2);
159
- expect(stats.expired).toBe(0);
160
- });
161
-
162
- it('应该返回会话列表', () => {
163
- manager.get('user-1');
164
- manager.get('user-2');
165
- manager.get('user-3');
166
- const sessions = manager.listSessions();
167
- expect(sessions).toContain('user-1');
168
- expect(sessions).toContain('user-2');
169
- expect(sessions).toContain('user-3');
170
- });
171
- });
172
-
173
- describe('dispose', () => {
174
- it('应该清理所有会话', () => {
175
- manager.get('user-1');
176
- manager.get('user-2');
177
- manager.dispose();
178
- expect(manager.has('user-1')).toBe(false);
179
- expect(manager.has('user-2')).toBe(false);
180
- });
181
- });
182
- });
183
-
184
- describe('SessionManager', () => {
185
- describe('generateId', () => {
186
- it('应该生成正确的会话 ID(带频道)', () => {
187
- const id = SessionManager.generateId('qq', 'user123', 'channel456');
188
- expect(id).toBe('qq:channel456:user123');
189
- });
190
-
191
- it('应该生成正确的会话 ID(无频道)', () => {
192
- const id = SessionManager.generateId('telegram', 'user123');
193
- expect(id).toBe('telegram:user123');
194
- });
195
- });
196
- });
197
-
198
- describe('createMemorySessionManager', () => {
199
- it('应该创建 SessionManager 实例', () => {
200
- const manager = createMemorySessionManager({ maxHistory: 50 });
201
- expect(manager).toBeDefined();
202
- expect(manager.get).toBeDefined();
203
- manager.dispose();
204
- });
205
- });
206
-
207
- describe('DatabaseSessionManager', () => {
208
- function createMockModel(records: any[] = []) {
209
- return {
210
- select: vi.fn().mockReturnValue({
211
- where: vi.fn().mockResolvedValue(records),
212
- }),
213
- create: vi.fn().mockResolvedValue(undefined),
214
- update: vi.fn().mockReturnValue({
215
- where: vi.fn().mockResolvedValue(undefined),
216
- }),
217
- delete: vi.fn().mockReturnValue({
218
- where: vi.fn().mockResolvedValue(undefined),
219
- }),
220
- };
221
- }
222
-
223
- afterEach(() => {
224
- vi.useRealTimers();
225
- });
226
-
227
- describe('loadSession JSON 解析', () => {
228
- it('应该解析字符串形式的 messages (JSON)', async () => {
229
- const messagesJson = JSON.stringify([
230
- { role: 'user', content: 'hello' },
231
- { role: 'assistant', content: 'hi there' },
232
- ]);
233
- const model = createMockModel([{
234
- session_id: 'test-1',
235
- messages: messagesJson,
236
- config: JSON.stringify({ provider: 'ollama' }),
237
- created_at: 1000,
238
- updated_at: 2000,
239
- }]);
240
-
241
- const manager = new DatabaseSessionManager(model);
242
- const session = await manager.get('test-1');
243
-
244
- expect(Array.isArray(session.messages)).toBe(true);
245
- expect(session.messages).toHaveLength(2);
246
- expect(session.messages[0]).toEqual({ role: 'user', content: 'hello' });
247
- manager.dispose();
248
- });
249
-
250
- it('应该解析字符串形式的 config (JSON)', async () => {
251
- const model = createMockModel([{
252
- session_id: 'test-2',
253
- messages: '[]',
254
- config: '{"provider":"ollama","maxHistory":100}',
255
- created_at: 1000,
256
- updated_at: 2000,
257
- }]);
258
-
259
- const manager = new DatabaseSessionManager(model);
260
- const session = await manager.get('test-2');
261
-
262
- expect(typeof session.config).toBe('object');
263
- expect(session.config).toEqual({ provider: 'ollama', maxHistory: 100 });
264
- manager.dispose();
265
- });
266
-
267
- it('messages 已是数组时不应二次解析', async () => {
268
- const messagesArray = [{ role: 'system', content: 'You are a bot' }];
269
- const model = createMockModel([{
270
- session_id: 'test-3',
271
- messages: messagesArray,
272
- config: { provider: 'openai' },
273
- created_at: 1000,
274
- updated_at: 2000,
275
- }]);
276
-
277
- const manager = new DatabaseSessionManager(model);
278
- const session = await manager.get('test-3');
279
-
280
- expect(session.messages).toBe(messagesArray);
281
- expect(session.config).toEqual({ provider: 'openai' });
282
- manager.dispose();
283
- });
284
-
285
- it('加载后的 messages 应支持 push 操作', async () => {
286
- const model = createMockModel([{
287
- session_id: 'test-4',
288
- messages: '[{"role":"user","content":"hi"}]',
289
- config: '{"provider":"openai"}',
290
- created_at: 1000,
291
- updated_at: 2000,
292
- }]);
293
-
294
- const manager = new DatabaseSessionManager(model);
295
- await manager.addMessage('test-4', { role: 'assistant', content: 'hello!' });
296
-
297
- const session = await manager.get('test-4');
298
- expect(session.messages).toHaveLength(2);
299
- expect(session.messages[1]).toEqual({ role: 'assistant', content: 'hello!' });
300
- manager.dispose();
301
- });
302
-
303
- it('messages 或 config 为 null/undefined 时应使用默认值', async () => {
304
- const model = createMockModel([{
305
- session_id: 'test-5',
306
- messages: null,
307
- config: null,
308
- created_at: 1000,
309
- updated_at: 2000,
310
- }]);
311
-
312
- const manager = new DatabaseSessionManager(model);
313
- const session = await manager.get('test-5');
314
-
315
- expect(Array.isArray(session.messages)).toBe(true);
316
- expect(session.messages).toHaveLength(0);
317
- expect(session.config).toEqual({ provider: 'openai' });
318
- manager.dispose();
319
- });
320
- });
321
-
322
- describe('新会话创建', () => {
323
- it('数据库无记录时应创建新会话', async () => {
324
- const model = createMockModel([]);
325
- const manager = new DatabaseSessionManager(model);
326
- const session = await manager.get('new-session');
327
-
328
- expect(session.id).toBe('new-session');
329
- expect(Array.isArray(session.messages)).toBe(true);
330
- expect(session.messages).toHaveLength(0);
331
- manager.dispose();
332
- });
333
- });
334
- });
@@ -1,80 +0,0 @@
1
- /**
2
- * ToneDetector 测试
3
- */
4
- import { describe, it, expect } from 'vitest';
5
- import { detectTone, type Tone } from '@zhin.js/agent';
6
-
7
- describe('detectTone', () => {
8
- it('空消息应返回 neutral', () => {
9
- const result = detectTone('');
10
- expect(result.tone).toBe('neutral');
11
- expect(result.hint).toBe('');
12
- });
13
-
14
- it('普通消息应返回 neutral', () => {
15
- const result = detectTone('你好,请帮我查一下天气');
16
- expect(result.tone).toBe('neutral');
17
- });
18
-
19
- describe('frustrated(沮丧/受挫)', () => {
20
- it('应检测负面关键词', () => {
21
- expect(detectTone('这个bug怎么回事').tone).toBe('frustrated');
22
- expect(detectTone('又错了,搞不定').tone).toBe('frustrated');
23
- expect(detectTone('不行啊,还是不对').tone).toBe('frustrated');
24
- });
25
-
26
- it('应检测大量感叹号', () => {
27
- expect(detectTone('到底怎么办!!!').tone).toBe('frustrated');
28
- });
29
-
30
- it('应检测大量大写字母', () => {
31
- const result = detectTone('WHY IS THIS NOT WORKING');
32
- expect(result.tone).toBe('frustrated');
33
- });
34
-
35
- it('hint 应包含共情建议', () => {
36
- const result = detectTone('怎么回事啊');
37
- expect(result.hint).toContain('耐心');
38
- });
39
- });
40
-
41
- describe('urgent(紧急)', () => {
42
- it('应检测紧急关键词', () => {
43
- expect(detectTone('急!请马上帮我处理').tone).toBe('urgent');
44
- expect(detectTone('赶紧看一下这个问题').tone).toBe('urgent');
45
- });
46
-
47
- it('hint 应建议直接给出方案', () => {
48
- const result = detectTone('紧急情况,尽快');
49
- expect(result.hint).toContain('效率');
50
- });
51
- });
52
-
53
- describe('sad(悲伤/低落)', () => {
54
- it('应检测悲伤关键词', () => {
55
- expect(detectTone('好难过啊').tone).toBe('sad');
56
- expect(detectTone('唉,不开心').tone).toBe('sad');
57
- });
58
-
59
- it('应检测省略号', () => {
60
- expect(detectTone('算了吧......好吧...').tone).toBe('sad');
61
- });
62
- });
63
-
64
- describe('excited(兴奋/开心)', () => {
65
- it('应检测正面关键词', () => {
66
- expect(detectTone('太好了!成功了!').tone).toBe('excited');
67
- expect(detectTone('太棒了,完美!').tone).toBe('excited');
68
- });
69
- });
70
-
71
- describe('questioning(提问)', () => {
72
- it('应检测多个问号', () => {
73
- expect(detectTone('这是什么?为什么?').tone).toBe('questioning');
74
- });
75
-
76
- it('短消息带问号应识别为提问', () => {
77
- expect(detectTone('为什么?').tone).toBe('questioning');
78
- });
79
- });
80
- });