@zhin.js/core 1.0.25 → 1.0.27

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 (202) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +84 -342
  3. package/lib/adapter.d.ts +17 -0
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +84 -2
  6. package/lib/adapter.js.map +1 -1
  7. package/lib/ai/agent.d.ts +126 -0
  8. package/lib/ai/agent.d.ts.map +1 -0
  9. package/lib/ai/agent.js +645 -0
  10. package/lib/ai/agent.js.map +1 -0
  11. package/lib/ai/context-manager.d.ts +213 -0
  12. package/lib/ai/context-manager.d.ts.map +1 -0
  13. package/lib/ai/context-manager.js +313 -0
  14. package/lib/ai/context-manager.js.map +1 -0
  15. package/lib/ai/conversation-memory.d.ts +181 -0
  16. package/lib/ai/conversation-memory.d.ts.map +1 -0
  17. package/lib/ai/conversation-memory.js +581 -0
  18. package/lib/ai/conversation-memory.js.map +1 -0
  19. package/lib/ai/follow-up.d.ts +131 -0
  20. package/lib/ai/follow-up.d.ts.map +1 -0
  21. package/lib/ai/follow-up.js +265 -0
  22. package/lib/ai/follow-up.js.map +1 -0
  23. package/lib/ai/index.d.ts +29 -0
  24. package/lib/ai/index.d.ts.map +1 -0
  25. package/lib/ai/index.js +34 -0
  26. package/lib/ai/index.js.map +1 -0
  27. package/lib/ai/init.d.ts +30 -0
  28. package/lib/ai/init.d.ts.map +1 -0
  29. package/lib/ai/init.js +424 -0
  30. package/lib/ai/init.js.map +1 -0
  31. package/lib/ai/output.d.ts +93 -0
  32. package/lib/ai/output.d.ts.map +1 -0
  33. package/lib/ai/output.js +176 -0
  34. package/lib/ai/output.js.map +1 -0
  35. package/lib/ai/providers/anthropic.d.ts +23 -0
  36. package/lib/ai/providers/anthropic.d.ts.map +1 -0
  37. package/lib/ai/providers/anthropic.js +322 -0
  38. package/lib/ai/providers/anthropic.js.map +1 -0
  39. package/lib/ai/providers/base.d.ts +43 -0
  40. package/lib/ai/providers/base.d.ts.map +1 -0
  41. package/lib/ai/providers/base.js +135 -0
  42. package/lib/ai/providers/base.js.map +1 -0
  43. package/lib/ai/providers/index.d.ts +12 -0
  44. package/lib/ai/providers/index.d.ts.map +1 -0
  45. package/lib/ai/providers/index.js +9 -0
  46. package/lib/ai/providers/index.js.map +1 -0
  47. package/lib/ai/providers/ollama.d.ts +25 -0
  48. package/lib/ai/providers/ollama.d.ts.map +1 -0
  49. package/lib/ai/providers/ollama.js +243 -0
  50. package/lib/ai/providers/ollama.js.map +1 -0
  51. package/lib/ai/providers/openai.d.ts +46 -0
  52. package/lib/ai/providers/openai.d.ts.map +1 -0
  53. package/lib/ai/providers/openai.js +132 -0
  54. package/lib/ai/providers/openai.js.map +1 -0
  55. package/lib/ai/rate-limiter.d.ts +38 -0
  56. package/lib/ai/rate-limiter.d.ts.map +1 -0
  57. package/lib/ai/rate-limiter.js +86 -0
  58. package/lib/ai/rate-limiter.js.map +1 -0
  59. package/lib/ai/service.d.ts +81 -0
  60. package/lib/ai/service.d.ts.map +1 -0
  61. package/lib/ai/service.js +274 -0
  62. package/lib/ai/service.js.map +1 -0
  63. package/lib/ai/session.d.ts +186 -0
  64. package/lib/ai/session.d.ts.map +1 -0
  65. package/lib/ai/session.js +443 -0
  66. package/lib/ai/session.js.map +1 -0
  67. package/lib/ai/tone-detector.d.ts +19 -0
  68. package/lib/ai/tone-detector.d.ts.map +1 -0
  69. package/lib/ai/tone-detector.js +72 -0
  70. package/lib/ai/tone-detector.js.map +1 -0
  71. package/lib/ai/tools.d.ts +45 -0
  72. package/lib/ai/tools.d.ts.map +1 -0
  73. package/lib/ai/tools.js +206 -0
  74. package/lib/ai/tools.js.map +1 -0
  75. package/lib/ai/types.d.ts +264 -0
  76. package/lib/ai/types.d.ts.map +1 -0
  77. package/lib/ai/types.js +6 -0
  78. package/lib/ai/types.js.map +1 -0
  79. package/lib/ai/user-profile.d.ts +56 -0
  80. package/lib/ai/user-profile.d.ts.map +1 -0
  81. package/lib/ai/user-profile.js +130 -0
  82. package/lib/ai/user-profile.js.map +1 -0
  83. package/lib/ai/zhin-agent.d.ts +165 -0
  84. package/lib/ai/zhin-agent.d.ts.map +1 -0
  85. package/lib/ai/zhin-agent.js +707 -0
  86. package/lib/ai/zhin-agent.js.map +1 -0
  87. package/lib/built/ai-trigger.d.ts.map +1 -1
  88. package/lib/built/ai-trigger.js +7 -3
  89. package/lib/built/ai-trigger.js.map +1 -1
  90. package/lib/built/command.d.ts +33 -17
  91. package/lib/built/command.d.ts.map +1 -1
  92. package/lib/built/command.js +71 -44
  93. package/lib/built/command.js.map +1 -1
  94. package/lib/built/component.d.ts +42 -15
  95. package/lib/built/component.d.ts.map +1 -1
  96. package/lib/built/component.js +84 -52
  97. package/lib/built/component.js.map +1 -1
  98. package/lib/built/config.d.ts +64 -5
  99. package/lib/built/config.d.ts.map +1 -1
  100. package/lib/built/config.js +129 -12
  101. package/lib/built/config.js.map +1 -1
  102. package/lib/built/cron.d.ts +41 -18
  103. package/lib/built/cron.d.ts.map +1 -1
  104. package/lib/built/cron.js +106 -63
  105. package/lib/built/cron.js.map +1 -1
  106. package/lib/built/database.d.ts +55 -6
  107. package/lib/built/database.d.ts.map +1 -1
  108. package/lib/built/database.js +93 -22
  109. package/lib/built/database.js.map +1 -1
  110. package/lib/built/dispatcher.d.ts +118 -0
  111. package/lib/built/dispatcher.d.ts.map +1 -0
  112. package/lib/built/dispatcher.js +196 -0
  113. package/lib/built/dispatcher.js.map +1 -0
  114. package/lib/built/permission.d.ts +45 -5
  115. package/lib/built/permission.d.ts.map +1 -1
  116. package/lib/built/permission.js +56 -11
  117. package/lib/built/permission.js.map +1 -1
  118. package/lib/built/skill.d.ts +117 -0
  119. package/lib/built/skill.d.ts.map +1 -0
  120. package/lib/built/skill.js +191 -0
  121. package/lib/built/skill.js.map +1 -0
  122. package/lib/built/tool.d.ts +71 -164
  123. package/lib/built/tool.d.ts.map +1 -1
  124. package/lib/built/tool.js +212 -297
  125. package/lib/built/tool.js.map +1 -1
  126. package/lib/feature.d.ts +75 -0
  127. package/lib/feature.d.ts.map +1 -0
  128. package/lib/feature.js +69 -0
  129. package/lib/feature.js.map +1 -0
  130. package/lib/index.d.ts +4 -0
  131. package/lib/index.d.ts.map +1 -1
  132. package/lib/index.js +7 -0
  133. package/lib/index.js.map +1 -1
  134. package/lib/plugin.d.ts +25 -17
  135. package/lib/plugin.d.ts.map +1 -1
  136. package/lib/plugin.js +180 -20
  137. package/lib/plugin.js.map +1 -1
  138. package/lib/types.d.ts +4 -9
  139. package/lib/types.d.ts.map +1 -1
  140. package/package.json +6 -6
  141. package/src/adapter.ts +101 -2
  142. package/src/ai/agent.ts +772 -0
  143. package/src/ai/context-manager.ts +440 -0
  144. package/src/ai/conversation-memory.ts +774 -0
  145. package/src/ai/follow-up.ts +357 -0
  146. package/src/ai/index.ts +128 -0
  147. package/src/ai/init.ts +502 -0
  148. package/src/ai/output.ts +261 -0
  149. package/src/ai/providers/anthropic.ts +375 -0
  150. package/src/ai/providers/base.ts +173 -0
  151. package/src/ai/providers/index.ts +13 -0
  152. package/src/ai/providers/ollama.ts +292 -0
  153. package/src/ai/providers/openai.ts +167 -0
  154. package/src/ai/rate-limiter.ts +129 -0
  155. package/src/ai/service.ts +319 -0
  156. package/src/ai/session.ts +544 -0
  157. package/src/ai/tone-detector.ts +89 -0
  158. package/src/ai/tools.ts +218 -0
  159. package/src/ai/types.ts +296 -0
  160. package/src/ai/user-profile.ts +181 -0
  161. package/src/ai/zhin-agent.ts +845 -0
  162. package/src/built/ai-trigger.ts +6 -3
  163. package/src/built/command.ts +75 -69
  164. package/src/built/component.ts +94 -76
  165. package/src/built/config.ts +288 -128
  166. package/src/built/cron.ts +117 -101
  167. package/src/built/database.ts +128 -33
  168. package/src/built/dispatcher.ts +332 -0
  169. package/src/built/permission.ts +146 -54
  170. package/src/built/skill.ts +280 -0
  171. package/src/built/tool.ts +245 -366
  172. package/src/feature.ts +113 -0
  173. package/src/index.ts +7 -0
  174. package/src/plugin.ts +198 -33
  175. package/src/types.ts +6 -10
  176. package/tests/adapter.test.ts +153 -1
  177. package/tests/ai/agent.test.ts +614 -0
  178. package/tests/ai/ai-trigger.test.ts +368 -0
  179. package/tests/ai/context-manager.test.ts +413 -0
  180. package/tests/ai/conversation-memory.test.ts +128 -0
  181. package/tests/ai/follow-up.test.ts +175 -0
  182. package/tests/ai/integration.test.ts +584 -0
  183. package/tests/ai/output.test.ts +128 -0
  184. package/tests/ai/providers.integration.test.ts +227 -0
  185. package/tests/ai/rate-limiter.test.ts +108 -0
  186. package/tests/ai/session.test.ts +375 -0
  187. package/tests/ai/setup.ts +308 -0
  188. package/tests/ai/tone-detector.test.ts +80 -0
  189. package/tests/ai/tool.test.ts +800 -0
  190. package/tests/ai/tools-builtin.test.ts +346 -0
  191. package/tests/ai/user-profile.test.ts +73 -0
  192. package/tests/ai/zhin-agent.test.ts +177 -0
  193. package/tests/config.test.ts +46 -0
  194. package/tests/cron.test.ts +94 -5
  195. package/tests/dispatcher.test.ts +146 -0
  196. package/tests/feature.test.ts +145 -0
  197. package/tests/features-builtin.test.ts +191 -0
  198. package/tests/plugin.test.ts +88 -14
  199. package/tests/skill-feature.test.ts +179 -0
  200. package/tests/tool-feature.test.ts +254 -0
  201. package/test/minimal-bot.ts +0 -31
  202. package/test/stress-test.ts +0 -123
@@ -0,0 +1,544 @@
1
+ /**
2
+ * @zhin.js/ai - Session Manager
3
+ * 会话管理器,支持上下文记忆和数据库持久化
4
+ *
5
+ * 特性:
6
+ * - 数据库持久化存储(使用 Zhin 的数据库服务)
7
+ * - 内存缓存加速读取
8
+ * - 自动过期清理
9
+ * - 更长的上下文记忆能力
10
+ */
11
+
12
+ import { Logger } from '@zhin.js/logger';
13
+ import type { ChatMessage, SessionConfig, Session } from './types.js';
14
+
15
+ const logger = new Logger(null, 'AI-Session');
16
+
17
+ /**
18
+ * 数据库模型定义
19
+ */
20
+ export const AI_SESSION_MODEL = {
21
+ session_id: { type: 'text' as const, nullable: false },
22
+ messages: { type: 'json' as const, default: [] },
23
+ config: { type: 'json' as const, default: {} },
24
+ created_at: { type: 'integer' as const, default: 0 },
25
+ updated_at: { type: 'integer' as const, default: 0 },
26
+ };
27
+
28
+ /**
29
+ * 数据库会话记录
30
+ */
31
+ interface SessionRecord {
32
+ id?: number;
33
+ session_id: string;
34
+ messages: ChatMessage[];
35
+ config: SessionConfig;
36
+ created_at: number;
37
+ updated_at: number;
38
+ }
39
+
40
+ /**
41
+ * 会话管理器接口
42
+ */
43
+ export interface ISessionManager {
44
+ get(sessionId: string, config?: SessionConfig): Session | Promise<Session>;
45
+ has(sessionId: string): boolean | Promise<boolean>;
46
+ addMessage(sessionId: string, message: ChatMessage): void | Promise<void>;
47
+ getMessages(sessionId: string): ChatMessage[] | Promise<ChatMessage[]>;
48
+ setSystemPrompt(sessionId: string, prompt: string): void | Promise<void>;
49
+ clear(sessionId: string): boolean | Promise<boolean>;
50
+ reset(sessionId: string): void | Promise<void>;
51
+ listSessions(): string[] | Promise<string[]>;
52
+ getStats(): { total: number; active: number; expired: number } | Promise<{ total: number; active: number; expired: number }>;
53
+ cleanup(): number | Promise<number>;
54
+ dispose(): void | Promise<void>;
55
+ }
56
+
57
+ /**
58
+ * 内存会话管理器(回退方案)
59
+ */
60
+ export class MemorySessionManager implements ISessionManager {
61
+ private sessions: Map<string, Session> = new Map();
62
+ private config: Required<Pick<SessionConfig, 'maxHistory' | 'expireMs'>>;
63
+ private cleanupTimer?: ReturnType<typeof setInterval>;
64
+
65
+ constructor(config: { maxHistory?: number; expireMs?: number } = {}) {
66
+ this.config = {
67
+ maxHistory: config.maxHistory ?? 100,
68
+ expireMs: config.expireMs ?? 24 * 60 * 60 * 1000, // 24 小时
69
+ };
70
+
71
+ // 定期清理过期会话
72
+ this.cleanupTimer = setInterval(() => this.cleanup(), 5 * 60 * 1000);
73
+ }
74
+
75
+ get(sessionId: string, config?: SessionConfig): Session {
76
+ let session = this.sessions.get(sessionId);
77
+
78
+ if (!session) {
79
+ session = {
80
+ id: sessionId,
81
+ config: config || { provider: 'openai' },
82
+ messages: [],
83
+ createdAt: Date.now(),
84
+ updatedAt: Date.now(),
85
+ };
86
+ this.sessions.set(sessionId, session);
87
+ } else {
88
+ session.updatedAt = Date.now();
89
+ }
90
+
91
+ return session;
92
+ }
93
+
94
+ has(sessionId: string): boolean {
95
+ return this.sessions.has(sessionId);
96
+ }
97
+
98
+ addMessage(sessionId: string, message: ChatMessage): void {
99
+ const session = this.get(sessionId);
100
+ session.messages.push(message);
101
+ session.updatedAt = Date.now();
102
+ this.trimMessages(session);
103
+ }
104
+
105
+ private trimMessages(session: Session): void {
106
+ const maxHistory = session.config.maxHistory ?? this.config.maxHistory;
107
+ if (session.messages.length > maxHistory) {
108
+ const systemMessages = session.messages.filter(m => m.role === 'system');
109
+ const otherMessages = session.messages.filter(m => m.role !== 'system');
110
+ const keepCount = maxHistory - systemMessages.length;
111
+ session.messages = [...systemMessages, ...otherMessages.slice(-keepCount)];
112
+ }
113
+ }
114
+
115
+ getMessages(sessionId: string): ChatMessage[] {
116
+ return this.sessions.get(sessionId)?.messages || [];
117
+ }
118
+
119
+ setSystemPrompt(sessionId: string, prompt: string): void {
120
+ const session = this.get(sessionId);
121
+ session.messages = session.messages.filter(m => m.role !== 'system');
122
+ session.messages.unshift({ role: 'system', content: prompt });
123
+ session.updatedAt = Date.now();
124
+ }
125
+
126
+ clear(sessionId: string): boolean {
127
+ return this.sessions.delete(sessionId);
128
+ }
129
+
130
+ reset(sessionId: string): void {
131
+ const session = this.sessions.get(sessionId);
132
+ if (session) {
133
+ const systemMessages = session.messages.filter(m => m.role === 'system');
134
+ session.messages = systemMessages;
135
+ session.updatedAt = Date.now();
136
+ }
137
+ }
138
+
139
+ listSessions(): string[] {
140
+ return Array.from(this.sessions.keys());
141
+ }
142
+
143
+ getStats(): { total: number; active: number; expired: number } {
144
+ const now = Date.now();
145
+ let active = 0;
146
+ let expired = 0;
147
+
148
+ for (const session of this.sessions.values()) {
149
+ if (now - session.updatedAt > this.config.expireMs) {
150
+ expired++;
151
+ } else {
152
+ active++;
153
+ }
154
+ }
155
+
156
+ return { total: this.sessions.size, active, expired };
157
+ }
158
+
159
+ cleanup(): number {
160
+ const now = Date.now();
161
+ let cleaned = 0;
162
+
163
+ for (const [id, session] of this.sessions) {
164
+ const expireMs = session.config.expireMs ?? this.config.expireMs;
165
+ if (now - session.updatedAt > expireMs) {
166
+ this.sessions.delete(id);
167
+ cleaned++;
168
+ }
169
+ }
170
+
171
+ return cleaned;
172
+ }
173
+
174
+ dispose(): void {
175
+ if (this.cleanupTimer) {
176
+ clearInterval(this.cleanupTimer);
177
+ this.cleanupTimer = undefined;
178
+ }
179
+ this.sessions.clear();
180
+ }
181
+ }
182
+
183
+ /**
184
+ * 数据库会话管理器
185
+ * 使用 Zhin 的数据库服务进行持久化存储
186
+ */
187
+ export class DatabaseSessionManager implements ISessionManager {
188
+ private cache: Map<string, Session> = new Map();
189
+ private config: Required<Pick<SessionConfig, 'maxHistory' | 'expireMs'>>;
190
+ private cleanupTimer?: ReturnType<typeof setInterval>;
191
+ private saveQueue: Map<string, Session> = new Map();
192
+ private saveTimer?: ReturnType<typeof setTimeout>;
193
+ private model: any; // 数据库模型
194
+
195
+ constructor(
196
+ model: any,
197
+ config: { maxHistory?: number; expireMs?: number } = {}
198
+ ) {
199
+ this.model = model;
200
+ this.config = {
201
+ maxHistory: config.maxHistory ?? 200, // 数据库支持更长的历史
202
+ expireMs: config.expireMs ?? 7 * 24 * 60 * 60 * 1000, // 7 天过期
203
+ };
204
+
205
+ // 定期清理过期会话(每小时)
206
+ this.cleanupTimer = setInterval(() => this.cleanup(), 60 * 60 * 1000);
207
+ }
208
+
209
+ /**
210
+ * 从数据库加载会话
211
+ */
212
+ private async loadSession(sessionId: string): Promise<Session | null> {
213
+ try {
214
+ const records = await this.model.select().where({ session_id: sessionId });
215
+ if (records && records.length > 0) {
216
+ const record = records[0] as SessionRecord;
217
+ // SQLite 中 json 类型存储为 TEXT,读回时需要解析
218
+ const messages = typeof record.messages === 'string'
219
+ ? JSON.parse(record.messages)
220
+ : (record.messages || []);
221
+ const config = typeof record.config === 'string'
222
+ ? JSON.parse(record.config)
223
+ : (record.config || { provider: 'openai' });
224
+ return {
225
+ id: record.session_id,
226
+ config: Array.isArray(config) ? { provider: 'openai' } : config,
227
+ messages: Array.isArray(messages) ? messages : [],
228
+ createdAt: record.created_at,
229
+ updatedAt: record.updated_at,
230
+ };
231
+ }
232
+ } catch (error) {
233
+ logger.debug('加载会话失败:', error);
234
+ }
235
+ return null;
236
+ }
237
+
238
+ /**
239
+ * 保存会话到数据库(防抖)
240
+ */
241
+ private schedulesSave(session: Session): void {
242
+ this.saveQueue.set(session.id, session);
243
+
244
+ if (!this.saveTimer) {
245
+ this.saveTimer = setTimeout(() => this.flushSaveQueue(), 1000);
246
+ }
247
+ }
248
+
249
+ /**
250
+ * 批量保存队列中的会话
251
+ */
252
+ private async flushSaveQueue(): Promise<void> {
253
+ this.saveTimer = undefined;
254
+
255
+ const sessions = Array.from(this.saveQueue.values());
256
+ this.saveQueue.clear();
257
+
258
+ for (const session of sessions) {
259
+ try {
260
+ const existing = await this.model.select().where({ session_id: session.id });
261
+ const record: Partial<SessionRecord> = {
262
+ session_id: session.id,
263
+ messages: session.messages,
264
+ config: session.config,
265
+ updated_at: session.updatedAt,
266
+ };
267
+
268
+ if (existing && existing.length > 0) {
269
+ await this.model.update(record).where({ session_id: session.id });
270
+ } else {
271
+ record.created_at = session.createdAt;
272
+ await this.model.create(record);
273
+ }
274
+ } catch (error) {
275
+ logger.debug(`保存会话 ${session.id} 失败:`, error);
276
+ }
277
+ }
278
+ }
279
+
280
+ async get(sessionId: string, config?: SessionConfig): Promise<Session> {
281
+ // 先检查缓存
282
+ let session = this.cache.get(sessionId);
283
+
284
+ if (!session) {
285
+ // 从数据库加载
286
+ session = await this.loadSession(sessionId) ?? undefined;
287
+
288
+ if (!session) {
289
+ // 创建新会话
290
+ session = {
291
+ id: sessionId,
292
+ config: config || { provider: 'openai' },
293
+ messages: [],
294
+ createdAt: Date.now(),
295
+ updatedAt: Date.now(),
296
+ };
297
+ }
298
+
299
+ this.cache.set(sessionId, session);
300
+ }
301
+
302
+ session.updatedAt = Date.now();
303
+ this.schedulesSave(session);
304
+
305
+ return session;
306
+ }
307
+
308
+ async has(sessionId: string): Promise<boolean> {
309
+ if (this.cache.has(sessionId)) {
310
+ return true;
311
+ }
312
+
313
+ try {
314
+ const records = await this.model.select().where({ session_id: sessionId });
315
+ return records && records.length > 0;
316
+ } catch {
317
+ return false;
318
+ }
319
+ }
320
+
321
+ async addMessage(sessionId: string, message: ChatMessage): Promise<void> {
322
+ const session = await this.get(sessionId);
323
+ session.messages.push(message);
324
+ session.updatedAt = Date.now();
325
+ this.trimMessages(session);
326
+ this.schedulesSave(session);
327
+ }
328
+
329
+ private trimMessages(session: Session): void {
330
+ const maxHistory = session.config.maxHistory ?? this.config.maxHistory;
331
+ if (session.messages.length > maxHistory) {
332
+ const systemMessages = session.messages.filter(m => m.role === 'system');
333
+ const otherMessages = session.messages.filter(m => m.role !== 'system');
334
+ const keepCount = maxHistory - systemMessages.length;
335
+ session.messages = [...systemMessages, ...otherMessages.slice(-keepCount)];
336
+ }
337
+ }
338
+
339
+ async getMessages(sessionId: string): Promise<ChatMessage[]> {
340
+ const session = await this.get(sessionId);
341
+ return session.messages;
342
+ }
343
+
344
+ async setSystemPrompt(sessionId: string, prompt: string): Promise<void> {
345
+ const session = await this.get(sessionId);
346
+ session.messages = session.messages.filter(m => m.role !== 'system');
347
+ session.messages.unshift({ role: 'system', content: prompt });
348
+ session.updatedAt = Date.now();
349
+ this.schedulesSave(session);
350
+ }
351
+
352
+ async clear(sessionId: string): Promise<boolean> {
353
+ this.cache.delete(sessionId);
354
+ this.saveQueue.delete(sessionId);
355
+
356
+ try {
357
+ await this.model.delete({ session_id: sessionId });
358
+ return true;
359
+ } catch (error) {
360
+ logger.debug(`删除会话 ${sessionId} 失败:`, error);
361
+ return false;
362
+ }
363
+ }
364
+
365
+ async reset(sessionId: string): Promise<void> {
366
+ const session = await this.get(sessionId);
367
+ const systemMessages = session.messages.filter(m => m.role === 'system');
368
+ session.messages = systemMessages;
369
+ session.updatedAt = Date.now();
370
+ this.schedulesSave(session);
371
+ }
372
+
373
+ async listSessions(): Promise<string[]> {
374
+ try {
375
+ const records = await this.model.select();
376
+ return (records as SessionRecord[]).map(r => r.session_id);
377
+ } catch (error) {
378
+ logger.debug('列出会话失败:', error);
379
+ return Array.from(this.cache.keys());
380
+ }
381
+ }
382
+
383
+ async getStats(): Promise<{ total: number; active: number; expired: number }> {
384
+ const now = Date.now();
385
+ let total = 0;
386
+ let active = 0;
387
+ let expired = 0;
388
+
389
+ try {
390
+ const records = await this.model.select() as SessionRecord[];
391
+ total = records.length;
392
+
393
+ for (const record of records) {
394
+ const expireMs = record.config?.expireMs ?? this.config.expireMs;
395
+ if (now - record.updated_at > expireMs) {
396
+ expired++;
397
+ } else {
398
+ active++;
399
+ }
400
+ }
401
+ } catch (error) {
402
+ logger.debug('获取统计失败:', error);
403
+ // 回退到缓存统计
404
+ total = this.cache.size;
405
+ for (const session of this.cache.values()) {
406
+ if (now - session.updatedAt > this.config.expireMs) {
407
+ expired++;
408
+ } else {
409
+ active++;
410
+ }
411
+ }
412
+ }
413
+
414
+ return { total, active, expired };
415
+ }
416
+
417
+ async cleanup(): Promise<number> {
418
+ const now = Date.now();
419
+ let cleaned = 0;
420
+
421
+ try {
422
+ const records = await this.model.select() as SessionRecord[];
423
+
424
+ for (const record of records) {
425
+ const expireMs = record.config?.expireMs ?? this.config.expireMs;
426
+ if (now - record.updated_at > expireMs) {
427
+ await this.model.delete({ session_id: record.session_id });
428
+ this.cache.delete(record.session_id);
429
+ cleaned++;
430
+ }
431
+ }
432
+ } catch (error) {
433
+ logger.debug('清理会话失败:', error);
434
+ }
435
+
436
+ return cleaned;
437
+ }
438
+
439
+ async dispose(): Promise<void> {
440
+ // 保存所有待保存的会话
441
+ if (this.saveTimer) {
442
+ clearTimeout(this.saveTimer);
443
+ this.saveTimer = undefined;
444
+ }
445
+ await this.flushSaveQueue();
446
+
447
+ if (this.cleanupTimer) {
448
+ clearInterval(this.cleanupTimer);
449
+ this.cleanupTimer = undefined;
450
+ }
451
+
452
+ this.cache.clear();
453
+ }
454
+ }
455
+
456
+ /**
457
+ * 会话管理器包装器
458
+ * 支持同步和异步接口的统一使用
459
+ */
460
+ export class SessionManager implements ISessionManager {
461
+ private manager: ISessionManager;
462
+
463
+ constructor(manager: ISessionManager) {
464
+ this.manager = manager;
465
+ }
466
+
467
+ /**
468
+ * 生成会话 ID
469
+ */
470
+ static generateId(platform: string, userId: string, channelId?: string): string {
471
+ return channelId
472
+ ? `${platform}:${channelId}:${userId}`
473
+ : `${platform}:${userId}`;
474
+ }
475
+
476
+ get(sessionId: string, config?: SessionConfig): Session | Promise<Session> {
477
+ return this.manager.get(sessionId, config);
478
+ }
479
+
480
+ has(sessionId: string): boolean | Promise<boolean> {
481
+ return this.manager.has(sessionId);
482
+ }
483
+
484
+ addMessage(sessionId: string, message: ChatMessage): void | Promise<void> {
485
+ return this.manager.addMessage(sessionId, message);
486
+ }
487
+
488
+ getMessages(sessionId: string): ChatMessage[] | Promise<ChatMessage[]> {
489
+ return this.manager.getMessages(sessionId);
490
+ }
491
+
492
+ setSystemPrompt(sessionId: string, prompt: string): void | Promise<void> {
493
+ return this.manager.setSystemPrompt(sessionId, prompt);
494
+ }
495
+
496
+ clear(sessionId: string): boolean | Promise<boolean> {
497
+ return this.manager.clear(sessionId);
498
+ }
499
+
500
+ reset(sessionId: string): void | Promise<void> {
501
+ return this.manager.reset(sessionId);
502
+ }
503
+
504
+ listSessions(): string[] | Promise<string[]> {
505
+ return this.manager.listSessions();
506
+ }
507
+
508
+ getStats(): { total: number; active: number; expired: number } | Promise<{ total: number; active: number; expired: number }> {
509
+ return this.manager.getStats();
510
+ }
511
+
512
+ cleanup(): number | Promise<number> {
513
+ return this.manager.cleanup();
514
+ }
515
+
516
+ dispose(): void | Promise<void> {
517
+ return this.manager.dispose();
518
+ }
519
+ }
520
+
521
+ /**
522
+ * 创建内存会话管理器(回退方案)
523
+ */
524
+ export function createMemorySessionManager(config?: { maxHistory?: number; expireMs?: number }): SessionManager {
525
+ return new SessionManager(new MemorySessionManager(config));
526
+ }
527
+
528
+ /**
529
+ * 创建数据库会话管理器
530
+ */
531
+ export function createDatabaseSessionManager(
532
+ model: any,
533
+ config?: { maxHistory?: number; expireMs?: number }
534
+ ): SessionManager {
535
+ return new SessionManager(new DatabaseSessionManager(model, config));
536
+ }
537
+
538
+ /**
539
+ * 创建会话管理器(向后兼容)
540
+ * @deprecated 使用 createMemorySessionManager 或 createDatabaseSessionManager
541
+ */
542
+ export function createSessionManager(config?: { maxHistory?: number; expireMs?: number }): SessionManager {
543
+ return createMemorySessionManager(config);
544
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * ToneDetector — 轻量级情绪/语气检测
3
+ *
4
+ * 通过标点符号、emoji 密度、关键词分析用户语气,
5
+ * 生成一条 hint 注入 system prompt,让 AI 的回复匹配用户情绪。
6
+ *
7
+ * 零 LLM 开销,纯正则/统计分析。
8
+ */
9
+
10
+ export type Tone = 'neutral' | 'frustrated' | 'excited' | 'questioning' | 'sad' | 'urgent';
11
+
12
+ interface ToneResult {
13
+ tone: Tone;
14
+ hint: string;
15
+ }
16
+
17
+ // 常见负面情绪词
18
+ const FRUSTRATED_WORDS = /不行|不对|又错|还是不|怎么回事|搞不定|烦死|崩溃|无语|什么鬼|bug|报错|失败|出问题/;
19
+ const SAD_WORDS = /难过|伤心|失落|遗憾|可惜|唉|哎|不开心|郁闷|心累/;
20
+ const URGENT_WORDS = /急|赶紧|马上|立刻|紧急|尽快|快点|asap|hurry/i;
21
+ const EXCITED_WORDS = /太好了|太棒了|厉害|牛|可以|成功|搞定|完美|赞|nice|amazing|awesome|cool/i;
22
+
23
+ /**
24
+ * 检测用户消息的情绪语气
25
+ */
26
+ export function detectTone(message: string): ToneResult {
27
+ const len = message.length;
28
+ if (len === 0) return { tone: 'neutral', hint: '' };
29
+
30
+ // 统计特征
31
+ const exclamations = (message.match(/!/g) || []).length + (message.match(/!/g) || []).length;
32
+ const questions = (message.match(/\?/g) || []).length + (message.match(/?/g) || []).length;
33
+ const ellipsis = (message.match(/\.\.\./g) || []).length + (message.match(/…/g) || []).length;
34
+ const capsRatio = len > 5 ? (message.match(/[A-Z]/g) || []).length / len : 0;
35
+
36
+ // emoji 检测(常见 Unicode 范围)
37
+ const emojiCount = (message.match(/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu) || []).length;
38
+
39
+ // 关键词检测
40
+ const isFrustrated = FRUSTRATED_WORDS.test(message);
41
+ const isSad = SAD_WORDS.test(message);
42
+ const isUrgent = URGENT_WORDS.test(message);
43
+ const isExcited = EXCITED_WORDS.test(message);
44
+
45
+ // 判定优先级: frustrated > urgent > sad > excited > questioning > neutral
46
+ if (isFrustrated || (exclamations >= 3 && !isExcited)) {
47
+ return {
48
+ tone: 'frustrated',
49
+ hint: '用户似乎有些沮丧或受挫,请用耐心、理解的语气回复,先表示共情再提供帮助。',
50
+ };
51
+ }
52
+
53
+ if (isUrgent) {
54
+ return {
55
+ tone: 'urgent',
56
+ hint: '用户似乎很着急,请直接给出解决方案,减少寒暄,优先效率。',
57
+ };
58
+ }
59
+
60
+ if (isSad || ellipsis >= 2) {
61
+ return {
62
+ tone: 'sad',
63
+ hint: '用户的语气似乎有些低落,请用温暖、关心的语气回复。',
64
+ };
65
+ }
66
+
67
+ if (isExcited || (emojiCount >= 2 && exclamations >= 1)) {
68
+ return {
69
+ tone: 'excited',
70
+ hint: '用户的心情不错,可以用更活泼、热情的语气回复。',
71
+ };
72
+ }
73
+
74
+ if (questions >= 2 || (questions >= 1 && len < 20)) {
75
+ return {
76
+ tone: 'questioning',
77
+ hint: '', // 提问是正常的,不需要特殊 hint
78
+ };
79
+ }
80
+
81
+ if (capsRatio > 0.5 && len > 10) {
82
+ return {
83
+ tone: 'frustrated',
84
+ hint: '用户使用了大量大写字母,可能在表达强烈情绪,请注意语气。',
85
+ };
86
+ }
87
+
88
+ return { tone: 'neutral', hint: '' };
89
+ }