lightclawbot 1.2.6 → 1.2.7

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 (99) hide show
  1. package/dist/src/gateway.js +50 -6
  2. package/dist/src/group/constants/index.js +20 -0
  3. package/dist/src/group/inbound/index.js +254 -0
  4. package/dist/src/group/index.js +15 -0
  5. package/dist/src/group/orchestrator/execution/agent-runner.js +299 -0
  6. package/dist/src/group/orchestrator/execution/index.js +7 -0
  7. package/dist/src/group/orchestrator/execution/prompt-builder.js +288 -0
  8. package/dist/src/group/orchestrator/execution/soul-resolver.js +38 -0
  9. package/dist/src/group/orchestrator/execution/types.js +7 -0
  10. package/dist/src/group/orchestrator/index.js +14 -0
  11. package/dist/src/group/orchestrator/lifecycle/conversation-state.js +162 -0
  12. package/dist/src/group/orchestrator/lifecycle/index.js +7 -0
  13. package/dist/src/group/orchestrator/lifecycle/ledger-writer.js +96 -0
  14. package/dist/src/group/orchestrator/lifecycle/run-registry.js +174 -0
  15. package/dist/src/group/orchestrator/orchestrator.js +265 -0
  16. package/dist/src/group/orchestrator/planning/index.js +13 -0
  17. package/dist/src/group/orchestrator/planning/plan-validator.js +233 -0
  18. package/dist/src/group/orchestrator/planning/planning-parser.js +207 -0
  19. package/dist/src/group/orchestrator/planning/subtask-executor.js +345 -0
  20. package/dist/src/group/orchestrator/planning/summarizer-runner.js +224 -0
  21. package/dist/src/group/orchestrator/routes/index.js +9 -0
  22. package/dist/src/group/orchestrator/routes/leader-dispatch.js +229 -0
  23. package/dist/src/group/orchestrator/routes/leader-orchestration-route.js +179 -0
  24. package/dist/src/group/orchestrator/routes/leader-planning.js +92 -0
  25. package/dist/src/group/orchestrator/routes/leader-self-answer.js +223 -0
  26. package/dist/src/group/orchestrator/routes/mention-concurrent-route.js +226 -0
  27. package/dist/src/group/orchestrator/routes/route-helpers.js +186 -0
  28. package/dist/src/group/orchestrator/routes/types.js +8 -0
  29. package/dist/src/group/services/group-cleanup-service.js +183 -0
  30. package/dist/src/group/services/group-creation-service.js +122 -0
  31. package/dist/src/group/services/group-deletion-service.js +111 -0
  32. package/dist/src/group/services/group-history-service.js +73 -0
  33. package/dist/src/group/services/group-member-service.js +169 -0
  34. package/dist/src/group/services/group-query-service.js +133 -0
  35. package/dist/src/group/services/group-update-service.js +144 -0
  36. package/dist/src/group/services/index.js +20 -0
  37. package/dist/src/group/storage/concurrency-manager.js +119 -0
  38. package/dist/src/group/storage/group-storage-core.js +227 -0
  39. package/dist/src/group/storage/index.js +12 -0
  40. package/dist/src/group/storage/message-reader.js +213 -0
  41. package/dist/src/group/storage/message-writer.js +229 -0
  42. package/dist/src/group/storage/slice-manager.js +165 -0
  43. package/dist/src/group/types/common.js +5 -0
  44. package/dist/src/group/types/index.js +5 -0
  45. package/dist/src/group/types/message.js +5 -0
  46. package/dist/src/group/types/orchestrator.js +5 -0
  47. package/dist/src/group/types/storage.js +5 -0
  48. package/dist/src/group/utils/id-generator.js +15 -0
  49. package/dist/src/group/utils/index.js +12 -0
  50. package/dist/src/group/utils/mime.js +36 -0
  51. package/dist/src/group/utils/normalize.js +32 -0
  52. package/dist/src/group/utils/run-helpers.js +36 -0
  53. package/dist/src/outbound.js +12 -19
  54. package/dist/src/shared.js +4 -3
  55. package/dist/src/socket/events/agents-request.js +147 -0
  56. package/dist/src/socket/events/chat-request.js +67 -0
  57. package/dist/src/socket/events/file-download.js +121 -0
  58. package/dist/src/socket/events/group-abort.js +59 -0
  59. package/dist/src/socket/events/group-history.js +59 -0
  60. package/dist/src/socket/events/group-member.js +83 -0
  61. package/dist/src/socket/events/group-request.js +91 -0
  62. package/dist/src/socket/events/history-request.js +95 -0
  63. package/dist/src/socket/events/index.js +39 -0
  64. package/dist/src/socket/events/message-private.js +82 -0
  65. package/dist/src/socket/handlers.js +53 -568
  66. package/dist/src/socket/native-socket.js +21 -20
  67. package/dist/src/socket/registry.js +6 -3
  68. package/dist/src/socket/reliable-emitter.js +16 -13
  69. package/dist/src/socket/service/chat-common.js +36 -0
  70. package/dist/src/socket/service/chat-create.js +75 -0
  71. package/dist/src/socket/service/chat-delete.js +94 -0
  72. package/dist/src/socket/service/chat-list.js +82 -0
  73. package/dist/src/socket/service/chat-update.js +83 -0
  74. package/dist/src/socket/service/group-abort.js +104 -0
  75. package/dist/src/socket/service/group-history.js +140 -0
  76. package/dist/src/socket/service/group-member.js +209 -0
  77. package/dist/src/socket/service/group.js +233 -0
  78. package/dist/src/socket/service/history.js +102 -0
  79. package/dist/src/socket/service/index.js +14 -0
  80. package/dist/src/socket/types/index.js +7 -0
  81. package/dist/src/socket/types/request.js +8 -0
  82. package/dist/src/socket/types/service.js +8 -0
  83. package/dist/src/socket/utils/agent-soul.js +95 -0
  84. package/dist/src/socket/utils/index.js +8 -0
  85. package/dist/src/socket/utils/message.js +83 -0
  86. package/dist/src/socket/utils/validate.js +42 -0
  87. package/dist/src/streaming/index.js +1 -0
  88. package/dist/src/streaming/stream-reply-sink.js +270 -14
  89. package/dist/src/streaming/types.js +20 -1
  90. package/dist/src/{download-tool.js → tools/download-tool.js} +41 -35
  91. package/dist/src/tools/group-history-tool.js +172 -0
  92. package/dist/src/{upload-tool.js → tools/upload-tool.js} +2 -2
  93. package/dist/src/tools.js +4 -3
  94. package/dist/src/utils/index.js +1 -0
  95. package/dist/src/utils/logger.js +38 -0
  96. package/openclaw.plugin.json +2 -1
  97. package/package.json +1 -1
  98. package/dist/src/socket/agent-soul.js +0 -41
  99. package/dist/src/socket/chat.js +0 -257
@@ -0,0 +1,227 @@
1
+ /**
2
+ * 群组存储核心服务
3
+ * 作为主要的接口类,整合分片管理器、消息读取器和消息写入器
4
+ * 提供统一的API接口,保持向后兼容性
5
+ */
6
+ import path from 'path';
7
+ import { resolveOpenClawHome } from '../../history/session-store.js';
8
+ import { SliceManager } from './slice-manager.js';
9
+ import { MessageReader } from './message-reader.js';
10
+ import { MessageWriter } from './message-writer.js';
11
+ export class GroupStorageCore {
12
+ /**
13
+ * 存储配置对象
14
+ */
15
+ config;
16
+ /**
17
+ * 分片管理器
18
+ */
19
+ sliceManager;
20
+ /**
21
+ * 消息读取器
22
+ */
23
+ messageReader;
24
+ /**
25
+ * 消息写入器
26
+ */
27
+ messageWriter;
28
+ /**
29
+ * 构造函数
30
+ * 初始化群组存储服务的配置参数和各个组件
31
+ *
32
+ * @param config - 部分配置参数,会与默认配置合并
33
+ */
34
+ constructor(config = {}) {
35
+ this.config = {
36
+ basePath: path.join(resolveOpenClawHome(), 'groups'),
37
+ maxGroupSize: 10,
38
+ maxHistorySize: 5000,
39
+ sliceConfig: {
40
+ maxSliceSize: 1000,
41
+ minSliceSize: 100,
42
+ strategy: 'hybrid'
43
+ },
44
+ ...config
45
+ };
46
+ this.sliceManager = new SliceManager();
47
+ this.messageReader = new MessageReader();
48
+ this.messageWriter = new MessageWriter();
49
+ }
50
+ /**
51
+ * 获取基础存储路径
52
+ * 返回配置的基础存储路径,用于外部模块了解存储位置
53
+ *
54
+ * @returns 基础存储路径字符串
55
+ */
56
+ getBasePath() {
57
+ return this.config.basePath;
58
+ }
59
+ /**
60
+ * 确保群组目录存在
61
+ * 内部方法,用于创建或确认群组目录的存在
62
+ *
63
+ * @param groupId - 群组ID
64
+ * @returns 群组目录的完整路径
65
+ * @throws 当目录创建失败时抛出错误
66
+ */
67
+ async ensureGroupDir(groupId) {
68
+ const { promises: fs } = await import('fs');
69
+ const groupDir = path.join(this.config.basePath, groupId);
70
+ await fs.mkdir(groupDir, { recursive: true });
71
+ return groupDir;
72
+ }
73
+ /**
74
+ * 读取群组元数据
75
+ * 从文件系统读取指定群组的元数据信息
76
+ *
77
+ * @param groupId - 要读取的群组ID
78
+ * @returns 群组元数据对象,如果群组不存在则返回null
79
+ */
80
+ async readGroupMeta(groupId) {
81
+ try {
82
+ const { promises: fs } = await import('fs');
83
+ const groupDir = path.join(this.config.basePath, groupId);
84
+ const metaPath = path.join(groupDir, 'meta.json');
85
+ const content = await fs.readFile(metaPath, 'utf-8');
86
+ return JSON.parse(content);
87
+ }
88
+ catch (error) {
89
+ // 处理文件不存在的特殊情况,返回null而不是抛出错误
90
+ if (error.code === 'ENOENT') {
91
+ return null;
92
+ }
93
+ throw error;
94
+ }
95
+ }
96
+ /**
97
+ * 写入群组元数据(原子操作)
98
+ * 使用原子操作方式写入群组元数据,避免数据损坏
99
+ *
100
+ * @param groupId - 要写入的群组ID
101
+ * @param meta - 群组元数据对象
102
+ * @returns Promise<void>
103
+ */
104
+ async writeGroupMeta(groupId, meta) {
105
+ const groupDir = await this.ensureGroupDir(groupId);
106
+ await this.messageWriter.writeGroupMeta(groupDir, meta);
107
+ }
108
+ /**
109
+ * 追加群账本条目
110
+ * 向群组的聊天记录文件中追加新的条目,支持分片存储
111
+ *
112
+ * @param groupId - 群组ID
113
+ * @param entry - 账本条目(不包含时间戳)
114
+ * @returns Promise<void>
115
+ */
116
+ async appendTranscriptEntry(groupId, entry) {
117
+ const groupDir = await this.ensureGroupDir(groupId);
118
+ const maxSliceSize = this.config.sliceConfig?.maxSliceSize || 1000;
119
+ await this.messageWriter.appendTranscriptEntry(groupDir, entry, maxSliceSize);
120
+ }
121
+ /**
122
+ * 批量追加消息
123
+ * 一次性追加多条消息,优化写入性能
124
+ *
125
+ * @param groupId - 群组ID
126
+ * @param entries - 消息条目数组
127
+ * @returns Promise<void>
128
+ */
129
+ async appendBatchTranscriptEntries(groupId, entries) {
130
+ const groupDir = await this.ensureGroupDir(groupId);
131
+ const maxSliceSize = this.config.sliceConfig?.maxSliceSize || 1000;
132
+ await this.messageWriter.appendBatchTranscriptEntries(groupDir, entries, maxSliceSize);
133
+ }
134
+ /**
135
+ * 读取群历史消息(游标分页)
136
+ * 读取指定群组的聊天历史记录,支持分页和过滤,支持多分片合并查询
137
+ *
138
+ * @param groupId - 群组ID
139
+ * @param options - 查询选项
140
+ * @returns 包含历史条目和分页信息的结果对象
141
+ */
142
+ async readTranscriptEntries(groupId, options = {}) {
143
+ const groupDir = path.join(this.config.basePath, groupId);
144
+ try {
145
+ const { promises: fs } = await import('fs');
146
+ await fs.access(groupDir);
147
+ return await this.messageReader.readTranscriptEntries(groupDir, options);
148
+ }
149
+ catch (error) {
150
+ // 处理目录不存在的特殊情况,返回空结果
151
+ if (error.code === 'ENOENT') {
152
+ return { entries: [], hasMore: false };
153
+ }
154
+ throw error;
155
+ }
156
+ }
157
+ /**
158
+ * 批量读取消息
159
+ * 读取指定时间范围内的所有消息,不分页
160
+ *
161
+ * @param groupId - 群组ID
162
+ * @param startTime - 开始时间戳
163
+ * @param endTime - 结束时间戳
164
+ * @param chatOnly - 是否只返回聊天内容
165
+ * @returns 时间范围内的所有消息
166
+ */
167
+ async readMessagesByTimeRange(groupId, startTime, endTime, chatOnly = false) {
168
+ const groupDir = path.join(this.config.basePath, groupId);
169
+ try {
170
+ const { promises: fs } = await import('fs');
171
+ await fs.access(groupDir);
172
+ return await this.messageReader.readMessagesByTimeRange(groupDir, startTime, endTime, chatOnly);
173
+ }
174
+ catch (error) {
175
+ if (error.code === 'ENOENT') {
176
+ return [];
177
+ }
178
+ throw error;
179
+ }
180
+ }
181
+ /**
182
+ * 获取消息统计信息
183
+ * 返回群组的消息统计信息
184
+ *
185
+ * @param groupId - 群组ID
186
+ * @returns 消息统计信息
187
+ */
188
+ async getMessageStats(groupId) {
189
+ const groupDir = path.join(this.config.basePath, groupId);
190
+ try {
191
+ const { promises: fs } = await import('fs');
192
+ await fs.access(groupDir);
193
+ return await this.messageReader.getMessageStats(groupDir);
194
+ }
195
+ catch (error) {
196
+ if (error.code === 'ENOENT') {
197
+ return { totalMessages: 0, sliceCount: 0 };
198
+ }
199
+ throw error;
200
+ }
201
+ }
202
+ /**
203
+ * 清除消息读取器的文件缓存。
204
+ * 在写入操作后调用,确保后续读取能获取到最新数据。
205
+ */
206
+ clearReaderCache() {
207
+ this.messageReader.clearCache();
208
+ }
209
+ /**
210
+ * 获取存储配置
211
+ * 返回当前的存储配置信息
212
+ *
213
+ * @returns 存储配置对象
214
+ */
215
+ getConfig() {
216
+ return { ...this.config };
217
+ }
218
+ /**
219
+ * 更新存储配置
220
+ * 动态更新存储配置参数
221
+ *
222
+ * @param newConfig - 新的配置参数
223
+ */
224
+ updateConfig(newConfig) {
225
+ this.config = { ...this.config, ...newConfig };
226
+ }
227
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @file 群组存储模块索引文件
3
+ * @description 统一导出所有存储相关的类和接口
4
+ */
5
+ // 导出核心接口类
6
+ export { GroupStorageCore } from './group-storage-core.js';
7
+ // 导出专门的服务类
8
+ export { SliceManager } from './slice-manager.js';
9
+ export { MessageReader } from './message-reader.js';
10
+ export { MessageWriter } from './message-writer.js';
11
+ // 导出并发管理器
12
+ export { ConcurrencyManager } from './concurrency-manager.js';
@@ -0,0 +1,213 @@
1
+ /**
2
+ * 消息读取器
3
+ * 专门负责群组历史消息的读取和查询功能
4
+ * 包括多分片合并查询、分页处理、消息过滤等高级功能
5
+ *
6
+ * 性能优化特性:
7
+ * - 文件内容缓存:减少重复文件读取操作
8
+ * - 批量读取优化:优化多分片查询性能
9
+ * - 智能分片处理:根据查询条件智能选择分片
10
+ */
11
+ import { promises as fs } from 'fs';
12
+ import path from 'path';
13
+ import { SliceManager } from './slice-manager.js';
14
+ export class MessageReader {
15
+ sliceManager;
16
+ /**
17
+ * 文件内容缓存
18
+ * key: 文件路径
19
+ * value: { content: 文件内容, timestamp: 缓存时间 }
20
+ */
21
+ fileCache = new Map();
22
+ /**
23
+ * 缓存有效期(毫秒)
24
+ */
25
+ CACHE_TTL = 30000; // 30秒
26
+ constructor() {
27
+ this.sliceManager = new SliceManager();
28
+ }
29
+ /**
30
+ * 清空缓存
31
+ * 可用于内存管理或测试
32
+ */
33
+ clearCache() {
34
+ this.fileCache.clear();
35
+ }
36
+ /**
37
+ * 读取分片文件内容(带缓存)
38
+ * 读取指定分片文件中的消息记录,支持缓存优化
39
+ *
40
+ * @param groupDir - 群组目录路径
41
+ * @param filename - 分片文件名
42
+ * @param options - 查询选项
43
+ * @returns 分片中的消息条目数组
44
+ */
45
+ async readSliceFile(groupDir, filename, options) {
46
+ const filePath = path.join(groupDir, filename);
47
+ const now = Date.now();
48
+ // 检查缓存
49
+ const cached = this.fileCache.get(filePath);
50
+ if (cached && now - cached.timestamp < this.CACHE_TTL) {
51
+ return this.processCachedContent(cached.content, options);
52
+ }
53
+ try {
54
+ const content = await fs.readFile(filePath, 'utf-8');
55
+ // 更新缓存
56
+ this.fileCache.set(filePath, { content, timestamp: now });
57
+ return this.processContent(content, options);
58
+ }
59
+ catch (error) {
60
+ // 处理文件不存在的特殊情况,返回空数组
61
+ if (error.code === 'ENOENT') {
62
+ return [];
63
+ }
64
+ throw error;
65
+ }
66
+ }
67
+ /**
68
+ * 处理缓存内容
69
+ */
70
+ processCachedContent(content, options) {
71
+ const lines = content.trim().split('\n').filter(line => line);
72
+ let entries = lines.map(line => {
73
+ try {
74
+ return JSON.parse(line);
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }).filter(Boolean);
80
+ // 应用过滤条件
81
+ if (options.chatOnly) {
82
+ entries = entries.filter(entry => entry.role !== 'system');
83
+ }
84
+ // 时间过滤和排序处理
85
+ return entries
86
+ .filter(entry => entry.ts < options.before)
87
+ .sort((a, b) => b.ts - a.ts)
88
+ .slice(0, options.limit);
89
+ }
90
+ /**
91
+ * 处理新读取的内容
92
+ */
93
+ processContent(content, options) {
94
+ return this.processCachedContent(content, options);
95
+ }
96
+ /**
97
+ * 批量读取分片文件(性能优化)
98
+ * 一次性读取多个分片,减少IO操作
99
+ */
100
+ async readMultipleSlices(groupDir, filenames, options) {
101
+ const results = await Promise.all(filenames.map(filename => this.readSliceFile(groupDir, filename, options)));
102
+ return results.flat();
103
+ }
104
+ /**
105
+ * 读取群历史消息(游标分页)
106
+ * 读取指定群组的聊天历史记录,支持分页和过滤,支持多分片合并查询
107
+ *
108
+ * @param groupDir - 群组目录路径
109
+ * @param options - 查询选项
110
+ * @param options.limit - 返回条数限制,默认50
111
+ * @param options.before - 时间戳上限,默认当前时间
112
+ * @param options.chatOnly - 是否只返回聊天内容,默认false
113
+ * @returns 包含历史条目和分页信息的结果对象
114
+ * @throws 当文件读取或解析失败时抛出错误(ENOENT错误除外)
115
+ */
116
+ async readTranscriptEntries(groupDir, options = {}) {
117
+ const { limit = 50, before = Date.now(), chatOnly = false } = options;
118
+ try {
119
+ // 获取相关分片列表
120
+ const relevantSlices = await this.sliceManager.getRelevantSlices(groupDir, before);
121
+ // 如果没有分片,返回空结果
122
+ if (relevantSlices.length === 0) {
123
+ return { entries: [], hasMore: false };
124
+ }
125
+ // 批量读取所有相关分片(性能优化)
126
+ const allEntries = await this.readMultipleSlices(groupDir, relevantSlices.map(slice => slice.filename), { before, limit: Number.MAX_SAFE_INTEGER, chatOnly });
127
+ // 排序和分页处理
128
+ const sortedEntries = allEntries
129
+ .sort((a, b) => b.ts - a.ts)
130
+ .slice(0, limit);
131
+ // 判断是否还有更多数据
132
+ const hasMore = await this.sliceManager.calculateHasMore(groupDir, before, sortedEntries);
133
+ const nextCursor = sortedEntries.length > 0 ? sortedEntries[0].ts : undefined;
134
+ return {
135
+ entries: sortedEntries.reverse(), // 按时间升序返回
136
+ hasMore,
137
+ nextCursor
138
+ };
139
+ }
140
+ catch (error) {
141
+ // 处理文件不存在的特殊情况,返回空结果
142
+ if (error.code === 'ENOENT') {
143
+ return { entries: [], hasMore: false };
144
+ }
145
+ throw error;
146
+ }
147
+ }
148
+ /**
149
+ * 批量读取消息
150
+ * 读取指定时间范围内的所有消息,不分页
151
+ *
152
+ * @param groupDir - 群组目录路径
153
+ * @param startTime - 开始时间戳
154
+ * @param endTime - 结束时间戳
155
+ * @param chatOnly - 是否只返回聊天内容
156
+ * @returns 时间范围内的所有消息
157
+ */
158
+ async readMessagesByTimeRange(groupDir, startTime, endTime, chatOnly = false) {
159
+ try {
160
+ const relevantSlices = await this.sliceManager.getRelevantSlices(groupDir, endTime);
161
+ let allEntries = [];
162
+ for (const slice of relevantSlices) {
163
+ // 如果分片的开始时间晚于结束时间,跳过
164
+ if (slice.startTime > endTime)
165
+ continue;
166
+ const sliceEntries = await this.readSliceFile(groupDir, slice.filename, {
167
+ before: endTime,
168
+ limit: Number.MAX_SAFE_INTEGER,
169
+ chatOnly
170
+ });
171
+ // 过滤开始时间之后的消息
172
+ const filteredEntries = sliceEntries.filter(entry => entry.ts >= startTime);
173
+ allEntries = [...filteredEntries, ...allEntries];
174
+ }
175
+ return allEntries.sort((a, b) => a.ts - b.ts);
176
+ }
177
+ catch (error) {
178
+ if (error.code === 'ENOENT') {
179
+ return [];
180
+ }
181
+ throw error;
182
+ }
183
+ }
184
+ /**
185
+ * 获取消息统计信息
186
+ * 返回群组的消息统计信息
187
+ *
188
+ * @param groupDir - 群组目录路径
189
+ * @returns 消息统计信息
190
+ */
191
+ async getMessageStats(groupDir) {
192
+ try {
193
+ const fs = await import('fs');
194
+ const indexPath = path.join(groupDir, 'index.json');
195
+ if (!fs.existsSync(indexPath)) {
196
+ return { totalMessages: 0, sliceCount: 0 };
197
+ }
198
+ const content = await fs.promises.readFile(indexPath, 'utf-8');
199
+ const index = JSON.parse(content);
200
+ const firstSlice = index.slices[0];
201
+ const lastSlice = index.slices[index.slices.length - 1];
202
+ return {
203
+ totalMessages: index.totalMessages || 0,
204
+ sliceCount: index.slices.length,
205
+ firstMessageTime: firstSlice?.startTime,
206
+ lastMessageTime: lastSlice?.endTime
207
+ };
208
+ }
209
+ catch (error) {
210
+ return { totalMessages: 0, sliceCount: 0 };
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * 消息写入器
3
+ * 专门负责群组消息的写入和存储管理
4
+ * 包括原子操作、分片切换、消息ID分配等核心功能
5
+ */
6
+ import { promises as fs } from 'fs';
7
+ import path from 'path';
8
+ import { SliceManager } from './slice-manager.js';
9
+ import { ConcurrencyManager } from './concurrency-manager.js';
10
+ export class MessageWriter {
11
+ sliceManager;
12
+ concurrencyManager;
13
+ constructor() {
14
+ this.sliceManager = new SliceManager();
15
+ this.concurrencyManager = new ConcurrencyManager();
16
+ }
17
+ /**
18
+ * 追加群账本条目
19
+ * 向群组的聊天记录文件中追加新的条目,支持分片存储
20
+ *
21
+ * @param groupDir - 群组目录路径
22
+ * @param entry - 账本条目(不包含时间戳)
23
+ * @param maxSliceSize - 单个分片最大消息数
24
+ * @returns Promise<void>
25
+ * @throws 当文件追加写入失败时抛出错误
26
+ */
27
+ async appendTranscriptEntry(groupDir, entry, maxSliceSize) {
28
+ // 使用锁确保消息ID分配的原子性
29
+ await this.concurrencyManager.withLock(`message-write-${path.basename(groupDir)}`, async () => {
30
+ // 获取当前活跃分片
31
+ const activeSlice = await this.sliceManager.getActiveSlice(groupDir, maxSliceSize);
32
+ const slicePath = path.join(groupDir, activeSlice.filename);
33
+ // 读取索引获取下一个消息ID
34
+ const index = await this.sliceManager.readSliceIndex(groupDir);
35
+ const messageId = index.nextMessageId || 1;
36
+ // 构建完整的账本条目,添加时间戳和消息ID
37
+ const fullEntry = {
38
+ ts: Date.now(),
39
+ id: messageId,
40
+ ...entry
41
+ };
42
+ // 格式化为JSON Lines格式
43
+ const line = JSON.stringify(fullEntry) + '\n';
44
+ const lineSize = Buffer.byteLength(line, 'utf-8');
45
+ // 使用追加模式写入
46
+ await fs.appendFile(slicePath, line, 'utf-8');
47
+ // 更新分片统计信息
48
+ await this.sliceManager.updateSliceStats(groupDir, activeSlice.filename, messageId, fullEntry.ts, lineSize);
49
+ }, 30000); // 30秒超时
50
+ }
51
+ /**
52
+ * 批量追加消息
53
+ * 一次性追加多条消息,优化写入性能
54
+ *
55
+ * @param groupDir - 群组目录路径
56
+ * @param entries - 消息条目数组
57
+ * @param maxSliceSize - 单个分片最大消息数
58
+ * @returns Promise<void>
59
+ */
60
+ async appendBatchTranscriptEntries(groupDir, entries, maxSliceSize) {
61
+ if (entries.length === 0)
62
+ return;
63
+ // 使用锁确保批量消息ID分配的原子性
64
+ await this.concurrencyManager.withLock(`batch-message-write-${path.basename(groupDir)}`, async () => {
65
+ // 获取当前活跃分片
66
+ const activeSlice = await this.sliceManager.getActiveSlice(groupDir, maxSliceSize);
67
+ const slicePath = path.join(groupDir, activeSlice.filename);
68
+ // 读取索引获取起始消息ID
69
+ const index = await this.sliceManager.readSliceIndex(groupDir);
70
+ let messageId = index.nextMessageId || 1;
71
+ // 构建批量写入内容
72
+ let batchContent = '';
73
+ let totalSize = 0;
74
+ const timestamp = Date.now();
75
+ for (const entry of entries) {
76
+ const fullEntry = {
77
+ ts: timestamp,
78
+ id: messageId,
79
+ ...entry
80
+ };
81
+ const line = JSON.stringify(fullEntry) + '\n';
82
+ batchContent += line;
83
+ totalSize += Buffer.byteLength(line, 'utf-8');
84
+ messageId++;
85
+ }
86
+ // 批量写入
87
+ await fs.appendFile(slicePath, batchContent, 'utf-8');
88
+ // 更新分片统计信息
89
+ await this.sliceManager.updateSliceStats(groupDir, activeSlice.filename, messageId - 1, // 最后一条消息的ID
90
+ timestamp, totalSize);
91
+ }, 30000); // 30秒超时
92
+ }
93
+ /**
94
+ * 写入群组元数据(原子操作)
95
+ * 使用原子操作方式写入群组元数据,避免数据损坏
96
+ *
97
+ * @param groupDir - 群组目录路径
98
+ * @param meta - 群组元数据对象
99
+ * @returns Promise<void>
100
+ */
101
+ async writeGroupMeta(groupDir, meta) {
102
+ const metaPath = path.join(groupDir, 'meta.json');
103
+ const tempPath = `${metaPath}.tmp`;
104
+ // 先写入临时文件
105
+ await fs.writeFile(tempPath, JSON.stringify(meta, null, 2));
106
+ // 原子重命名
107
+ await fs.rename(tempPath, metaPath);
108
+ }
109
+ /**
110
+ * 清理过期分片
111
+ * 根据配置清理过期的分片文件,释放存储空间
112
+ *
113
+ * @param groupDir - 群组目录路径
114
+ * @param maxHistorySize - 最大历史消息数量
115
+ * @returns 被清理的分片数量
116
+ */
117
+ async cleanupExpiredSlices(groupDir, maxHistorySize) {
118
+ const index = await this.sliceManager.readSliceIndex(groupDir);
119
+ if (index.totalMessages <= maxHistorySize) {
120
+ return 0;
121
+ }
122
+ // 计算需要保留的消息数量
123
+ const messagesToKeep = Math.min(maxHistorySize, index.totalMessages);
124
+ let messagesKept = 0;
125
+ let slicesToRemove = [];
126
+ // 从最新的分片开始计算
127
+ const sortedSlices = [...index.slices].sort((a, b) => b.startTime - a.startTime);
128
+ for (const slice of sortedSlices) {
129
+ if (messagesKept + slice.messageCount <= messagesToKeep) {
130
+ messagesKept += slice.messageCount;
131
+ }
132
+ else {
133
+ slicesToRemove.push(slice);
134
+ }
135
+ }
136
+ // 移除过期的分片
137
+ for (const slice of slicesToRemove) {
138
+ const slicePath = path.join(groupDir, slice.filename);
139
+ try {
140
+ await fs.unlink(slicePath);
141
+ }
142
+ catch (error) {
143
+ // 忽略文件不存在的错误
144
+ if (error.code !== 'ENOENT') {
145
+ throw error;
146
+ }
147
+ }
148
+ }
149
+ // 更新索引
150
+ if (slicesToRemove.length > 0) {
151
+ index.slices = index.slices.filter(slice => !slicesToRemove.includes(slice));
152
+ index.totalMessages = messagesKept;
153
+ await this.sliceManager.writeSliceIndex(groupDir, index);
154
+ }
155
+ return slicesToRemove.length;
156
+ }
157
+ /**
158
+ * 压缩分片文件
159
+ * 将多个小分片合并为一个大分片,优化存储效率
160
+ *
161
+ * @param groupDir - 群组目录路径
162
+ * @param minSliceSize - 触发压缩的最小消息数
163
+ * @returns 是否执行了压缩操作
164
+ */
165
+ async compactSlices(groupDir, minSliceSize) {
166
+ const index = await this.sliceManager.readSliceIndex(groupDir);
167
+ // 找出需要压缩的小分片
168
+ const smallSlices = index.slices.filter(slice => slice.messageCount < minSliceSize);
169
+ if (smallSlices.length <= 1) {
170
+ return false; // 只有一个或没有小分片,不需要压缩
171
+ }
172
+ // 按时间顺序排序
173
+ smallSlices.sort((a, b) => a.startTime - b.startTime);
174
+ // 读取所有小分片的消息
175
+ let allMessages = [];
176
+ for (const slice of smallSlices) {
177
+ const slicePath = path.join(groupDir, slice.filename);
178
+ try {
179
+ const content = await fs.readFile(slicePath, 'utf-8');
180
+ const lines = content.trim().split('\n').filter(line => line);
181
+ const messages = lines.map(line => JSON.parse(line));
182
+ allMessages = [...allMessages, ...messages];
183
+ }
184
+ catch (error) {
185
+ // 忽略读取错误
186
+ continue;
187
+ }
188
+ }
189
+ // 按时间排序
190
+ allMessages.sort((a, b) => a.ts - b.ts);
191
+ // 创建新的合并分片
192
+ const timestamp = Date.now();
193
+ const dateStr = new Date(timestamp).toISOString().split('T')[0].replace(/-/g, '');
194
+ const sequence = index.nextSliceSequence || 1;
195
+ const newFilename = `chat-${dateStr}-compacted-${String(sequence).padStart(3, '0')}.jsonl`;
196
+ // 写入合并后的分片
197
+ const newSlicePath = path.join(groupDir, newFilename);
198
+ let fileContent = '';
199
+ for (const message of allMessages) {
200
+ fileContent += JSON.stringify(message) + '\n';
201
+ }
202
+ await fs.writeFile(newSlicePath, fileContent, 'utf-8');
203
+ // 删除旧的小分片
204
+ for (const slice of smallSlices) {
205
+ const slicePath = path.join(groupDir, slice.filename);
206
+ try {
207
+ await fs.unlink(slicePath);
208
+ }
209
+ catch (error) {
210
+ // 忽略删除错误
211
+ }
212
+ }
213
+ // 更新索引
214
+ index.slices = index.slices.filter(slice => !smallSlices.includes(slice));
215
+ const newSliceInfo = {
216
+ filename: newFilename,
217
+ startTime: allMessages[0]?.ts || timestamp,
218
+ endTime: allMessages[allMessages.length - 1]?.ts || timestamp,
219
+ messageCount: allMessages.length,
220
+ fileSize: Buffer.byteLength(fileContent, 'utf-8'),
221
+ minId: allMessages[0]?.id,
222
+ maxId: allMessages[allMessages.length - 1]?.id
223
+ };
224
+ index.slices.push(newSliceInfo);
225
+ index.nextSliceSequence = sequence + 1;
226
+ await this.sliceManager.writeSliceIndex(groupDir, index);
227
+ return true;
228
+ }
229
+ }