@zhin.js/core 1.0.37 → 1.0.39

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 (204) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +57 -3
  3. package/lib/adapter.d.ts +11 -0
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +61 -0
  6. package/lib/adapter.js.map +1 -1
  7. package/lib/ai/index.d.ts +3 -39
  8. package/lib/ai/index.d.ts.map +1 -1
  9. package/lib/ai/index.js +2 -44
  10. package/lib/ai/index.js.map +1 -1
  11. package/lib/ai/providers/anthropic.d.ts.map +1 -1
  12. package/lib/ai/providers/anthropic.js +2 -0
  13. package/lib/ai/providers/anthropic.js.map +1 -1
  14. package/lib/ai/providers/openai.d.ts.map +1 -1
  15. package/lib/ai/providers/openai.js +8 -0
  16. package/lib/ai/providers/openai.js.map +1 -1
  17. package/lib/ai/types.d.ts +5 -3
  18. package/lib/ai/types.d.ts.map +1 -1
  19. package/lib/built/ai-trigger.js.map +1 -1
  20. package/lib/built/common-adapter-tools.d.ts +55 -0
  21. package/lib/built/common-adapter-tools.d.ts.map +1 -0
  22. package/lib/built/common-adapter-tools.js +158 -0
  23. package/lib/built/common-adapter-tools.js.map +1 -0
  24. package/lib/built/dispatcher.d.ts.map +1 -1
  25. package/lib/built/dispatcher.js +50 -46
  26. package/lib/built/dispatcher.js.map +1 -1
  27. package/lib/built/skill.d.ts.map +1 -1
  28. package/lib/built/skill.js +0 -1
  29. package/lib/built/skill.js.map +1 -1
  30. package/lib/built/tool.d.ts +3 -3
  31. package/lib/built/tool.d.ts.map +1 -1
  32. package/lib/built/tool.js.map +1 -1
  33. package/lib/feature.d.ts +16 -1
  34. package/lib/feature.d.ts.map +1 -1
  35. package/lib/feature.js +41 -2
  36. package/lib/feature.js.map +1 -1
  37. package/lib/index.d.ts +1 -0
  38. package/lib/index.d.ts.map +1 -1
  39. package/lib/index.js +2 -0
  40. package/lib/index.js.map +1 -1
  41. package/lib/plugin.d.ts +38 -1
  42. package/lib/plugin.d.ts.map +1 -1
  43. package/lib/plugin.js +73 -22
  44. package/lib/plugin.js.map +1 -1
  45. package/lib/scheduler/scheduler.js +1 -1
  46. package/lib/scheduler/scheduler.js.map +1 -1
  47. package/lib/types.d.ts +43 -28
  48. package/lib/types.d.ts.map +1 -1
  49. package/lib/utils.d.ts +12 -3
  50. package/lib/utils.d.ts.map +1 -1
  51. package/lib/utils.js +64 -54
  52. package/lib/utils.js.map +1 -1
  53. package/package.json +5 -5
  54. package/src/adapter.ts +85 -5
  55. package/src/ai/index.ts +8 -186
  56. package/src/ai/providers/anthropic.ts +1 -0
  57. package/src/ai/providers/openai.ts +5 -1
  58. package/src/ai/types.ts +6 -4
  59. package/src/built/ai-trigger.ts +2 -2
  60. package/src/built/common-adapter-tools.ts +207 -0
  61. package/src/built/dispatcher.ts +51 -52
  62. package/src/built/skill.ts +3 -4
  63. package/src/built/tool.ts +3 -3
  64. package/src/feature.ts +45 -2
  65. package/src/index.ts +2 -0
  66. package/src/plugin.ts +92 -31
  67. package/src/scheduler/scheduler.ts +1 -1
  68. package/src/types.ts +39 -28
  69. package/src/utils.ts +63 -52
  70. package/tests/ai/setup.ts +2 -2
  71. package/tests/utils.test.ts +1 -3
  72. package/lib/ai/agent.d.ts +0 -130
  73. package/lib/ai/agent.d.ts.map +0 -1
  74. package/lib/ai/agent.js +0 -702
  75. package/lib/ai/agent.js.map +0 -1
  76. package/lib/ai/bootstrap.d.ts +0 -91
  77. package/lib/ai/bootstrap.d.ts.map +0 -1
  78. package/lib/ai/bootstrap.js +0 -243
  79. package/lib/ai/bootstrap.js.map +0 -1
  80. package/lib/ai/builtin-tools.d.ts +0 -59
  81. package/lib/ai/builtin-tools.d.ts.map +0 -1
  82. package/lib/ai/builtin-tools.js +0 -777
  83. package/lib/ai/builtin-tools.js.map +0 -1
  84. package/lib/ai/compaction.d.ts +0 -132
  85. package/lib/ai/compaction.d.ts.map +0 -1
  86. package/lib/ai/compaction.js +0 -370
  87. package/lib/ai/compaction.js.map +0 -1
  88. package/lib/ai/context-manager.d.ts +0 -213
  89. package/lib/ai/context-manager.d.ts.map +0 -1
  90. package/lib/ai/context-manager.js +0 -313
  91. package/lib/ai/context-manager.js.map +0 -1
  92. package/lib/ai/conversation-memory.d.ts +0 -181
  93. package/lib/ai/conversation-memory.d.ts.map +0 -1
  94. package/lib/ai/conversation-memory.js +0 -581
  95. package/lib/ai/conversation-memory.js.map +0 -1
  96. package/lib/ai/cron-engine.d.ts +0 -92
  97. package/lib/ai/cron-engine.d.ts.map +0 -1
  98. package/lib/ai/cron-engine.js +0 -278
  99. package/lib/ai/cron-engine.js.map +0 -1
  100. package/lib/ai/follow-up.d.ts +0 -131
  101. package/lib/ai/follow-up.d.ts.map +0 -1
  102. package/lib/ai/follow-up.js +0 -265
  103. package/lib/ai/follow-up.js.map +0 -1
  104. package/lib/ai/hooks.d.ts +0 -143
  105. package/lib/ai/hooks.d.ts.map +0 -1
  106. package/lib/ai/hooks.js +0 -108
  107. package/lib/ai/hooks.js.map +0 -1
  108. package/lib/ai/init.d.ts +0 -30
  109. package/lib/ai/init.d.ts.map +0 -1
  110. package/lib/ai/init.js +0 -686
  111. package/lib/ai/init.js.map +0 -1
  112. package/lib/ai/output.d.ts +0 -93
  113. package/lib/ai/output.d.ts.map +0 -1
  114. package/lib/ai/output.js +0 -176
  115. package/lib/ai/output.js.map +0 -1
  116. package/lib/ai/rate-limiter.d.ts +0 -38
  117. package/lib/ai/rate-limiter.d.ts.map +0 -1
  118. package/lib/ai/rate-limiter.js +0 -86
  119. package/lib/ai/rate-limiter.js.map +0 -1
  120. package/lib/ai/service.d.ts +0 -88
  121. package/lib/ai/service.d.ts.map +0 -1
  122. package/lib/ai/service.js +0 -285
  123. package/lib/ai/service.js.map +0 -1
  124. package/lib/ai/session.d.ts +0 -186
  125. package/lib/ai/session.d.ts.map +0 -1
  126. package/lib/ai/session.js +0 -443
  127. package/lib/ai/session.js.map +0 -1
  128. package/lib/ai/subagent.d.ts +0 -50
  129. package/lib/ai/subagent.d.ts.map +0 -1
  130. package/lib/ai/subagent.js +0 -144
  131. package/lib/ai/subagent.js.map +0 -1
  132. package/lib/ai/tone-detector.d.ts +0 -19
  133. package/lib/ai/tone-detector.d.ts.map +0 -1
  134. package/lib/ai/tone-detector.js +0 -72
  135. package/lib/ai/tone-detector.js.map +0 -1
  136. package/lib/ai/tools.d.ts +0 -45
  137. package/lib/ai/tools.d.ts.map +0 -1
  138. package/lib/ai/tools.js +0 -206
  139. package/lib/ai/tools.js.map +0 -1
  140. package/lib/ai/user-profile.d.ts +0 -56
  141. package/lib/ai/user-profile.d.ts.map +0 -1
  142. package/lib/ai/user-profile.js +0 -130
  143. package/lib/ai/user-profile.js.map +0 -1
  144. package/lib/ai/zhin-agent/builtin-tools.d.ts +0 -17
  145. package/lib/ai/zhin-agent/builtin-tools.d.ts.map +0 -1
  146. package/lib/ai/zhin-agent/builtin-tools.js +0 -220
  147. package/lib/ai/zhin-agent/builtin-tools.js.map +0 -1
  148. package/lib/ai/zhin-agent/config.d.ts +0 -54
  149. package/lib/ai/zhin-agent/config.d.ts.map +0 -1
  150. package/lib/ai/zhin-agent/config.js +0 -76
  151. package/lib/ai/zhin-agent/config.js.map +0 -1
  152. package/lib/ai/zhin-agent/exec-policy.d.ts +0 -20
  153. package/lib/ai/zhin-agent/exec-policy.d.ts.map +0 -1
  154. package/lib/ai/zhin-agent/exec-policy.js +0 -71
  155. package/lib/ai/zhin-agent/exec-policy.js.map +0 -1
  156. package/lib/ai/zhin-agent/index.d.ts +0 -70
  157. package/lib/ai/zhin-agent/index.d.ts.map +0 -1
  158. package/lib/ai/zhin-agent/index.js +0 -404
  159. package/lib/ai/zhin-agent/index.js.map +0 -1
  160. package/lib/ai/zhin-agent/prompt.d.ts +0 -21
  161. package/lib/ai/zhin-agent/prompt.d.ts.map +0 -1
  162. package/lib/ai/zhin-agent/prompt.js +0 -111
  163. package/lib/ai/zhin-agent/prompt.js.map +0 -1
  164. package/lib/ai/zhin-agent/tool-collector.d.ts +0 -22
  165. package/lib/ai/zhin-agent/tool-collector.d.ts.map +0 -1
  166. package/lib/ai/zhin-agent/tool-collector.js +0 -218
  167. package/lib/ai/zhin-agent/tool-collector.js.map +0 -1
  168. package/src/ai/agent.ts +0 -831
  169. package/src/ai/bootstrap.ts +0 -309
  170. package/src/ai/builtin-tools.ts +0 -849
  171. package/src/ai/compaction.ts +0 -529
  172. package/src/ai/context-manager.ts +0 -440
  173. package/src/ai/conversation-memory.ts +0 -774
  174. package/src/ai/cron-engine.ts +0 -337
  175. package/src/ai/follow-up.ts +0 -357
  176. package/src/ai/hooks.ts +0 -223
  177. package/src/ai/init.ts +0 -762
  178. package/src/ai/output.ts +0 -261
  179. package/src/ai/rate-limiter.ts +0 -129
  180. package/src/ai/service.ts +0 -331
  181. package/src/ai/session.ts +0 -544
  182. package/src/ai/subagent.ts +0 -209
  183. package/src/ai/tone-detector.ts +0 -89
  184. package/src/ai/tools.ts +0 -218
  185. package/src/ai/user-profile.ts +0 -181
  186. package/src/ai/zhin-agent/builtin-tools.ts +0 -247
  187. package/src/ai/zhin-agent/config.ts +0 -113
  188. package/src/ai/zhin-agent/exec-policy.ts +0 -78
  189. package/src/ai/zhin-agent/index.ts +0 -512
  190. package/src/ai/zhin-agent/prompt.ts +0 -131
  191. package/src/ai/zhin-agent/tool-collector.ts +0 -243
  192. package/tests/ai/agent.test.ts +0 -614
  193. package/tests/ai/context-manager.test.ts +0 -413
  194. package/tests/ai/conversation-memory.test.ts +0 -128
  195. package/tests/ai/follow-up.test.ts +0 -175
  196. package/tests/ai/integration.test.ts +0 -584
  197. package/tests/ai/output.test.ts +0 -128
  198. package/tests/ai/rate-limiter.test.ts +0 -108
  199. package/tests/ai/session.test.ts +0 -375
  200. package/tests/ai/subagent.test.ts +0 -270
  201. package/tests/ai/tone-detector.test.ts +0 -80
  202. package/tests/ai/tools-builtin.test.ts +0 -346
  203. package/tests/ai/user-profile.test.ts +0 -73
  204. package/tests/ai/zhin-agent.test.ts +0 -177
@@ -1,128 +0,0 @@
1
- /**
2
- * AI Output 模块测试
3
- */
4
- import { describe, it, expect } from 'vitest';
5
- import { parseOutput, renderToPlainText, renderToSatori } from '../../src/ai/output.js';
6
-
7
- describe('parseOutput', () => {
8
- it('纯文本应返回单个 TextElement', () => {
9
- const result = parseOutput('Hello, world!');
10
- expect(result).toHaveLength(1);
11
- expect(result[0]).toMatchObject({ type: 'text', content: 'Hello, world!', format: 'markdown' });
12
- });
13
-
14
- it('空字符串应返回空白 TextElement', () => {
15
- const result = parseOutput('');
16
- expect(result).toHaveLength(1);
17
- expect(result[0]).toMatchObject({ type: 'text', content: '', format: 'plain' });
18
- });
19
-
20
- it('应解析图片 ![alt](url)', () => {
21
- const result = parseOutput('看看这张图 ![cat](https://example.com/cat.png)');
22
- expect(result).toHaveLength(2);
23
- expect(result[0]).toMatchObject({ type: 'text', content: '看看这张图' });
24
- expect(result[1]).toMatchObject({ type: 'image', url: 'https://example.com/cat.png', alt: 'cat' });
25
- });
26
-
27
- it('应解析音频 [audio](url)', () => {
28
- const result = parseOutput('[audio](https://example.com/song.mp3)');
29
- expect(result).toHaveLength(1);
30
- expect(result[0]).toMatchObject({ type: 'audio', url: 'https://example.com/song.mp3' });
31
- });
32
-
33
- it('应解析视频 [video](url)', () => {
34
- const result = parseOutput('这是视频 [video](https://example.com/video.mp4)');
35
- expect(result).toHaveLength(2);
36
- expect(result[1]).toMatchObject({ type: 'video', url: 'https://example.com/video.mp4' });
37
- });
38
-
39
- it('应解析文件 [file:name](url)', () => {
40
- const result = parseOutput('[file:report.pdf](https://example.com/report.pdf)');
41
- expect(result).toHaveLength(1);
42
- expect(result[0]).toMatchObject({ type: 'file', url: 'https://example.com/report.pdf', name: 'report.pdf' });
43
- });
44
-
45
- it('应解析 card 代码块', () => {
46
- const raw = '```card\n{"title":"天气","description":"晴天"}\n```';
47
- const result = parseOutput(raw);
48
- expect(result.some(el => el.type === 'card')).toBe(true);
49
- const card = result.find(el => el.type === 'card')!;
50
- expect(card).toMatchObject({ type: 'card', title: '天气', description: '晴天' });
51
- });
52
-
53
- it('无效 card JSON 应降级为文本', () => {
54
- const raw = '```card\n{invalid json}\n```';
55
- const result = parseOutput(raw);
56
- expect(result.some(el => el.type === 'text')).toBe(true);
57
- });
58
-
59
- it('应处理混合内容', () => {
60
- const raw = '看看这个 ![](https://img.png) 更多文字';
61
- const result = parseOutput(raw);
62
- expect(result.length).toBeGreaterThanOrEqual(2);
63
- expect(result.some(el => el.type === 'image')).toBe(true);
64
- });
65
- });
66
-
67
- describe('renderToPlainText', () => {
68
- it('应渲染文本元素', () => {
69
- expect(renderToPlainText([{ type: 'text', content: 'Hello' }])).toBe('Hello');
70
- });
71
-
72
- it('应渲染图片为 [图片: alt]', () => {
73
- const result = renderToPlainText([{ type: 'image', url: 'url', alt: '猫' }]);
74
- expect(result).toBe('[图片: 猫]');
75
- });
76
-
77
- it('应渲染音频的 fallbackText', () => {
78
- const result = renderToPlainText([{ type: 'audio', url: 'url', fallbackText: '一段音乐' }]);
79
- expect(result).toBe('一段音乐');
80
- });
81
-
82
- it('应渲染卡片标题和字段', () => {
83
- const result = renderToPlainText([{
84
- type: 'card',
85
- title: '天气',
86
- description: '晴天',
87
- fields: [{ label: '温度', value: '25°C' }],
88
- }]);
89
- expect(result).toContain('天气');
90
- expect(result).toContain('晴天');
91
- expect(result).toContain('温度: 25°C');
92
- });
93
-
94
- it('应渲染文件为 [文件: name]', () => {
95
- const result = renderToPlainText([{ type: 'file', url: 'url', name: 'report.pdf' }]);
96
- expect(result).toContain('[文件: report.pdf]');
97
- });
98
- });
99
-
100
- describe('renderToSatori', () => {
101
- it('应将文本包裹在 <p> 标签中', () => {
102
- const result = renderToSatori([{ type: 'text', content: 'Hello' }]);
103
- expect(result).toBe('<p>Hello</p>');
104
- });
105
-
106
- it('应生成 <img> 标签', () => {
107
- const result = renderToSatori([{ type: 'image', url: 'https://img.png', alt: '猫' }]);
108
- expect(result).toContain('<img');
109
- expect(result).toContain('alt="猫"');
110
- });
111
-
112
- it('应转义 HTML 特殊字符', () => {
113
- const result = renderToSatori([{ type: 'text', content: '<script>alert("xss")</script>' }]);
114
- expect(result).not.toContain('<script>');
115
- expect(result).toContain('&lt;script&gt;');
116
- });
117
-
118
- it('应渲染卡片结构', () => {
119
- const result = renderToSatori([{
120
- type: 'card',
121
- title: '标题',
122
- description: '描述',
123
- }]);
124
- expect(result).toContain('<div class="card">');
125
- expect(result).toContain('<h3>标题</h3>');
126
- expect(result).toContain('<p>描述</p>');
127
- });
128
- });
@@ -1,108 +0,0 @@
1
- /**
2
- * RateLimiter 测试
3
- */
4
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
5
- import { RateLimiter } from '../../src/ai/rate-limiter.js';
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,375 +0,0 @@
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
- DatabaseSessionManager,
28
- SessionManager,
29
- createMemorySessionManager
30
- } from '../../src/ai/session.js';
31
-
32
- describe('MemorySessionManager', () => {
33
- let manager: MemorySessionManager;
34
-
35
- beforeEach(() => {
36
- vi.useFakeTimers();
37
- manager = new MemorySessionManager({
38
- maxHistory: 10,
39
- expireMs: 60000, // 1 分钟
40
- });
41
- });
42
-
43
- afterEach(() => {
44
- manager.dispose();
45
- vi.useRealTimers();
46
- });
47
-
48
- describe('会话创建', () => {
49
- it('应该创建新会话', () => {
50
- const session = manager.get('user-1');
51
-
52
- expect(session).toBeDefined();
53
- expect(session.id).toBe('user-1');
54
- expect(session.messages).toEqual([]);
55
- });
56
-
57
- it('应该返回已存在的会话', () => {
58
- const session1 = manager.get('user-1');
59
- manager.addMessage('user-1', { role: 'user', content: 'test' });
60
-
61
- const session2 = manager.get('user-1');
62
-
63
- expect(session2.messages).toHaveLength(1);
64
- });
65
-
66
- it('不同用户应该有不同的会话', () => {
67
- const session1 = manager.get('user-1');
68
- const session2 = manager.get('user-2');
69
-
70
- expect(session1.id).not.toBe(session2.id);
71
- });
72
- });
73
-
74
- describe('消息历史', () => {
75
- it('应该添加消息到历史', () => {
76
- manager.addMessage('user-1', { role: 'user', content: '你好' });
77
- manager.addMessage('user-1', { role: 'assistant', content: '你好!有什么可以帮你的?' });
78
-
79
- const messages = manager.getMessages('user-1');
80
- expect(messages).toHaveLength(2);
81
- expect(messages[0].role).toBe('user');
82
- expect(messages[1].role).toBe('assistant');
83
- });
84
-
85
- it('应该限制历史记录数量', () => {
86
- // 添加超过限制的消息
87
- for (let i = 0; i < 15; i++) {
88
- manager.addMessage('user-1', { role: 'user', content: `消息 ${i}` });
89
- }
90
-
91
- const messages = manager.getMessages('user-1');
92
- expect(messages.length).toBeLessThanOrEqual(10);
93
- });
94
-
95
- it('系统消息应该保留', () => {
96
- manager.setSystemPrompt('user-1', '你是一个助手');
97
-
98
- for (let i = 0; i < 15; i++) {
99
- manager.addMessage('user-1', { role: 'user', content: `消息 ${i}` });
100
- }
101
-
102
- const messages = manager.getMessages('user-1');
103
- const systemMessages = messages.filter(m => m.role === 'system');
104
- expect(systemMessages.length).toBe(1);
105
- });
106
- });
107
-
108
- describe('系统提示', () => {
109
- it('应该设置系统提示', () => {
110
- manager.setSystemPrompt('user-1', '你是一个助手');
111
-
112
- const messages = manager.getMessages('user-1');
113
- expect(messages[0].role).toBe('system');
114
- expect(messages[0].content).toBe('你是一个助手');
115
- });
116
-
117
- it('应该替换旧的系统提示', () => {
118
- manager.setSystemPrompt('user-1', '旧提示');
119
- manager.setSystemPrompt('user-1', '新提示');
120
-
121
- const messages = manager.getMessages('user-1');
122
- const systemMessages = messages.filter(m => m.role === 'system');
123
- expect(systemMessages.length).toBe(1);
124
- expect(systemMessages[0].content).toBe('新提示');
125
- });
126
- });
127
-
128
- describe('会话清理', () => {
129
- it('应该清除指定会话', () => {
130
- manager.get('user-1');
131
- manager.addMessage('user-1', { role: 'user', content: 'test' });
132
-
133
- expect(manager.clear('user-1')).toBe(true);
134
- expect(manager.has('user-1')).toBe(false);
135
- });
136
-
137
- it('清除不存在的会话应返回 false', () => {
138
- expect(manager.clear('nonexistent')).toBe(false);
139
- });
140
-
141
- it('reset 应该保留系统消息', () => {
142
- manager.setSystemPrompt('user-1', '系统提示');
143
- manager.addMessage('user-1', { role: 'user', content: 'test' });
144
-
145
- manager.reset('user-1');
146
-
147
- const messages = manager.getMessages('user-1');
148
- expect(messages.length).toBe(1);
149
- expect(messages[0].role).toBe('system');
150
- });
151
- });
152
-
153
- describe('会话超时', () => {
154
- it('应该在超时后清理会话', () => {
155
- manager.get('user-1');
156
-
157
- expect(manager.has('user-1')).toBe(true);
158
-
159
- // 前进时间超过超时时间
160
- vi.advanceTimersByTime(61000);
161
-
162
- // 触发清理
163
- manager.cleanup();
164
-
165
- expect(manager.has('user-1')).toBe(false);
166
- });
167
-
168
- it('活动会话不应该被清理', () => {
169
- manager.get('user-1');
170
-
171
- // 前进一半时间
172
- vi.advanceTimersByTime(30000);
173
-
174
- // 添加新消息(刷新活动时间)
175
- manager.addMessage('user-1', { role: 'user', content: '新消息' });
176
-
177
- // 再前进一半时间
178
- vi.advanceTimersByTime(30000);
179
-
180
- manager.cleanup();
181
-
182
- // 会话应该还在
183
- expect(manager.has('user-1')).toBe(true);
184
- });
185
- });
186
-
187
- describe('统计信息', () => {
188
- it('应该返回正确的统计信息', () => {
189
- manager.get('user-1');
190
- manager.get('user-2');
191
-
192
- const stats = manager.getStats();
193
- expect(stats.total).toBe(2);
194
- expect(stats.active).toBe(2);
195
- expect(stats.expired).toBe(0);
196
- });
197
-
198
- it('应该返回会话列表', () => {
199
- manager.get('user-1');
200
- manager.get('user-2');
201
- manager.get('user-3');
202
-
203
- const sessions = manager.listSessions();
204
- expect(sessions).toContain('user-1');
205
- expect(sessions).toContain('user-2');
206
- expect(sessions).toContain('user-3');
207
- });
208
- });
209
-
210
- describe('dispose', () => {
211
- it('应该清理所有会话', () => {
212
- manager.get('user-1');
213
- manager.get('user-2');
214
-
215
- manager.dispose();
216
-
217
- expect(manager.has('user-1')).toBe(false);
218
- expect(manager.has('user-2')).toBe(false);
219
- });
220
- });
221
- });
222
-
223
- describe('SessionManager', () => {
224
- describe('generateId', () => {
225
- it('应该生成正确的会话 ID(带频道)', () => {
226
- const id = SessionManager.generateId('qq', 'user123', 'channel456');
227
- expect(id).toBe('qq:channel456:user123');
228
- });
229
-
230
- it('应该生成正确的会话 ID(无频道)', () => {
231
- const id = SessionManager.generateId('telegram', 'user123');
232
- expect(id).toBe('telegram:user123');
233
- });
234
- });
235
- });
236
-
237
- describe('createMemorySessionManager', () => {
238
- it('应该创建 SessionManager 实例', () => {
239
- const manager = createMemorySessionManager({ maxHistory: 50 });
240
- expect(manager).toBeDefined();
241
- expect(manager.get).toBeDefined();
242
- manager.dispose();
243
- });
244
- });
245
-
246
- describe('DatabaseSessionManager', () => {
247
- // Mock 数据库模型
248
- function createMockModel(records: any[] = []) {
249
- return {
250
- select: vi.fn().mockReturnValue({
251
- where: vi.fn().mockResolvedValue(records),
252
- }),
253
- create: vi.fn().mockResolvedValue(undefined),
254
- update: vi.fn().mockReturnValue({
255
- where: vi.fn().mockResolvedValue(undefined),
256
- }),
257
- delete: vi.fn().mockReturnValue({
258
- where: vi.fn().mockResolvedValue(undefined),
259
- }),
260
- };
261
- }
262
-
263
- afterEach(() => {
264
- vi.useRealTimers();
265
- });
266
-
267
- describe('loadSession JSON 解析', () => {
268
- it('应该解析字符串形式的 messages (JSON)', async () => {
269
- const messagesJson = JSON.stringify([
270
- { role: 'user', content: 'hello' },
271
- { role: 'assistant', content: 'hi there' },
272
- ]);
273
- const model = createMockModel([{
274
- session_id: 'test-1',
275
- messages: messagesJson,
276
- config: JSON.stringify({ provider: 'ollama' }),
277
- created_at: 1000,
278
- updated_at: 2000,
279
- }]);
280
-
281
- const manager = new DatabaseSessionManager(model);
282
- const session = await manager.get('test-1');
283
-
284
- expect(Array.isArray(session.messages)).toBe(true);
285
- expect(session.messages).toHaveLength(2);
286
- expect(session.messages[0]).toEqual({ role: 'user', content: 'hello' });
287
- manager.dispose();
288
- });
289
-
290
- it('应该解析字符串形式的 config (JSON)', async () => {
291
- const model = createMockModel([{
292
- session_id: 'test-2',
293
- messages: '[]',
294
- config: '{"provider":"ollama","maxHistory":100}',
295
- created_at: 1000,
296
- updated_at: 2000,
297
- }]);
298
-
299
- const manager = new DatabaseSessionManager(model);
300
- const session = await manager.get('test-2');
301
-
302
- expect(typeof session.config).toBe('object');
303
- expect(session.config).toEqual({ provider: 'ollama', maxHistory: 100 });
304
- manager.dispose();
305
- });
306
-
307
- it('messages 已是数组时不应二次解析', async () => {
308
- const messagesArray = [{ role: 'system', content: 'You are a bot' }];
309
- const model = createMockModel([{
310
- session_id: 'test-3',
311
- messages: messagesArray,
312
- config: { provider: 'openai' },
313
- created_at: 1000,
314
- updated_at: 2000,
315
- }]);
316
-
317
- const manager = new DatabaseSessionManager(model);
318
- const session = await manager.get('test-3');
319
-
320
- expect(session.messages).toBe(messagesArray);
321
- expect(session.config).toEqual({ provider: 'openai' });
322
- manager.dispose();
323
- });
324
-
325
- it('加载后的 messages 应支持 push 操作', async () => {
326
- const model = createMockModel([{
327
- session_id: 'test-4',
328
- messages: '[{"role":"user","content":"hi"}]',
329
- config: '{"provider":"openai"}',
330
- created_at: 1000,
331
- updated_at: 2000,
332
- }]);
333
-
334
- const manager = new DatabaseSessionManager(model);
335
- // addMessage 内部调用 session.messages.push
336
- await manager.addMessage('test-4', { role: 'assistant', content: 'hello!' });
337
-
338
- const session = await manager.get('test-4');
339
- expect(session.messages).toHaveLength(2);
340
- expect(session.messages[1]).toEqual({ role: 'assistant', content: 'hello!' });
341
- manager.dispose();
342
- });
343
-
344
- it('messages 或 config 为 null/undefined 时应使用默认值', async () => {
345
- const model = createMockModel([{
346
- session_id: 'test-5',
347
- messages: null,
348
- config: null,
349
- created_at: 1000,
350
- updated_at: 2000,
351
- }]);
352
-
353
- const manager = new DatabaseSessionManager(model);
354
- const session = await manager.get('test-5');
355
-
356
- expect(Array.isArray(session.messages)).toBe(true);
357
- expect(session.messages).toHaveLength(0);
358
- expect(session.config).toEqual({ provider: 'openai' });
359
- manager.dispose();
360
- });
361
- });
362
-
363
- describe('新会话创建', () => {
364
- it('数据库无记录时应创建新会话', async () => {
365
- const model = createMockModel([]); // 空结果
366
- const manager = new DatabaseSessionManager(model);
367
- const session = await manager.get('new-session');
368
-
369
- expect(session.id).toBe('new-session');
370
- expect(Array.isArray(session.messages)).toBe(true);
371
- expect(session.messages).toHaveLength(0);
372
- manager.dispose();
373
- });
374
- });
375
- });