opencode-dingtalk 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/__tests__/connection-manager.test.js +107 -0
- package/dist/__tests__/message-queue.test.js +298 -0
- package/dist/config.js +181 -0
- package/dist/connection-manager.js +103 -0
- package/dist/dingtalk/bot.js +2129 -0
- package/dist/dingtalk/dingtalk-api.js +126 -0
- package/dist/dingtalk/dingtalk-client.js +390 -0
- package/dist/dingtalk/types.js +8 -0
- package/dist/events.js +423 -0
- package/dist/index.js +164 -0
- package/dist/index.test.js +6 -0
- package/dist/instance.js +49 -0
- package/dist/lock.js +57 -0
- package/dist/logger.js +43 -0
- package/dist/message-queue.js +604 -0
- package/dist/registry.js +77 -0
- package/dist/standalone.js +110 -0
- package/dist/state.js +550 -0
- package/dist/types/dingtalk-stub.js +3 -0
- package/dist/utils.js +171 -0
- package/package.json +56 -0
package/dist/state.js
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getDataDir } from "./utils.js";
|
|
4
|
+
export const AICardStatus = {
|
|
5
|
+
PROCESSING: "processing",
|
|
6
|
+
INPUTING: "inputing",
|
|
7
|
+
FINISHED: "finished",
|
|
8
|
+
FAILED: "failed",
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Per-instance state. Each BotInstance holds its own InstanceState,
|
|
12
|
+
* ensuring complete isolation between multiple DingTalk bots.
|
|
13
|
+
*/
|
|
14
|
+
export class InstanceState {
|
|
15
|
+
instanceId;
|
|
16
|
+
dwClient = null;
|
|
17
|
+
botConfig = null;
|
|
18
|
+
watchdogTimer = null;
|
|
19
|
+
client = null;
|
|
20
|
+
serverUrl = null;
|
|
21
|
+
activeUsers = new Map();
|
|
22
|
+
accessToken = null;
|
|
23
|
+
accessTokenExpiry = 0;
|
|
24
|
+
activeSessionId = null;
|
|
25
|
+
// 群聊相关状态
|
|
26
|
+
groupSessions = new Map();
|
|
27
|
+
/** 群聊消息历史记录的最大条数 */
|
|
28
|
+
GROUP_HISTORY_MAX = 20;
|
|
29
|
+
pendingResponses = new Map();
|
|
30
|
+
processedMessages = new Set();
|
|
31
|
+
pendingPermissions = new Map();
|
|
32
|
+
/** Users who sent a message that hasn't received an AI reply yet */
|
|
33
|
+
usersWithPendingRequests = new Set();
|
|
34
|
+
pendingQuestions = new Map();
|
|
35
|
+
sessionStatus = null;
|
|
36
|
+
sessionTodos = new Map();
|
|
37
|
+
sessionAgents = new Map();
|
|
38
|
+
aiCardInstances = new Map();
|
|
39
|
+
activeCardsByTarget = new Map();
|
|
40
|
+
/** sessionId -> userId 映射,用于响应时找到正确的用户 */
|
|
41
|
+
sessionToUserMap = new Map();
|
|
42
|
+
/** Message dedup map (moved from bot.ts module-level) */
|
|
43
|
+
processedInbound = new Map();
|
|
44
|
+
/** 连接管理器 */
|
|
45
|
+
connectionManager = null;
|
|
46
|
+
/** 消息队列 */
|
|
47
|
+
messageQueue = null;
|
|
48
|
+
/** 出站队列定时器 */
|
|
49
|
+
outboundQueueTimer = null;
|
|
50
|
+
/** 消息确认记录 */
|
|
51
|
+
messageAcks = new Map();
|
|
52
|
+
constructor(instanceId) {
|
|
53
|
+
this.instanceId = instanceId;
|
|
54
|
+
this.loadUsersFromDisk();
|
|
55
|
+
this.loadGroupSessionsFromDisk();
|
|
56
|
+
this.loadAllSessionMapsFromDisk(); // 加载session映射(单聊+群聊)
|
|
57
|
+
}
|
|
58
|
+
// ============ Persistence ============
|
|
59
|
+
getUsersPersistPath() {
|
|
60
|
+
return path.join(getDataDir(), `active-users-${this.instanceId}.json`);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 获取群聊 session 持久化文件路径
|
|
64
|
+
*/
|
|
65
|
+
getGroupSessionsPersistPath() {
|
|
66
|
+
return path.join(getDataDir(), `group-sessions-${this.instanceId}.json`);
|
|
67
|
+
}
|
|
68
|
+
loadUsersFromDisk() {
|
|
69
|
+
const filePath = this.getUsersPersistPath();
|
|
70
|
+
try {
|
|
71
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
72
|
+
const parsed = JSON.parse(raw);
|
|
73
|
+
if (Array.isArray(parsed.users)) {
|
|
74
|
+
this.activeUsers.clear();
|
|
75
|
+
for (const u of parsed.users) {
|
|
76
|
+
if (!u || typeof u.userId !== "string")
|
|
77
|
+
continue;
|
|
78
|
+
this.activeUsers.set(u.userId, {
|
|
79
|
+
userId: u.userId,
|
|
80
|
+
username: typeof u.username === "string" ? u.username : undefined,
|
|
81
|
+
firstSeenAt: typeof u.firstSeenAt === "number" ? u.firstSeenAt : Date.now(),
|
|
82
|
+
lastActiveAt: typeof u.lastActiveAt === "number" ? u.lastActiveAt : Date.now(),
|
|
83
|
+
sessionId: typeof u.sessionId === "string" ? u.sessionId : null,
|
|
84
|
+
sessionWebhook: typeof u.sessionWebhook === "string" ? u.sessionWebhook : undefined,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 从磁盘加载群聊 session
|
|
95
|
+
*/
|
|
96
|
+
loadGroupSessionsFromDisk() {
|
|
97
|
+
const filePath = this.getGroupSessionsPersistPath();
|
|
98
|
+
try {
|
|
99
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
100
|
+
const parsed = JSON.parse(raw);
|
|
101
|
+
if (Array.isArray(parsed.groupSessions)) {
|
|
102
|
+
for (const gs of parsed.groupSessions) {
|
|
103
|
+
if (!gs || typeof gs.conversationId !== "string")
|
|
104
|
+
continue;
|
|
105
|
+
this.groupSessions.set(gs.conversationId, {
|
|
106
|
+
conversationId: gs.conversationId,
|
|
107
|
+
title: typeof gs.title === "string" ? gs.title : undefined,
|
|
108
|
+
activeSessionId: typeof gs.activeSessionId === "string" ? gs.activeSessionId : null,
|
|
109
|
+
sessionWebhook: undefined,
|
|
110
|
+
createdAt: typeof gs.createdAt === "number" ? gs.createdAt : Date.now(),
|
|
111
|
+
lastActiveAt: typeof gs.lastActiveAt === "number" ? gs.lastActiveAt : Date.now(),
|
|
112
|
+
messageHistory: [],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
saveUsersToDisk() {
|
|
122
|
+
const filePath = this.getUsersPersistPath();
|
|
123
|
+
try {
|
|
124
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const users = Array.from(this.activeUsers.values());
|
|
130
|
+
try {
|
|
131
|
+
fs.writeFileSync(filePath, JSON.stringify({ users }, null, 2) + "\n", "utf8");
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 保存群聊 session 到磁盘
|
|
139
|
+
*/
|
|
140
|
+
saveGroupSessionsToDisk() {
|
|
141
|
+
const filePath = this.getGroupSessionsPersistPath();
|
|
142
|
+
try {
|
|
143
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const groupSessions = Array.from(this.groupSessions.values()).map(gs => ({
|
|
149
|
+
conversationId: gs.conversationId,
|
|
150
|
+
title: gs.title,
|
|
151
|
+
activeSessionId: gs.activeSessionId,
|
|
152
|
+
createdAt: gs.createdAt,
|
|
153
|
+
lastActiveAt: gs.lastActiveAt,
|
|
154
|
+
}));
|
|
155
|
+
try {
|
|
156
|
+
fs.writeFileSync(filePath, JSON.stringify({ groupSessions }, null, 2) + "\n", "utf8");
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// ============ Session Map Persistence ============
|
|
163
|
+
/**
|
|
164
|
+
* 获取session映射持久化文件路径
|
|
165
|
+
*/
|
|
166
|
+
getSessionMapPersistPath() {
|
|
167
|
+
return path.join(getDataDir(), `session-map-${this.instanceId}.json`);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 获取文件锁路径
|
|
171
|
+
*/
|
|
172
|
+
getLockFilePath() {
|
|
173
|
+
return path.join(getDataDir(), `session-map-${this.instanceId}.lock`);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 获取文件锁
|
|
177
|
+
* @param timeoutMs 锁超时时间(毫秒),默认 5000ms
|
|
178
|
+
* @returns 文件描述符,失败返回 null
|
|
179
|
+
*/
|
|
180
|
+
async acquireLock(timeoutMs = 5000) {
|
|
181
|
+
const lockPath = this.getLockFilePath();
|
|
182
|
+
const startTime = Date.now();
|
|
183
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
184
|
+
try {
|
|
185
|
+
// 尝试创建锁文件(独占模式)
|
|
186
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
187
|
+
// 写入进程ID和时间戳
|
|
188
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
189
|
+
return fd;
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
if (err.code === 'EEXIST') {
|
|
193
|
+
// 锁文件已存在,检查是否过期(超过30秒视为过期)
|
|
194
|
+
try {
|
|
195
|
+
const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
196
|
+
if (Date.now() - lockData.timestamp > 30000) {
|
|
197
|
+
// 锁已过期,删除并重试
|
|
198
|
+
fs.unlinkSync(lockPath);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// 锁文件损坏,删除并重试
|
|
204
|
+
try {
|
|
205
|
+
fs.unlinkSync(lockPath);
|
|
206
|
+
}
|
|
207
|
+
catch { }
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
// 等待100ms后重试
|
|
211
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
// 其他错误,返回 null
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return null; // 超时
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 释放文件锁
|
|
223
|
+
* @param fd 文件描述符
|
|
224
|
+
*/
|
|
225
|
+
releaseLock(fd) {
|
|
226
|
+
if (fd === null)
|
|
227
|
+
return;
|
|
228
|
+
try {
|
|
229
|
+
fs.closeSync(fd);
|
|
230
|
+
fs.unlinkSync(this.getLockFilePath());
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// 忽略释放锁时的错误
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* 保存单聊session映射到磁盘
|
|
238
|
+
* 使用全局锁避免并发写入
|
|
239
|
+
*/
|
|
240
|
+
async saveSingleChatSessionMapToDisk() {
|
|
241
|
+
const fd = await this.acquireLock();
|
|
242
|
+
if (fd === null) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const filePath = this.getSessionMapPersistPath();
|
|
247
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
248
|
+
// 读取现有数据(如果存在)
|
|
249
|
+
let existingData = {};
|
|
250
|
+
try {
|
|
251
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
252
|
+
existingData = JSON.parse(raw);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// 文件不存在或解析失败,使用空对象
|
|
256
|
+
}
|
|
257
|
+
// 更新单聊部分
|
|
258
|
+
existingData.singleChatSessions = {
|
|
259
|
+
user_session_map: Array.from(this.sessionToUserMap.entries()),
|
|
260
|
+
lastUpdated: Date.now(),
|
|
261
|
+
};
|
|
262
|
+
// 保留群聊部分(如果存在)
|
|
263
|
+
if (!existingData.groupChatSessions) {
|
|
264
|
+
existingData.groupChatSessions = {
|
|
265
|
+
conversation_session_map: [],
|
|
266
|
+
lastUpdated: Date.now(),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
fs.writeFileSync(filePath, JSON.stringify(existingData, null, 2) + "\n", "utf8");
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
// 忽略保存错误
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
this.releaseLock(fd);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* 保存群聊session映射到磁盘
|
|
280
|
+
* 使用全局锁避免并发写入
|
|
281
|
+
*/
|
|
282
|
+
async saveGroupChatSessionMapToDisk() {
|
|
283
|
+
const fd = await this.acquireLock();
|
|
284
|
+
if (fd === null) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const filePath = this.getSessionMapPersistPath();
|
|
289
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
290
|
+
// 读取现有数据(如果存在)
|
|
291
|
+
let existingData = {};
|
|
292
|
+
try {
|
|
293
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
294
|
+
existingData = JSON.parse(raw);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// 文件不存在或解析失败,使用空对象
|
|
298
|
+
}
|
|
299
|
+
// 更新群聊部分
|
|
300
|
+
existingData.groupChatSessions = {
|
|
301
|
+
conversation_session_map: Array.from(this.groupSessions.entries()).map(([convId, session]) => [convId, session.activeSessionId]),
|
|
302
|
+
lastUpdated: Date.now(),
|
|
303
|
+
};
|
|
304
|
+
// 保留单聊部分(如果存在)
|
|
305
|
+
if (!existingData.singleChatSessions) {
|
|
306
|
+
existingData.singleChatSessions = {
|
|
307
|
+
user_session_map: [],
|
|
308
|
+
lastUpdated: Date.now(),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
fs.writeFileSync(filePath, JSON.stringify(existingData, null, 2) + "\n", "utf8");
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
// 忽略保存错误
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
this.releaseLock(fd);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* 从磁盘加载所有session映射(单聊 + 群聊)
|
|
322
|
+
* 启动时调用,无需加锁(单线程启动)
|
|
323
|
+
*/
|
|
324
|
+
loadAllSessionMapsFromDisk() {
|
|
325
|
+
const filePath = this.getSessionMapPersistPath();
|
|
326
|
+
try {
|
|
327
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
328
|
+
const parsed = JSON.parse(raw);
|
|
329
|
+
// 加载单聊映射
|
|
330
|
+
if (parsed.singleChatSessions) {
|
|
331
|
+
if (Array.isArray(parsed.singleChatSessions.user_session_map)) {
|
|
332
|
+
this.sessionToUserMap = new Map(parsed.singleChatSessions.user_session_map);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// 群聊的 activeSessionId 已经在 loadGroupSessionsFromDisk 中加载
|
|
336
|
+
// 这里无需重复加载
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// 文件不存在或解析失败,忽略
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// ============ User Tracking ============
|
|
343
|
+
/**
|
|
344
|
+
* 记录用户信息
|
|
345
|
+
* @param userId 用户ID
|
|
346
|
+
* @param username 用户名
|
|
347
|
+
* @param sessionWebhook 单聊的 session webhook
|
|
348
|
+
* @param conversationId 群聊会话ID(如果是群聊则传入)
|
|
349
|
+
* @param groupWebhook 群聊的 session webhook(如果是群聊则传入)
|
|
350
|
+
*/
|
|
351
|
+
trackUser(userId, username, sessionWebhook, conversationId, groupWebhook) {
|
|
352
|
+
const existing = this.activeUsers.get(userId);
|
|
353
|
+
if (existing) {
|
|
354
|
+
existing.lastActiveAt = Date.now();
|
|
355
|
+
if (username)
|
|
356
|
+
existing.username = username;
|
|
357
|
+
// 单聊的 webhook
|
|
358
|
+
if (sessionWebhook)
|
|
359
|
+
existing.sessionWebhook = sessionWebhook;
|
|
360
|
+
// 群聊的 webhook 需要单独存储,避免覆盖单聊的 webhook
|
|
361
|
+
if (conversationId && groupWebhook) {
|
|
362
|
+
if (!existing.groupSessionWebhooks) {
|
|
363
|
+
existing.groupSessionWebhooks = new Map();
|
|
364
|
+
}
|
|
365
|
+
existing.groupSessionWebhooks.set(conversationId, groupWebhook);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
const newUser = {
|
|
370
|
+
userId,
|
|
371
|
+
username,
|
|
372
|
+
firstSeenAt: Date.now(),
|
|
373
|
+
lastActiveAt: Date.now(),
|
|
374
|
+
sessionId: null,
|
|
375
|
+
sessionWebhook,
|
|
376
|
+
};
|
|
377
|
+
// 群聊的 webhook
|
|
378
|
+
if (conversationId && groupWebhook) {
|
|
379
|
+
newUser.groupSessionWebhooks = new Map();
|
|
380
|
+
newUser.groupSessionWebhooks.set(conversationId, groupWebhook);
|
|
381
|
+
}
|
|
382
|
+
this.activeUsers.set(userId, newUser);
|
|
383
|
+
}
|
|
384
|
+
this.saveUsersToDisk();
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* 获取用户在指定群聊中的 session webhook
|
|
388
|
+
*/
|
|
389
|
+
getGroupSessionWebhook(userId, conversationId) {
|
|
390
|
+
const user = this.activeUsers.get(userId);
|
|
391
|
+
return user?.groupSessionWebhooks?.get(conversationId);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* 获取用户的 OpenCode 会话 ID,如不存在则返回 null
|
|
395
|
+
*/
|
|
396
|
+
getUserSessionId(userId) {
|
|
397
|
+
const user = this.activeUsers.get(userId);
|
|
398
|
+
return user?.sessionId;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* 设置用户的 OpenCode 会话 ID
|
|
402
|
+
*/
|
|
403
|
+
setUserSessionId(userId, sessionId) {
|
|
404
|
+
const user = this.activeUsers.get(userId);
|
|
405
|
+
if (user) {
|
|
406
|
+
user.sessionId = sessionId;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* 获取用户消息历史的 key(用于 pendingResponses)
|
|
411
|
+
*/
|
|
412
|
+
getUserResponseKey(userId, sessionId) {
|
|
413
|
+
return `user:${userId}:${sessionId}`;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* 关联 sessionId 和 userId
|
|
417
|
+
*/
|
|
418
|
+
bindSessionToUser(sessionId, userId) {
|
|
419
|
+
this.sessionToUserMap.set(sessionId, userId);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* 根据 sessionId 获取 userId
|
|
423
|
+
*/
|
|
424
|
+
getUserIdBySession(sessionId) {
|
|
425
|
+
return this.sessionToUserMap.get(sessionId);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* 清理 session 到 user 的映射
|
|
429
|
+
*/
|
|
430
|
+
unbindSession(sessionId) {
|
|
431
|
+
this.sessionToUserMap.delete(sessionId);
|
|
432
|
+
}
|
|
433
|
+
getActiveUsers() {
|
|
434
|
+
return Array.from(this.activeUsers.values());
|
|
435
|
+
}
|
|
436
|
+
isUserActive(userId) {
|
|
437
|
+
return this.activeUsers.has(userId);
|
|
438
|
+
}
|
|
439
|
+
// ============ Session Agent ============
|
|
440
|
+
setSessionAgent(sessionId, agent) {
|
|
441
|
+
this.sessionAgents.set(sessionId, agent);
|
|
442
|
+
}
|
|
443
|
+
getActiveAgent() {
|
|
444
|
+
if (!this.activeSessionId)
|
|
445
|
+
return null;
|
|
446
|
+
return this.sessionAgents.get(this.activeSessionId) ?? null;
|
|
447
|
+
}
|
|
448
|
+
setActiveAgent(agent) {
|
|
449
|
+
if (this.activeSessionId) {
|
|
450
|
+
this.sessionAgents.set(this.activeSessionId, agent);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// ============ Group Session Management ============
|
|
454
|
+
/**
|
|
455
|
+
* 获取或创建群聊会话
|
|
456
|
+
*/
|
|
457
|
+
getOrCreateGroupSession(conversationId, title) {
|
|
458
|
+
let session = this.groupSessions.get(conversationId);
|
|
459
|
+
if (!session) {
|
|
460
|
+
session = {
|
|
461
|
+
conversationId,
|
|
462
|
+
title,
|
|
463
|
+
activeSessionId: null,
|
|
464
|
+
createdAt: Date.now(),
|
|
465
|
+
lastActiveAt: Date.now(),
|
|
466
|
+
messageHistory: [],
|
|
467
|
+
};
|
|
468
|
+
this.groupSessions.set(conversationId, session);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
session.lastActiveAt = Date.now();
|
|
472
|
+
if (title)
|
|
473
|
+
session.title = title;
|
|
474
|
+
}
|
|
475
|
+
// 保存到磁盘
|
|
476
|
+
this.saveGroupSessionsToDisk();
|
|
477
|
+
return session;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* 更新群聊的 activeSessionId 并持久化
|
|
481
|
+
*/
|
|
482
|
+
setGroupSessionId(conversationId, sessionId) {
|
|
483
|
+
const session = this.groupSessions.get(conversationId);
|
|
484
|
+
if (session) {
|
|
485
|
+
session.activeSessionId = sessionId;
|
|
486
|
+
this.saveGroupSessionsToDisk();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* 添加群聊消息到历史记录
|
|
491
|
+
*/
|
|
492
|
+
addGroupMessage(conversationId, message) {
|
|
493
|
+
const session = this.groupSessions.get(conversationId);
|
|
494
|
+
if (!session)
|
|
495
|
+
return;
|
|
496
|
+
const newMessage = {
|
|
497
|
+
id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
498
|
+
timestamp: Date.now(),
|
|
499
|
+
...message,
|
|
500
|
+
};
|
|
501
|
+
session.messageHistory.push(newMessage);
|
|
502
|
+
// 限制历史记录数量
|
|
503
|
+
if (session.messageHistory.length > this.GROUP_HISTORY_MAX) {
|
|
504
|
+
session.messageHistory = session.messageHistory.slice(-this.GROUP_HISTORY_MAX);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* 获取群聊消息历史(用于大模型判断相关性)
|
|
509
|
+
*/
|
|
510
|
+
getGroupMessageHistory(conversationId) {
|
|
511
|
+
const session = this.groupSessions.get(conversationId);
|
|
512
|
+
return session?.messageHistory || [];
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* 为群聊创建或获取活跃的 OpenCode 会话
|
|
516
|
+
* @param conversationId 群聊会话ID
|
|
517
|
+
* @param title 可选的会话标题
|
|
518
|
+
*/
|
|
519
|
+
async ensureGroupActiveSession(conversationId, title) {
|
|
520
|
+
if (!this.client)
|
|
521
|
+
return null;
|
|
522
|
+
const session = this.groupSessions.get(conversationId);
|
|
523
|
+
if (!session)
|
|
524
|
+
return null;
|
|
525
|
+
if (!session.activeSessionId) {
|
|
526
|
+
const res = await this.client.session.create({
|
|
527
|
+
body: {
|
|
528
|
+
title: title || `钉钉群聊: ${conversationId}`,
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
if (res.data?.id) {
|
|
532
|
+
session.activeSessionId = res.data.id;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return session.activeSessionId;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// ============ Backward Compatibility ============
|
|
539
|
+
let defaultInstance = null;
|
|
540
|
+
/**
|
|
541
|
+
* Returns the default (singleton) InstanceState.
|
|
542
|
+
* Kept for backward compatibility — new code should use
|
|
543
|
+
* explicit InstanceState instances via BotInstance / registry.
|
|
544
|
+
*/
|
|
545
|
+
export function getState() {
|
|
546
|
+
if (!defaultInstance) {
|
|
547
|
+
defaultInstance = new InstanceState("default");
|
|
548
|
+
}
|
|
549
|
+
return defaultInstance;
|
|
550
|
+
}
|