@wu529778790/open-im 1.9.3-beta.5 → 1.9.3-beta.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.
- package/dist/adapters/claude-sdk-adapter.d.ts +5 -0
- package/dist/adapters/claude-sdk-adapter.js +111 -2
- package/dist/config.js +40 -9
- package/dist/feishu/message-sender.js +21 -0
- package/dist/qq/client.js +2 -0
- package/dist/qq/message-sender.js +4 -4
- package/dist/session/session-manager.js +18 -13
- package/dist/telegram/message-sender.js +1 -3
- package/dist/wework/event-handler.js +78 -80
- package/dist/wework/message-sender.d.ts +1 -2
- package/dist/wework/message-sender.js +6 -13
- package/dist/workbuddy/event-handler.js +12 -9
- package/package.json +1 -1
|
@@ -15,5 +15,10 @@ export declare class ClaudeSDKAdapter implements ToolAdapter {
|
|
|
15
15
|
* 清理所有活跃的 SDK 会话和流
|
|
16
16
|
*/
|
|
17
17
|
static destroy(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Remove a specific session from the in-memory cache and close it.
|
|
20
|
+
* Useful when the caller knows a session is corrupted.
|
|
21
|
+
*/
|
|
22
|
+
static removeSession(sessionId: string): void;
|
|
18
23
|
run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
|
|
19
24
|
}
|
|
@@ -38,6 +38,30 @@ const cleanupInterval = setInterval(() => {
|
|
|
38
38
|
}
|
|
39
39
|
}, CLEANUP_INTERVAL_MS);
|
|
40
40
|
cleanupInterval.unref(); // 不阻止进程退出
|
|
41
|
+
// Lazy cleanup: check idle sessions periodically during getOrCreateSession calls
|
|
42
|
+
let lazyCleanupCounter = 0;
|
|
43
|
+
const LAZY_CLEANUP_INTERVAL = 10;
|
|
44
|
+
let sessionSeq = 0;
|
|
45
|
+
function lazyCleanupIdleSessions() {
|
|
46
|
+
lazyCleanupCounter++;
|
|
47
|
+
if (lazyCleanupCounter % LAZY_CLEANUP_INTERVAL !== 0)
|
|
48
|
+
return;
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
for (const [id, lastUsed] of sessionLastUsed) {
|
|
51
|
+
if (now - lastUsed > SESSION_IDLE_TTL_MS) {
|
|
52
|
+
const s = activeSessions.get(id);
|
|
53
|
+
if (s) {
|
|
54
|
+
try {
|
|
55
|
+
s.close();
|
|
56
|
+
}
|
|
57
|
+
catch { /* ignore */ }
|
|
58
|
+
activeSessions.delete(id);
|
|
59
|
+
}
|
|
60
|
+
sessionLastUsed.delete(id);
|
|
61
|
+
log.info(`Lazy cleanup: idle session ${id} (unused ${Math.round((now - lastUsed) / 60000)}min)`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
41
65
|
// Mutex to serialize process.chdir() calls across concurrent users
|
|
42
66
|
let chdirMutex = Promise.resolve();
|
|
43
67
|
function withChdirMutex(fn) {
|
|
@@ -66,6 +90,9 @@ function isAssistant(msg) {
|
|
|
66
90
|
function isResult(msg) {
|
|
67
91
|
return msg.type === 'result';
|
|
68
92
|
}
|
|
93
|
+
function isSessionCorruptionError(msg) {
|
|
94
|
+
return /session\s*(not found|expired|corrupt)|no\s*conversation\s*found/i.test(msg);
|
|
95
|
+
}
|
|
69
96
|
/**
|
|
70
97
|
* 获取或创建 SDKSession
|
|
71
98
|
* @param sessionId 已有的 sessionId,如果为 undefined 则创建新会话
|
|
@@ -75,6 +102,7 @@ function isResult(msg) {
|
|
|
75
102
|
* @returns SDKSession 对象和实际的 sessionId
|
|
76
103
|
*/
|
|
77
104
|
async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
|
|
105
|
+
lazyCleanupIdleSessions();
|
|
78
106
|
const resolvedModel = model?.trim() || 'claude-opus-4-5';
|
|
79
107
|
const sessionOptions = {
|
|
80
108
|
model: resolvedModel,
|
|
@@ -118,7 +146,7 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
|
|
|
118
146
|
session = unstable_v2_createSession(sessionOptions);
|
|
119
147
|
// 新会话的 sessionId 需要从第一个消息中获取
|
|
120
148
|
// 暂时返回 undefined,稍后在 init 消息中获取
|
|
121
|
-
const tempId = `pending-${
|
|
149
|
+
const tempId = `pending-${++sessionSeq}`;
|
|
122
150
|
activeSessions.set(tempId, session);
|
|
123
151
|
sessionLastUsed.set(tempId, Date.now());
|
|
124
152
|
log.info(`Created new session (tempId: ${tempId})`);
|
|
@@ -160,6 +188,22 @@ export class ClaudeSDKAdapter {
|
|
|
160
188
|
activeSessions.clear();
|
|
161
189
|
sessionLastUsed.clear();
|
|
162
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Remove a specific session from the in-memory cache and close it.
|
|
193
|
+
* Useful when the caller knows a session is corrupted.
|
|
194
|
+
*/
|
|
195
|
+
static removeSession(sessionId) {
|
|
196
|
+
const session = activeSessions.get(sessionId);
|
|
197
|
+
if (session) {
|
|
198
|
+
try {
|
|
199
|
+
session.close();
|
|
200
|
+
}
|
|
201
|
+
catch { /* ignore */ }
|
|
202
|
+
activeSessions.delete(sessionId);
|
|
203
|
+
sessionLastUsed.delete(sessionId);
|
|
204
|
+
log.info(`Explicitly removed session: ${sessionId}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
163
207
|
run(prompt, sessionId, workDir, callbacks, options) {
|
|
164
208
|
log.info(`[V2] run() entry model=${String(options?.model ?? '')} baseUrl=${process.env.ANTHROPIC_BASE_URL ?? '(default)'}`);
|
|
165
209
|
const abortController = new AbortController();
|
|
@@ -167,6 +211,8 @@ export class ClaudeSDKAdapter {
|
|
|
167
211
|
let actualSessionId;
|
|
168
212
|
let pendingTempId; // 记录临时 ID,用于 abort 时清理
|
|
169
213
|
let runSettled = false;
|
|
214
|
+
let currentStream; // 用于 abort 时立即中断 stream
|
|
215
|
+
let timeoutHandle;
|
|
170
216
|
const permissionMode = options?.skipPermissions
|
|
171
217
|
? 'bypassPermissions'
|
|
172
218
|
: options?.permissionMode === 'acceptEdits'
|
|
@@ -193,6 +239,7 @@ export class ClaudeSDKAdapter {
|
|
|
193
239
|
await session.send(prompt);
|
|
194
240
|
// 获取响应流
|
|
195
241
|
const stream = session.stream();
|
|
242
|
+
currentStream = stream;
|
|
196
243
|
activeStreams.add(stream);
|
|
197
244
|
let accumulated = '';
|
|
198
245
|
let accumulatedThinking = '';
|
|
@@ -258,10 +305,20 @@ export class ClaudeSDKAdapter {
|
|
|
258
305
|
log.info(`[V2] Result: subtype=${m.subtype}, num_turns=${m.num_turns}, sessionId=${actualSessionId ?? 'unknown'}`);
|
|
259
306
|
// 检查会话错误
|
|
260
307
|
if (!success) {
|
|
308
|
+
if (timeoutHandle)
|
|
309
|
+
clearTimeout(timeoutHandle);
|
|
261
310
|
runSettled = true;
|
|
262
311
|
const noConvErr = errs.find((e) => e.includes('No conversation found') || e.includes('session not found'));
|
|
263
312
|
if (noConvErr) {
|
|
264
|
-
log.warn(`Session ${actualSessionId} not found,
|
|
313
|
+
log.warn(`Session ${actualSessionId} not found, removing from active sessions`);
|
|
314
|
+
if (actualSessionId) {
|
|
315
|
+
activeSessions.delete(actualSessionId);
|
|
316
|
+
sessionLastUsed.delete(actualSessionId);
|
|
317
|
+
try {
|
|
318
|
+
session.close();
|
|
319
|
+
}
|
|
320
|
+
catch { /* ignore */ }
|
|
321
|
+
}
|
|
265
322
|
callbacks.onSessionInvalid?.();
|
|
266
323
|
}
|
|
267
324
|
const errMsg = errs[0] || '未知错误';
|
|
@@ -286,6 +343,8 @@ export class ClaudeSDKAdapter {
|
|
|
286
343
|
result.result = accumulated;
|
|
287
344
|
}
|
|
288
345
|
runSettled = true;
|
|
346
|
+
if (timeoutHandle)
|
|
347
|
+
clearTimeout(timeoutHandle);
|
|
289
348
|
callbacks.onComplete(result);
|
|
290
349
|
return;
|
|
291
350
|
}
|
|
@@ -294,6 +353,8 @@ export class ClaudeSDKAdapter {
|
|
|
294
353
|
if (!streamClosed) {
|
|
295
354
|
if (accumulated) {
|
|
296
355
|
log.info('Stream ended without result message, using accumulated text');
|
|
356
|
+
if (timeoutHandle)
|
|
357
|
+
clearTimeout(timeoutHandle);
|
|
297
358
|
runSettled = true;
|
|
298
359
|
callbacks.onComplete({
|
|
299
360
|
success: true,
|
|
@@ -308,6 +369,8 @@ export class ClaudeSDKAdapter {
|
|
|
308
369
|
else {
|
|
309
370
|
// 流结束但无 result 也无 accumulated:必须触发回调,否则 Promise 永远挂起
|
|
310
371
|
log.warn('Stream ended with no result and no accumulated text, calling onError to prevent stuck state');
|
|
372
|
+
if (timeoutHandle)
|
|
373
|
+
clearTimeout(timeoutHandle);
|
|
311
374
|
runSettled = true;
|
|
312
375
|
callbacks.onError('AI 响应异常结束(无输出),请重试');
|
|
313
376
|
}
|
|
@@ -330,6 +393,8 @@ export class ClaudeSDKAdapter {
|
|
|
330
393
|
return;
|
|
331
394
|
}
|
|
332
395
|
runSettled = true;
|
|
396
|
+
if (timeoutHandle)
|
|
397
|
+
clearTimeout(timeoutHandle);
|
|
333
398
|
const errorObj = err;
|
|
334
399
|
const msg = errorObj.message || String(err);
|
|
335
400
|
log.error(`Claude SDK V2 error: ${msg}`);
|
|
@@ -342,6 +407,20 @@ export class ClaudeSDKAdapter {
|
|
|
342
407
|
activeSessions.delete(errIdToClean);
|
|
343
408
|
log.info(`Cleaned up pending session after error: ${errIdToClean}`);
|
|
344
409
|
}
|
|
410
|
+
// If error suggests a corrupted session, remove it from cache to prevent reuse
|
|
411
|
+
if (actualSessionId && isSessionCorruptionError(msg)) {
|
|
412
|
+
const corrupted = activeSessions.get(actualSessionId);
|
|
413
|
+
activeSessions.delete(actualSessionId);
|
|
414
|
+
sessionLastUsed.delete(actualSessionId);
|
|
415
|
+
if (corrupted) {
|
|
416
|
+
try {
|
|
417
|
+
corrupted.close();
|
|
418
|
+
}
|
|
419
|
+
catch { /* ignore */ }
|
|
420
|
+
}
|
|
421
|
+
log.warn(`Removed corrupted session ${actualSessionId} after error: ${msg}`);
|
|
422
|
+
callbacks.onSessionInvalid?.();
|
|
423
|
+
}
|
|
345
424
|
callbacks.onError(msg);
|
|
346
425
|
}
|
|
347
426
|
};
|
|
@@ -349,15 +428,45 @@ export class ClaudeSDKAdapter {
|
|
|
349
428
|
runSession().catch((err) => {
|
|
350
429
|
if (!runSettled) {
|
|
351
430
|
runSettled = true;
|
|
431
|
+
if (timeoutHandle)
|
|
432
|
+
clearTimeout(timeoutHandle);
|
|
352
433
|
const msg = err instanceof Error ? err.message : String(err);
|
|
353
434
|
log.error(`Unhandled runSession error: ${msg}`);
|
|
354
435
|
callbacks.onError(msg);
|
|
355
436
|
}
|
|
356
437
|
});
|
|
438
|
+
// 强制执行超时
|
|
439
|
+
if (options?.timeoutMs && options.timeoutMs > 0) {
|
|
440
|
+
timeoutHandle = setTimeout(() => {
|
|
441
|
+
if (!runSettled) {
|
|
442
|
+
log.warn(`Session timed out after ${options.timeoutMs}ms, aborting`);
|
|
443
|
+
abortController.abort();
|
|
444
|
+
// 立即中断 stream,不等下一条消息
|
|
445
|
+
if (currentStream) {
|
|
446
|
+
try {
|
|
447
|
+
currentStream.return?.();
|
|
448
|
+
}
|
|
449
|
+
catch { /* ignore */ }
|
|
450
|
+
}
|
|
451
|
+
runSettled = true;
|
|
452
|
+
callbacks.onError(`AI 响应超时(${Math.round(options.timeoutMs / 1000)}s),请重试`);
|
|
453
|
+
}
|
|
454
|
+
}, options.timeoutMs);
|
|
455
|
+
timeoutHandle.unref();
|
|
456
|
+
}
|
|
357
457
|
return {
|
|
358
458
|
abort: () => {
|
|
359
459
|
log.info('Aborting session run');
|
|
360
460
|
abortController.abort();
|
|
461
|
+
if (timeoutHandle)
|
|
462
|
+
clearTimeout(timeoutHandle);
|
|
463
|
+
// 立即中断 stream,不等下一条消息
|
|
464
|
+
if (currentStream) {
|
|
465
|
+
try {
|
|
466
|
+
currentStream.return?.();
|
|
467
|
+
}
|
|
468
|
+
catch { /* ignore */ }
|
|
469
|
+
}
|
|
361
470
|
},
|
|
362
471
|
};
|
|
363
472
|
}
|
package/dist/config.js
CHANGED
|
@@ -4,7 +4,7 @@ try {
|
|
|
4
4
|
catch {
|
|
5
5
|
/* dotenv optional */
|
|
6
6
|
}
|
|
7
|
-
import { readFileSync, writeFileSync, accessSync, constants, existsSync, mkdirSync } from 'node:fs';
|
|
7
|
+
import { readFileSync, writeFileSync, accessSync, constants, existsSync, mkdirSync, statSync } from 'node:fs';
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
9
|
import { join, dirname, isAbsolute } from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
@@ -22,6 +22,9 @@ const OLD_ROOT_KEYS = [
|
|
|
22
22
|
'claudeWorkDir',
|
|
23
23
|
'claudeTimeoutMs', 'claudeModel',
|
|
24
24
|
];
|
|
25
|
+
// Config cache with mtime tracking
|
|
26
|
+
let cachedConfig = null;
|
|
27
|
+
let cachedClaudeEnv = null;
|
|
25
28
|
function hasOldConfigFormat(raw) {
|
|
26
29
|
const hasOld = OLD_ROOT_KEYS.some((k) => raw[k] !== undefined && raw[k] !== null);
|
|
27
30
|
const hasNew = raw.tools && typeof raw.tools === 'object' && raw.tools.claude;
|
|
@@ -78,6 +81,16 @@ function migrateToNewConfigFormat(raw) {
|
|
|
78
81
|
}
|
|
79
82
|
export function loadFileConfig() {
|
|
80
83
|
try {
|
|
84
|
+
// Check if file exists and get mtime for cache validation
|
|
85
|
+
if (!existsSync(CONFIG_PATH))
|
|
86
|
+
return {};
|
|
87
|
+
const stats = statSync(CONFIG_PATH);
|
|
88
|
+
const currentMtime = stats.mtimeMs;
|
|
89
|
+
// Return cached config if file hasn't changed
|
|
90
|
+
if (cachedConfig && cachedConfig.mtime === currentMtime) {
|
|
91
|
+
return cachedConfig.config;
|
|
92
|
+
}
|
|
93
|
+
// File changed or no cache, read and parse
|
|
81
94
|
const raw = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
82
95
|
if (!raw || typeof raw !== 'object')
|
|
83
96
|
return {};
|
|
@@ -87,8 +100,12 @@ export function loadFileConfig() {
|
|
|
87
100
|
if (!existsSync(dir))
|
|
88
101
|
mkdirSync(dir, { recursive: true });
|
|
89
102
|
writeFileSync(CONFIG_PATH, JSON.stringify(migrated, null, 2), 'utf-8');
|
|
103
|
+
// Update cache with migrated config
|
|
104
|
+
cachedConfig = { config: migrated, mtime: currentMtime };
|
|
90
105
|
return migrated;
|
|
91
106
|
}
|
|
107
|
+
// Update cache
|
|
108
|
+
cachedConfig = { config: raw, mtime: currentMtime };
|
|
92
109
|
return raw;
|
|
93
110
|
}
|
|
94
111
|
catch {
|
|
@@ -100,6 +117,8 @@ export function saveFileConfig(raw) {
|
|
|
100
117
|
if (!existsSync(dir))
|
|
101
118
|
mkdirSync(dir, { recursive: true });
|
|
102
119
|
writeFileSync(CONFIG_PATH, JSON.stringify(raw, null, 2), 'utf-8');
|
|
120
|
+
// Invalidate cache after save (next read will get fresh mtime)
|
|
121
|
+
cachedConfig = null;
|
|
103
122
|
}
|
|
104
123
|
/** 获取用户主目录(兼容不同运行环境,如 launchd、systemd 等) */
|
|
105
124
|
export function getClaudeConfigHome() {
|
|
@@ -114,16 +133,26 @@ export function loadClaudeSettingsEnv() {
|
|
|
114
133
|
];
|
|
115
134
|
for (const p of paths) {
|
|
116
135
|
try {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
136
|
+
// Check cache first
|
|
137
|
+
if (existsSync(p)) {
|
|
138
|
+
const stats = statSync(p);
|
|
139
|
+
const currentMtime = stats.mtimeMs;
|
|
140
|
+
if (cachedClaudeEnv && cachedClaudeEnv.mtime === currentMtime && cachedClaudeEnv.env) {
|
|
141
|
+
return cachedClaudeEnv.env;
|
|
142
|
+
}
|
|
143
|
+
const raw = JSON.parse(readFileSync(p, 'utf-8'));
|
|
144
|
+
const env = raw?.env;
|
|
145
|
+
if (env && typeof env === 'object') {
|
|
146
|
+
const result = {};
|
|
147
|
+
for (const [k, v] of Object.entries(env)) {
|
|
148
|
+
if (v != null && typeof k === 'string') {
|
|
149
|
+
result[k] = String(v);
|
|
150
|
+
}
|
|
124
151
|
}
|
|
152
|
+
// Update cache
|
|
153
|
+
cachedClaudeEnv = { env: result, mtime: currentMtime };
|
|
154
|
+
return result;
|
|
125
155
|
}
|
|
126
|
-
return result;
|
|
127
156
|
}
|
|
128
157
|
}
|
|
129
158
|
catch {
|
|
@@ -156,6 +185,8 @@ export function saveClaudeSettingsEnv(env) {
|
|
|
156
185
|
existing.env = { ...existing.env, ...env };
|
|
157
186
|
// 写入文件
|
|
158
187
|
writeFileSync(claudeSettingsPath, JSON.stringify(existing, null, 2), 'utf-8');
|
|
188
|
+
// Invalidate cache after save
|
|
189
|
+
cachedClaudeEnv = null;
|
|
159
190
|
}
|
|
160
191
|
catch (error) {
|
|
161
192
|
log.error('Failed to save Claude settings:', error);
|
|
@@ -164,9 +164,25 @@ export async function sendErrorCard(cardId, error) {
|
|
|
164
164
|
// Track if patch API is working for this session
|
|
165
165
|
let patchApiWorking = true;
|
|
166
166
|
let patchFailCount = 0;
|
|
167
|
+
let patchDisabledAt = null; // When patch was disabled
|
|
167
168
|
const MAX_PATCH_FAILURES_BEFORE_DISABLE = 3;
|
|
169
|
+
const PATCH_RECOVERY_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes before retrying
|
|
170
|
+
// Attempt to recover patch API after cooldown period
|
|
171
|
+
function attemptPatchRecovery() {
|
|
172
|
+
if (!patchApiWorking && patchDisabledAt !== null) {
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
if (now - patchDisabledAt >= PATCH_RECOVERY_COOLDOWN_MS) {
|
|
175
|
+
log.info('Attempting to recover patch API after cooldown period');
|
|
176
|
+
patchApiWorking = true;
|
|
177
|
+
patchFailCount = 0;
|
|
178
|
+
patchDisabledAt = null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
168
182
|
export async function updateMessage(chatId, messageId, content, status, note, toolId = 'claude') {
|
|
169
183
|
const client = getClient();
|
|
184
|
+
// Try to recover patch API if cooldown period has passed
|
|
185
|
+
attemptPatchRecovery();
|
|
170
186
|
const title = getToolTitle(toolId, status);
|
|
171
187
|
const cardContent = createFeishuCard(title, content, status, note);
|
|
172
188
|
// Try to use patch API for in-place update (streaming)
|
|
@@ -198,6 +214,7 @@ export async function updateMessage(chatId, messageId, content, status, note, to
|
|
|
198
214
|
if (patchFailCount >= MAX_PATCH_FAILURES_BEFORE_DISABLE) {
|
|
199
215
|
log.warn('Patch API disabled for this session due to repeated failures');
|
|
200
216
|
patchApiWorking = false;
|
|
217
|
+
patchDisabledAt = Date.now();
|
|
201
218
|
}
|
|
202
219
|
// 流式更新时不 fallback 到 delete+create,否则会生成新消息但 caller 仍持旧 msgId,导致后续 patch 全失败并不断创建新消息
|
|
203
220
|
// 直接返回,下次节流周期会重试
|
|
@@ -210,6 +227,7 @@ export async function updateMessage(chatId, messageId, content, status, note, to
|
|
|
210
227
|
if (patchFailCount >= MAX_PATCH_FAILURES_BEFORE_DISABLE) {
|
|
211
228
|
log.warn('Patch API disabled for this session due to repeated errors');
|
|
212
229
|
patchApiWorking = false;
|
|
230
|
+
patchDisabledAt = Date.now();
|
|
213
231
|
}
|
|
214
232
|
}
|
|
215
233
|
}
|
|
@@ -219,6 +237,8 @@ export async function updateMessage(chatId, messageId, content, status, note, to
|
|
|
219
237
|
export async function sendFinalMessages(chatId, messageId, fullContent, note, toolId = 'claude') {
|
|
220
238
|
const client = getClient();
|
|
221
239
|
const parts = splitLongContent(fullContent, MAX_FEISHU_MESSAGE_LENGTH);
|
|
240
|
+
// Try to recover patch API if cooldown period has passed
|
|
241
|
+
attemptPatchRecovery();
|
|
222
242
|
// If content fits in one message and patch is working, try patch for smooth transition
|
|
223
243
|
if (parts.length === 1 && patchApiWorking) {
|
|
224
244
|
const cardContent = createFeishuCard(getToolTitle(toolId, 'done'), fullContent, 'done');
|
|
@@ -319,6 +339,7 @@ export async function sendImageReply(chatId, imagePath) {
|
|
|
319
339
|
Authorization: `Bearer ${token}`,
|
|
320
340
|
},
|
|
321
341
|
body: form,
|
|
342
|
+
signal: AbortSignal.timeout(60000),
|
|
322
343
|
});
|
|
323
344
|
if (!uploadResp.ok) {
|
|
324
345
|
throw new Error(`Failed to upload image: ${uploadResp.statusText}`);
|
package/dist/qq/client.js
CHANGED
|
@@ -48,6 +48,7 @@ async function fetchAccessToken(config) {
|
|
|
48
48
|
appId: config.qqAppId,
|
|
49
49
|
clientSecret: config.qqSecret,
|
|
50
50
|
}),
|
|
51
|
+
signal: AbortSignal.timeout(15000),
|
|
51
52
|
});
|
|
52
53
|
const data = (await response.json());
|
|
53
54
|
if (!response.ok || !data.access_token) {
|
|
@@ -65,6 +66,7 @@ async function apiRequest(config, method, path, body) {
|
|
|
65
66
|
method,
|
|
66
67
|
headers: buildAuthHeaders(token),
|
|
67
68
|
body: body ? JSON.stringify(body) : undefined,
|
|
69
|
+
signal: AbortSignal.timeout(15000),
|
|
68
70
|
});
|
|
69
71
|
if (!response.ok) {
|
|
70
72
|
const text = await response.text().catch(() => "");
|
|
@@ -9,15 +9,15 @@ const MAX_QQ_MESSAGE_LENGTH = 1500;
|
|
|
9
9
|
const pendingReplies = new Map();
|
|
10
10
|
// Periodic cleanup of orphaned pending replies
|
|
11
11
|
const PENDING_MAX_AGE_MS = 10 * 60 * 1000; // 10 minutes
|
|
12
|
+
const CLEANUP_INTERVAL_MS = 2 * 60 * 1000; // Check every 2 minutes
|
|
12
13
|
setInterval(() => {
|
|
13
14
|
const now = Date.now();
|
|
14
15
|
for (const [id, state] of pendingReplies) {
|
|
15
|
-
|
|
16
|
-
if (pendingReplies.size > 100) {
|
|
16
|
+
if (now - state.createdAt > PENDING_MAX_AGE_MS) {
|
|
17
17
|
pendingReplies.delete(id);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
},
|
|
20
|
+
}, CLEANUP_INTERVAL_MS).unref();
|
|
21
21
|
function parseChatTarget(chatId) {
|
|
22
22
|
if (chatId.startsWith("group:")) {
|
|
23
23
|
return { kind: "group", id: chatId.slice("group:".length) };
|
|
@@ -57,7 +57,7 @@ export async function sendImageReply(chatId, imagePath) {
|
|
|
57
57
|
}
|
|
58
58
|
export async function sendThinkingMessage(chatId, replyToMessageId, _toolId = "claude") {
|
|
59
59
|
const messageId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
60
|
-
pendingReplies.set(messageId, { replyToMessageId });
|
|
60
|
+
pendingReplies.set(messageId, { replyToMessageId, createdAt: Date.now() });
|
|
61
61
|
return messageId;
|
|
62
62
|
}
|
|
63
63
|
export async function updateMessage(_chatId, _messageId, _content, _status, _note, _toolId = "claude") {
|
|
@@ -68,16 +68,20 @@ export class SessionManager {
|
|
|
68
68
|
return undefined;
|
|
69
69
|
}
|
|
70
70
|
setSessionIdForThread(userId, threadId, toolId, sessionId) {
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
s
|
|
74
|
-
|
|
75
|
-
if (t) {
|
|
76
|
-
if (!t.sessionIds)
|
|
77
|
-
t.sessionIds = {};
|
|
78
|
-
t.sessionIds[toolId] = sessionId;
|
|
79
|
-
this.save();
|
|
71
|
+
let s = this.sessions.get(userId);
|
|
72
|
+
if (!s) {
|
|
73
|
+
s = { workDir: this.defaultWorkDir, activeConvId: randomBytes(4).toString('hex') };
|
|
74
|
+
this.sessions.set(userId, s);
|
|
80
75
|
}
|
|
76
|
+
if (!s.threads)
|
|
77
|
+
s.threads = {};
|
|
78
|
+
if (!s.threads[threadId])
|
|
79
|
+
s.threads[threadId] = {};
|
|
80
|
+
const t = s.threads[threadId];
|
|
81
|
+
if (!t.sessionIds)
|
|
82
|
+
t.sessionIds = {};
|
|
83
|
+
t.sessionIds[toolId] = sessionId;
|
|
84
|
+
this.save();
|
|
81
85
|
}
|
|
82
86
|
getWorkDir(userId) {
|
|
83
87
|
return this.sessions.get(userId)?.workDir ?? this.defaultWorkDir;
|
|
@@ -103,8 +107,9 @@ export class SessionManager {
|
|
|
103
107
|
const currentDir = this.getWorkDir(userId);
|
|
104
108
|
const realPath = await this.resolveAndValidate(currentDir, workDir);
|
|
105
109
|
const s = this.sessions.get(userId);
|
|
110
|
+
let oldConvId;
|
|
106
111
|
if (s) {
|
|
107
|
-
|
|
112
|
+
oldConvId = s.activeConvId;
|
|
108
113
|
this.persistActiveConvSessions(userId, s);
|
|
109
114
|
s.workDir = realPath;
|
|
110
115
|
s.sessionIds = {};
|
|
@@ -120,7 +125,7 @@ export class SessionManager {
|
|
|
120
125
|
});
|
|
121
126
|
}
|
|
122
127
|
this.flushSync();
|
|
123
|
-
log.info(`WorkDir changed for user ${userId}: ${realPath}, oldConvId=${
|
|
128
|
+
log.info(`WorkDir changed for user ${userId}: ${realPath}, oldConvId=${oldConvId}`);
|
|
124
129
|
return realPath;
|
|
125
130
|
}
|
|
126
131
|
/**
|
|
@@ -294,8 +299,8 @@ export class SessionManager {
|
|
|
294
299
|
}
|
|
295
300
|
}
|
|
296
301
|
}
|
|
297
|
-
catch {
|
|
298
|
-
|
|
302
|
+
catch (err) {
|
|
303
|
+
log.warn('Failed to load sessions file, starting with empty state:', err instanceof Error ? err.message : err);
|
|
299
304
|
}
|
|
300
305
|
}
|
|
301
306
|
save() {
|
|
@@ -120,9 +120,7 @@ export async function sendFinalMessages(chatId, messageId, fullContent, note, to
|
|
|
120
120
|
export async function sendTextReply(chatId, text) {
|
|
121
121
|
const bot = getBot();
|
|
122
122
|
try {
|
|
123
|
-
await bot.telegram.sendMessage(Number(chatId), formatMessage(text, "done", undefined, OPEN_IM_SYSTEM_TITLE)
|
|
124
|
-
parse_mode: "Markdown",
|
|
125
|
-
});
|
|
123
|
+
await bot.telegram.sendMessage(Number(chatId), formatMessage(text, "done", undefined, OPEN_IM_SYSTEM_TITLE));
|
|
126
124
|
}
|
|
127
125
|
catch (err) {
|
|
128
126
|
log.error("Failed to send text:", err);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { resolvePlatformAiCommand } from '../config.js';
|
|
5
5
|
import { AccessControl } from '../access/access-control.js';
|
|
6
6
|
import { RequestQueue } from '../queue/request-queue.js';
|
|
7
|
-
import { sendThinkingMessage, updateMessage, sendFinalMessages, sendTextReply, sendImageReply,
|
|
7
|
+
import { sendThinkingMessage, updateMessage, sendFinalMessages, sendTextReply, sendImageReply, sendDirectorySelection, startTypingLoop, } from './message-sender.js';
|
|
8
8
|
import { CommandHandler } from '../commands/handler.js';
|
|
9
9
|
import { getAdapter } from '../adapters/registry.js';
|
|
10
10
|
import { runAITask } from '../shared/ai-task.js';
|
|
@@ -164,97 +164,98 @@ export function setupWeWorkHandlers(config, sessionManager) {
|
|
|
164
164
|
const requestQueue = new RequestQueue();
|
|
165
165
|
const runningTasks = new Map();
|
|
166
166
|
const stopTaskCleanup = startTaskCleanup(runningTasks);
|
|
167
|
+
// Mutable ref that captures the req_id of the message currently being handled.
|
|
168
|
+
// WeWork requires req_id to reply; CommandHandler doesn't carry it, so we inject
|
|
169
|
+
// it via a closure. WeWork delivers messages sequentially over WebSocket, so
|
|
170
|
+
// there is no race condition between concurrent messages from the same bot.
|
|
171
|
+
const senderCtx = { reqId: '' };
|
|
167
172
|
const commandHandler = new CommandHandler({
|
|
168
173
|
config,
|
|
169
174
|
sessionManager,
|
|
170
175
|
requestQueue,
|
|
171
|
-
sender: {
|
|
176
|
+
sender: {
|
|
177
|
+
sendTextReply: (chatId, text) => sendTextReply(chatId, text, senderCtx.reqId),
|
|
178
|
+
sendDirectorySelection: (chatId, currentDir, userId) => sendDirectorySelection(chatId, currentDir, userId, senderCtx.reqId),
|
|
179
|
+
},
|
|
172
180
|
getRunningTasksSize: () => runningTasks.size,
|
|
173
181
|
});
|
|
174
182
|
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId, reqId) {
|
|
175
183
|
log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, promptLength=${prompt.length}`);
|
|
176
|
-
|
|
177
|
-
|
|
184
|
+
const aiCommand = resolvePlatformAiCommand(config, 'wework');
|
|
185
|
+
const toolAdapter = getAdapter(aiCommand);
|
|
186
|
+
if (!toolAdapter) {
|
|
187
|
+
log.error(`[handleAIRequest] No adapter found for: ${aiCommand}`);
|
|
188
|
+
await sendTextReply(chatId, `AI tool is not configured: ${aiCommand}`, reqId);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const sessionId = convId
|
|
192
|
+
? sessionManager.getSessionIdForConv(userId, convId, aiCommand)
|
|
193
|
+
: undefined;
|
|
194
|
+
log.info(`[handleAIRequest] Running ${aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
|
|
195
|
+
const toolId = aiCommand;
|
|
196
|
+
let msgId;
|
|
178
197
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
await sendTextReply(chatId, `AI tool is not configured: ${aiCommand}`, reqId);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const sessionId = convId
|
|
187
|
-
? sessionManager.getSessionIdForConv(userId, convId, aiCommand)
|
|
188
|
-
: undefined;
|
|
189
|
-
log.info(`[handleAIRequest] Running ${aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
|
|
190
|
-
const toolId = aiCommand;
|
|
191
|
-
let msgId;
|
|
198
|
+
msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId, reqId);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
log.error('Failed to send thinking message:', err);
|
|
192
202
|
try {
|
|
193
|
-
|
|
203
|
+
await sendTextReply(chatId, '启动 AI 处理失败,请重试。', reqId);
|
|
194
204
|
}
|
|
195
205
|
catch (err) {
|
|
196
|
-
log.
|
|
197
|
-
try {
|
|
198
|
-
await sendTextReply(chatId, '启动 AI 处理失败,请重试。', reqId);
|
|
199
|
-
}
|
|
200
|
-
catch (err) {
|
|
201
|
-
log.warn('Failed to send startup error reply:', err);
|
|
202
|
-
}
|
|
203
|
-
return;
|
|
206
|
+
log.warn('Failed to send startup error reply:', err);
|
|
204
207
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const stopTyping = startTypingLoop(chatId);
|
|
211
|
+
const taskKey = `${userId}:${msgId}`;
|
|
212
|
+
// Safety timeout: abort hung tasks before stream expires, unblocking the queue
|
|
213
|
+
let safetyTimer = setTimeout(() => {
|
|
214
|
+
safetyTimer = null;
|
|
215
|
+
const state = runningTasks.get(taskKey);
|
|
216
|
+
if (state) {
|
|
217
|
+
log.warn(`[SAFETY_TIMEOUT] Task ${taskKey} exceeded ${WEWORK_TASK_SAFETY_TIMEOUT_MS}ms, aborting`);
|
|
218
|
+
state.handle.abort();
|
|
219
|
+
runningTasks.delete(taskKey);
|
|
220
|
+
stopTyping();
|
|
221
|
+
sendTextReply(chatId, `AI 处理超时(${Math.round(WEWORK_TASK_SAFETY_TIMEOUT_MS / 1000)}s),已自动取消。请重试。`, reqId).catch(() => { });
|
|
222
|
+
}
|
|
223
|
+
}, WEWORK_TASK_SAFETY_TIMEOUT_MS);
|
|
224
|
+
const clearSafetyTimer = () => {
|
|
225
|
+
if (safetyTimer) {
|
|
226
|
+
clearTimeout(safetyTimer);
|
|
209
227
|
safetyTimer = null;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'wework', taskKey }, prompt, toolAdapter, {
|
|
231
|
+
throttleMs: WEWORK_THROTTLE_MS,
|
|
232
|
+
streamUpdate: async (content, toolNote) => {
|
|
233
|
+
const note = buildProgressNote(toolNote);
|
|
234
|
+
try {
|
|
235
|
+
await updateMessage(chatId, msgId, content, 'streaming', note, toolId, reqId);
|
|
217
236
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (safetyTimer) {
|
|
221
|
-
clearTimeout(safetyTimer);
|
|
222
|
-
safetyTimer = null;
|
|
237
|
+
catch (err) {
|
|
238
|
+
log.debug('Stream update failed:', err);
|
|
223
239
|
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
clearSafetyTimer();
|
|
244
|
-
stopTyping();
|
|
245
|
-
runningTasks.delete(taskKey);
|
|
246
|
-
},
|
|
247
|
-
onTaskReady: (state) => {
|
|
248
|
-
runningTasks.set(taskKey, state);
|
|
249
|
-
},
|
|
250
|
-
sendImage: async (path) => {
|
|
251
|
-
await sendImageReply(chatId, path);
|
|
252
|
-
},
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
finally {
|
|
256
|
-
setCurrentReqId(null);
|
|
257
|
-
}
|
|
240
|
+
},
|
|
241
|
+
sendComplete: async (content, note) => {
|
|
242
|
+
await sendFinalMessages(chatId, msgId, content, note ?? '', toolId, reqId);
|
|
243
|
+
},
|
|
244
|
+
sendError: async (error) => {
|
|
245
|
+
await updateMessage(chatId, msgId, `Error: ${error}`, 'error', buildErrorNote(), toolId, reqId);
|
|
246
|
+
},
|
|
247
|
+
extraCleanup: () => {
|
|
248
|
+
clearSafetyTimer();
|
|
249
|
+
stopTyping();
|
|
250
|
+
runningTasks.delete(taskKey);
|
|
251
|
+
},
|
|
252
|
+
onTaskReady: (state) => {
|
|
253
|
+
runningTasks.set(taskKey, state);
|
|
254
|
+
},
|
|
255
|
+
sendImage: async (path) => {
|
|
256
|
+
await sendImageReply(chatId, path);
|
|
257
|
+
},
|
|
258
|
+
});
|
|
258
259
|
}
|
|
259
260
|
async function enqueuePrompt(userId, chatId, prompt, reqId) {
|
|
260
261
|
const workDir = sessionManager.getWorkDir(userId);
|
|
@@ -272,7 +273,7 @@ export function setupWeWorkHandlers(config, sessionManager) {
|
|
|
272
273
|
async function handleEvent(data) {
|
|
273
274
|
log.info('[handleEvent] Called with data:', JSON.stringify(data).slice(0, 800));
|
|
274
275
|
const reqId = data.headers?.req_id ?? '';
|
|
275
|
-
|
|
276
|
+
senderCtx.reqId = reqId;
|
|
276
277
|
try {
|
|
277
278
|
const body = data.body;
|
|
278
279
|
const msgType = body.msgtype;
|
|
@@ -329,9 +330,6 @@ export function setupWeWorkHandlers(config, sessionManager) {
|
|
|
329
330
|
catch (err) {
|
|
330
331
|
log.error('[handleEvent] Error processing event:', err);
|
|
331
332
|
}
|
|
332
|
-
finally {
|
|
333
|
-
setCurrentReqId(null);
|
|
334
|
-
}
|
|
335
333
|
}
|
|
336
334
|
return {
|
|
337
335
|
stop: () => stopTaskCleanup(),
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* WeWork (企业微信/WeCom) Message Sender
|
|
3
3
|
* 通过 WebSocket `aibot_respond_msg` 发送消息,并透传 `req_id`
|
|
4
4
|
*/
|
|
5
|
-
export declare function setCurrentReqId(reqId: string | null): void;
|
|
6
5
|
type MessageStatus = 'thinking' | 'streaming' | 'done' | 'error';
|
|
7
6
|
/**
|
|
8
7
|
* Send thinking message to WeWork.
|
|
@@ -22,7 +21,7 @@ export declare function sendProactiveTextReply(chatId: string, text: string): Pr
|
|
|
22
21
|
*/
|
|
23
22
|
export declare function sendTextReply(chatId: string, text: string, threadCtxOrReqId?: import('../shared/types.js').ThreadContext | string): Promise<void>;
|
|
24
23
|
export declare function sendImageReply(chatId: string, imagePath: string): Promise<void>;
|
|
25
|
-
export declare function sendDirectorySelection(chatId: string, currentDir: string, _userId: string): Promise<void>;
|
|
24
|
+
export declare function sendDirectorySelection(chatId: string, currentDir: string, _userId: string, reqId?: string): Promise<void>;
|
|
26
25
|
export declare function startTypingLoop(_chatId: string): () => void;
|
|
27
26
|
export declare function sendErrorMessage(chatId: string, error: string, reqId?: string): Promise<void>;
|
|
28
27
|
export {};
|
|
@@ -14,18 +14,12 @@ import { readFile } from 'node:fs/promises';
|
|
|
14
14
|
const log = createLogger('WeWorkSender');
|
|
15
15
|
const STREAM_SEND_INTERVAL_MS = 900;
|
|
16
16
|
const STREAM_SAFE_TTL_MS = 5 * 60 * 1000;
|
|
17
|
-
/** 当前同步处理中的 req_id,仅用于 commandHandler 等同步调用。 */
|
|
18
|
-
let currentReqId = null;
|
|
19
|
-
export function setCurrentReqId(reqId) {
|
|
20
|
-
currentReqId = reqId;
|
|
21
|
-
}
|
|
22
17
|
function getReqId(explicitReqId) {
|
|
23
|
-
|
|
24
|
-
if (!id) {
|
|
18
|
+
if (!explicitReqId) {
|
|
25
19
|
log.warn('No req_id - cannot send WeWork reply');
|
|
26
20
|
return '';
|
|
27
21
|
}
|
|
28
|
-
return
|
|
22
|
+
return explicitReqId;
|
|
29
23
|
}
|
|
30
24
|
const STATUS_CONFIG = {
|
|
31
25
|
thinking: { icon: '[thinking]', title: '思考中' },
|
|
@@ -300,10 +294,9 @@ export async function sendProactiveTextReply(chatId, text) {
|
|
|
300
294
|
*/
|
|
301
295
|
export async function sendTextReply(chatId, text, threadCtxOrReqId) {
|
|
302
296
|
const message = formatWeWorkMessage(OPEN_IM_SYSTEM_TITLE, text, 'done');
|
|
303
|
-
const
|
|
304
|
-
const effectiveReqId = explicitReqId ?? currentReqId;
|
|
297
|
+
const effectiveReqId = typeof threadCtxOrReqId === 'string' ? threadCtxOrReqId : undefined;
|
|
305
298
|
try {
|
|
306
|
-
sendText(getReqId(effectiveReqId
|
|
299
|
+
sendText(getReqId(effectiveReqId), message);
|
|
307
300
|
log.info(`Text reply sent to user ${chatId}`);
|
|
308
301
|
}
|
|
309
302
|
catch (err) {
|
|
@@ -332,8 +325,8 @@ export async function sendImageReply(chatId, imagePath) {
|
|
|
332
325
|
await sendTextReply(chatId, `Generated image saved at: ${imagePath}`);
|
|
333
326
|
}
|
|
334
327
|
}
|
|
335
|
-
export async function sendDirectorySelection(chatId, currentDir, _userId) {
|
|
336
|
-
await sendTextReply(chatId, buildDirectoryMessage(currentDir));
|
|
328
|
+
export async function sendDirectorySelection(chatId, currentDir, _userId, reqId) {
|
|
329
|
+
await sendTextReply(chatId, buildDirectoryMessage(currentDir), reqId);
|
|
337
330
|
}
|
|
338
331
|
export function startTypingLoop(_chatId) {
|
|
339
332
|
return () => { };
|
|
@@ -20,18 +20,13 @@ export function setupWorkBuddyHandlers(config, sessionManager) {
|
|
|
20
20
|
const runningTasks = new Map();
|
|
21
21
|
const taskKeyByChatId = new Map();
|
|
22
22
|
const stopTaskCleanup = startTaskCleanup(runningTasks);
|
|
23
|
-
|
|
24
|
-
const
|
|
23
|
+
// Base dependencies for creating per-event CommandHandler
|
|
24
|
+
const baseCommandDeps = {
|
|
25
25
|
config,
|
|
26
26
|
sessionManager,
|
|
27
27
|
requestQueue,
|
|
28
|
-
sender: {
|
|
29
|
-
sendTextReply: async (chatId, text) => {
|
|
30
|
-
await sendTextReply(null, chatId, text, currentMsgId);
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
28
|
getRunningTasksSize: () => runningTasks.size,
|
|
34
|
-
}
|
|
29
|
+
};
|
|
35
30
|
async function handleAIRequest(userId, chatId, msgId, prompt, workDir, convId) {
|
|
36
31
|
log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, promptLength=${prompt.length}`);
|
|
37
32
|
const aiCommand = resolvePlatformAiCommand(config, 'workbuddy');
|
|
@@ -72,7 +67,6 @@ export function setupWorkBuddyHandlers(config, sessionManager) {
|
|
|
72
67
|
});
|
|
73
68
|
}
|
|
74
69
|
async function handleEvent(chatId, msgId, content) {
|
|
75
|
-
currentMsgId = msgId;
|
|
76
70
|
log.info(`[handleEvent] chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}"`);
|
|
77
71
|
// Use chatId as userId for WorkBuddy (WeChat KF doesn't have separate userId)
|
|
78
72
|
const userId = chatId;
|
|
@@ -86,6 +80,15 @@ export function setupWorkBuddyHandlers(config, sessionManager) {
|
|
|
86
80
|
setChatUser(chatId, userId, 'workbuddy');
|
|
87
81
|
const workDir = sessionManager.getWorkDir(userId);
|
|
88
82
|
const convId = sessionManager.getConvId(userId);
|
|
83
|
+
// Create a per-event CommandHandler with sender that captures msgId for this event
|
|
84
|
+
const commandHandler = new CommandHandler({
|
|
85
|
+
...baseCommandDeps,
|
|
86
|
+
sender: {
|
|
87
|
+
sendTextReply: async (c, t) => {
|
|
88
|
+
await sendTextReply(null, c, t, msgId);
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
89
92
|
// Try command handler first
|
|
90
93
|
try {
|
|
91
94
|
const handled = await commandHandler.dispatch(text, chatId, userId, 'workbuddy', (u, c, p, w, conv, _r, m) => handleAIRequest(u, c, msgId, p, w, conv));
|