@wu529778790/open-im 1.10.8 → 1.10.9-beta.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/dist/adapters/claude-sdk-adapter.js +40 -5
- package/dist/adapters/codebuddy-adapter.js +6 -8
- package/dist/adapters/codex-adapter.js +5 -13
- package/dist/config/file-io.d.ts +2 -0
- package/dist/config/file-io.js +86 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +10 -2
- package/dist/shared/ai-task.js +3 -1
- package/dist/workbuddy/centrifuge-client.js +31 -6
- package/package.json +2 -1
|
@@ -59,6 +59,8 @@ let sessionIdleTtlMs = 30 * 60 * 1000; // 默认 30 分钟未使用则清理
|
|
|
59
59
|
let sessionIdleCleanupDisabled = false;
|
|
60
60
|
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 每 5 分钟检查一次
|
|
61
61
|
const MAX_ACTIVE_SESSIONS = 100;
|
|
62
|
+
const MAX_CAPTURED_STDERR_CHARS = 4000;
|
|
63
|
+
const MAX_EXPOSED_STDERR_CHARS = 500;
|
|
62
64
|
let sessionSeq = 0;
|
|
63
65
|
/**
|
|
64
66
|
* 由 initAdapters 根据配置调用。ttlMinutes≤0 时关闭空闲回收(仍受 MAX_ACTIVE_SESSIONS 限制)。
|
|
@@ -137,6 +139,35 @@ function isResult(msg) {
|
|
|
137
139
|
function isSessionCorruptionError(msg) {
|
|
138
140
|
return /session\s*(not found|expired|corrupt)|no\s*conversation\s*found/i.test(msg);
|
|
139
141
|
}
|
|
142
|
+
function appendStderrSnippet(previous, chunk) {
|
|
143
|
+
const next = previous + chunk;
|
|
144
|
+
if (next.length <= MAX_CAPTURED_STDERR_CHARS)
|
|
145
|
+
return next;
|
|
146
|
+
return next.slice(-MAX_CAPTURED_STDERR_CHARS);
|
|
147
|
+
}
|
|
148
|
+
function getUsefulStderrSnippet(stderr) {
|
|
149
|
+
const lines = stderr
|
|
150
|
+
.split(/\r?\n/)
|
|
151
|
+
.map((line) => line.trim())
|
|
152
|
+
.filter(Boolean);
|
|
153
|
+
if (lines.length === 0)
|
|
154
|
+
return undefined;
|
|
155
|
+
const summary = lines.slice(-3).join(' | ');
|
|
156
|
+
return summary.length <= MAX_EXPOSED_STDERR_CHARS
|
|
157
|
+
? summary
|
|
158
|
+
: summary.slice(0, MAX_EXPOSED_STDERR_CHARS - 3) + '...';
|
|
159
|
+
}
|
|
160
|
+
function enrichClaudeErrorMessage(message, stderr) {
|
|
161
|
+
const snippet = getUsefulStderrSnippet(stderr);
|
|
162
|
+
if (!snippet)
|
|
163
|
+
return message;
|
|
164
|
+
if (message.includes(snippet))
|
|
165
|
+
return message;
|
|
166
|
+
if (/process exited with code \d+/i.test(message) || /无输出/.test(message) || /未知错误/.test(message)) {
|
|
167
|
+
return `${message} stderr: ${snippet}`;
|
|
168
|
+
}
|
|
169
|
+
return message;
|
|
170
|
+
}
|
|
140
171
|
/**
|
|
141
172
|
* 获取或创建 SDKSession
|
|
142
173
|
* @param sessionId 已有的 sessionId,如果为 undefined 则创建新会话
|
|
@@ -145,7 +176,7 @@ function isSessionCorruptionError(msg) {
|
|
|
145
176
|
* @param permissionMode 权限模式
|
|
146
177
|
* @returns SDKSession 对象和实际的 sessionId
|
|
147
178
|
*/
|
|
148
|
-
async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
|
|
179
|
+
async function getOrCreateSession(sessionId, workDir, model, permissionMode, onStderr) {
|
|
149
180
|
// 刷新 Claude 环境变量(支持 cc switch 后无需重启即可生效)
|
|
150
181
|
refreshClaudeEnvToProcess();
|
|
151
182
|
const resolvedModel = model?.trim() || process.env.ANTHROPIC_MODEL?.trim() || 'claude-opus-4-5';
|
|
@@ -155,6 +186,7 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
|
|
|
155
186
|
const sessionOptions = {
|
|
156
187
|
model: resolvedModel,
|
|
157
188
|
permissionMode,
|
|
189
|
+
stderr: onStderr,
|
|
158
190
|
};
|
|
159
191
|
const baseUrl = process.env.ANTHROPIC_BASE_URL ?? '(default)';
|
|
160
192
|
log.info(`[V2] getOrCreateSession model param=${String(model ?? '')} resolved=${resolvedModel} baseUrl=${baseUrl} workDir=${workDir}`);
|
|
@@ -260,6 +292,7 @@ export class ClaudeSDKAdapter {
|
|
|
260
292
|
let pendingTempId; // 记录临时 ID,用于 abort 时清理
|
|
261
293
|
let runSettled = false;
|
|
262
294
|
let currentStream; // 用于 abort 时立即中断 stream
|
|
295
|
+
let recentStderr = '';
|
|
263
296
|
const permissionMode = options?.skipPermissions
|
|
264
297
|
? 'bypassPermissions'
|
|
265
298
|
: options?.permissionMode === 'acceptEdits'
|
|
@@ -279,7 +312,9 @@ export class ClaudeSDKAdapter {
|
|
|
279
312
|
log.info(`[V2] Session: ${sessionId ?? 'new'}, prompt="${prompt.slice(0, 50)}..."`);
|
|
280
313
|
log.info(`[V2] model param=${String(options?.model ?? '')} baseUrl=${process.env.ANTHROPIC_BASE_URL ?? '(default)'}`);
|
|
281
314
|
// 获取或创建会话
|
|
282
|
-
const { session, sessionId: returnedId } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode)
|
|
315
|
+
const { session, sessionId: returnedId } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode, (data) => {
|
|
316
|
+
recentStderr = appendStderrSnippet(recentStderr, data);
|
|
317
|
+
});
|
|
283
318
|
if (returnedId.startsWith('pending-')) {
|
|
284
319
|
pendingTempId = returnedId;
|
|
285
320
|
}
|
|
@@ -380,7 +415,7 @@ export class ClaudeSDKAdapter {
|
|
|
380
415
|
}
|
|
381
416
|
callbacks.onSessionInvalid?.();
|
|
382
417
|
}
|
|
383
|
-
const errMsg = errs[0] || '未知错误';
|
|
418
|
+
const errMsg = enrichClaudeErrorMessage(errs[0] || '未知错误', recentStderr);
|
|
384
419
|
callbacks.onError(errMsg);
|
|
385
420
|
return;
|
|
386
421
|
}
|
|
@@ -425,7 +460,7 @@ export class ClaudeSDKAdapter {
|
|
|
425
460
|
// 流结束但无 result 也无 accumulated:必须触发回调,否则 Promise 永远挂起
|
|
426
461
|
log.warn('Stream ended with no result and no accumulated text, calling onError to prevent stuck state');
|
|
427
462
|
runSettled = true;
|
|
428
|
-
callbacks.onError('AI 响应异常结束(无输出),请重试');
|
|
463
|
+
callbacks.onError(enrichClaudeErrorMessage('AI 响应异常结束(无输出),请重试', recentStderr));
|
|
429
464
|
}
|
|
430
465
|
}
|
|
431
466
|
}
|
|
@@ -447,7 +482,7 @@ export class ClaudeSDKAdapter {
|
|
|
447
482
|
}
|
|
448
483
|
runSettled = true;
|
|
449
484
|
const errorObj = err;
|
|
450
|
-
const msg = errorObj.message || String(err);
|
|
485
|
+
const msg = enrichClaudeErrorMessage(errorObj.message || String(err), recentStderr);
|
|
451
486
|
log.error(`Claude SDK V2 error: ${msg}`);
|
|
452
487
|
if (errorObj.stack) {
|
|
453
488
|
log.error(`Error stack: ${errorObj.stack}`);
|
|
@@ -25,14 +25,12 @@ export class CodeBuddyAdapter {
|
|
|
25
25
|
},
|
|
26
26
|
onError: (err) => {
|
|
27
27
|
const msg = typeof err === 'string' ? err : String(err);
|
|
28
|
-
const friendly = msg.includes('
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
? 'CodeBuddy 会话已失效,旧 session 已清理。请直接重试当前请求。'
|
|
35
|
-
: msg;
|
|
28
|
+
const friendly = msg.includes('No conversation found') ||
|
|
29
|
+
msg.includes('Session not found') ||
|
|
30
|
+
msg.includes('Invalid session') ||
|
|
31
|
+
msg.includes('Unable to resume')
|
|
32
|
+
? 'CodeBuddy 会话已失效,旧 session 已清理。请直接重试当前请求。'
|
|
33
|
+
: msg;
|
|
36
34
|
callbacks.onError(friendly);
|
|
37
35
|
},
|
|
38
36
|
onSessionId: callbacks.onSessionId,
|
|
@@ -36,19 +36,11 @@ export class CodexAdapter {
|
|
|
36
36
|
},
|
|
37
37
|
onError: (err) => {
|
|
38
38
|
const msg = typeof err === "string" ? err : String(err);
|
|
39
|
-
const friendly = msg.includes("
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
msg.includes("ENOTFOUND") ||
|
|
45
|
-
msg.includes("ETIMEDOUT")
|
|
46
|
-
? "Codex 网络请求失败。如无法访问 chatgpt.com,请在 tools.codex.proxy 或 CODEX_PROXY 中配置代理。"
|
|
47
|
-
: msg.includes("No session found") ||
|
|
48
|
-
msg.includes("No conversation found") ||
|
|
49
|
-
msg.includes("Unable to find session")
|
|
50
|
-
? "Codex 会话已失效,旧 session 已清理。请直接重试当前请求。"
|
|
51
|
-
: msg;
|
|
39
|
+
const friendly = msg.includes("No session found") ||
|
|
40
|
+
msg.includes("No conversation found") ||
|
|
41
|
+
msg.includes("Unable to find session")
|
|
42
|
+
? "Codex 会话已失效,旧 session 已清理。请直接重试当前请求。"
|
|
43
|
+
: msg;
|
|
52
44
|
callbacks.onError(friendly);
|
|
53
45
|
},
|
|
54
46
|
onSessionId: callbacks.onSessionId,
|
package/dist/config/file-io.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export declare function loadClaudeSettingsEnv(): Record<string, string>;
|
|
|
14
14
|
export declare function saveClaudeSettingsEnv(env: Record<string, string>): void;
|
|
15
15
|
export declare function normalizeAiCommand(value: unknown, fallback: AiCommand): AiCommand;
|
|
16
16
|
export declare function hasCodexAuth(): boolean;
|
|
17
|
+
export declare function hasCodeBuddyAuthIndicators(): boolean;
|
|
18
|
+
export declare function getClaudeSdkRuntimeIssue(): string | null;
|
|
17
19
|
export declare function parseCommaSeparated(value: string): string[];
|
|
18
20
|
/**
|
|
19
21
|
* 将最新的 Claude 认证环境变量按优先级合并到 process.env。
|
package/dist/config/file-io.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, chmodSync } from 'node:fs';
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, chmodSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
2
3
|
import { join, dirname } from 'node:path';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
4
6
|
import { createLogger } from '../logger.js';
|
|
5
7
|
import { APP_HOME } from '../constants.js';
|
|
6
8
|
const log = createLogger('config');
|
|
@@ -10,12 +12,17 @@ export const CODEX_AUTH_PATHS = [
|
|
|
10
12
|
join(homedir(), '.config', 'codex', 'auth.json'),
|
|
11
13
|
join(homedir(), 'AppData', 'Roaming', 'codex', 'auth.json'),
|
|
12
14
|
];
|
|
15
|
+
const CODEBUDDY_HOME_PATHS = [
|
|
16
|
+
join(homedir(), '.codebuddy'),
|
|
17
|
+
join(homedir(), '.codebuddycn'),
|
|
18
|
+
];
|
|
13
19
|
const OLD_ROOT_KEYS = [
|
|
14
20
|
'claudeWorkDir',
|
|
15
21
|
'claudeTimeoutMs',
|
|
16
22
|
'claudeModel',
|
|
17
23
|
];
|
|
18
24
|
const AI_COMMANDS = ['claude', 'codex', 'codebuddy'];
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
19
26
|
/** Claude 认证相关的环境变量 key 列表 */
|
|
20
27
|
export const CLAUDE_AUTH_ENV_KEYS = [
|
|
21
28
|
'ANTHROPIC_API_KEY',
|
|
@@ -231,6 +238,84 @@ export function hasCodexAuth() {
|
|
|
231
238
|
}
|
|
232
239
|
});
|
|
233
240
|
}
|
|
241
|
+
export function hasCodeBuddyAuthIndicators() {
|
|
242
|
+
if (process.env.CODEBUDDY_API_KEY || process.env.CODEBUDDY_AUTH_TOKEN)
|
|
243
|
+
return true;
|
|
244
|
+
return CODEBUDDY_HOME_PATHS.some((base) => {
|
|
245
|
+
try {
|
|
246
|
+
if (!existsSync(base))
|
|
247
|
+
return false;
|
|
248
|
+
const settingsPath = join(base, 'settings.json');
|
|
249
|
+
if (existsSync(settingsPath)) {
|
|
250
|
+
try {
|
|
251
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
252
|
+
if (settings.apiKeyHelper)
|
|
253
|
+
return true;
|
|
254
|
+
const env = settings.env;
|
|
255
|
+
if (env && typeof env === 'object' && !Array.isArray(env)) {
|
|
256
|
+
const envRecord = env;
|
|
257
|
+
if (envRecord.CODEBUDDY_API_KEY || envRecord.CODEBUDDY_AUTH_TOKEN)
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Ignore malformed settings and keep checking other indicators.
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const localStorageDir = join(base, 'local_storage');
|
|
266
|
+
if (!existsSync(localStorageDir))
|
|
267
|
+
return false;
|
|
268
|
+
return readdirSync(localStorageDir)
|
|
269
|
+
.filter((name) => name.endsWith('.info'))
|
|
270
|
+
.some((name) => {
|
|
271
|
+
try {
|
|
272
|
+
const content = readFileSync(join(localStorageDir, name), 'utf-8');
|
|
273
|
+
return /"userId"\s*:|"nickname"\s*:|"enterpriseName"\s*:|"accessToken"\s*:/.test(content);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
export function getClaudeSdkRuntimeIssue() {
|
|
286
|
+
let cliPath;
|
|
287
|
+
try {
|
|
288
|
+
const sdkEntry = require.resolve('@anthropic-ai/claude-agent-sdk');
|
|
289
|
+
const sdkDir = dirname(sdkEntry);
|
|
290
|
+
cliPath = join(sdkDir, 'cli.js');
|
|
291
|
+
if (!existsSync(cliPath)) {
|
|
292
|
+
return `Claude SDK 安装不完整:缺少 ${cliPath}。请重新安装依赖后再启动。`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
return `未找到 @anthropic-ai/claude-agent-sdk:${error instanceof Error ? error.message : String(error)}`;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
execFileSync(process.execPath, [cliPath, '--version'], {
|
|
300
|
+
stdio: 'pipe',
|
|
301
|
+
env: {
|
|
302
|
+
...process.env,
|
|
303
|
+
CLAUDE_CODE_ENTRYPOINT: process.env.CLAUDE_CODE_ENTRYPOINT || 'sdk-ts',
|
|
304
|
+
},
|
|
305
|
+
timeout: 5000,
|
|
306
|
+
windowsHide: process.platform === 'win32',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
const execError = error;
|
|
311
|
+
const stderr = Buffer.isBuffer(execError.stderr)
|
|
312
|
+
? execError.stderr.toString('utf-8')
|
|
313
|
+
: execError.stderr;
|
|
314
|
+
const details = stderr?.trim() || execError.message || String(error);
|
|
315
|
+
return `Claude SDK 运行时不可用:${details}`;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
234
319
|
export function parseCommaSeparated(value) {
|
|
235
320
|
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
236
321
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { Platform, AiCommand, Config, FilePlatformTelegram, FilePlatformFeishu, FilePlatformQQ, FilePlatformWechat, FilePlatformWework, FilePlatformDingtalk, FilePlatformWorkBuddy, FileToolClaude, FileToolCodex, FileToolCodeBuddy, FileConfig, } from './config/types.js';
|
|
2
2
|
import type { Platform, AiCommand, Config } from './config/types.js';
|
|
3
|
-
export { CONFIG_PATH, loadFileConfig, saveFileConfig, getClaudeConfigHome, loadClaudeSettingsEnv, saveClaudeSettingsEnv, normalizeAiCommand, hasCodexAuth, parseCommaSeparated, CLAUDE_AUTH_ENV_KEYS, refreshClaudeEnvToProcess, processEnvForNonClaudeCliChild, CODEX_AUTH_PATHS, } from './config/file-io.js';
|
|
3
|
+
export { CONFIG_PATH, loadFileConfig, saveFileConfig, getClaudeConfigHome, getClaudeSdkRuntimeIssue, hasCodeBuddyAuthIndicators, loadClaudeSettingsEnv, saveClaudeSettingsEnv, normalizeAiCommand, hasCodexAuth, parseCommaSeparated, CLAUDE_AUTH_ENV_KEYS, refreshClaudeEnvToProcess, processEnvForNonClaudeCliChild, CODEX_AUTH_PATHS, } from './config/file-io.js';
|
|
4
4
|
/** 检测是否需要交互式配置(无 token 且无环境变量) */
|
|
5
5
|
export declare function needsSetup(): boolean;
|
|
6
6
|
export declare function loadConfig(): Config;
|
package/dist/config.js
CHANGED
|
@@ -27,8 +27,8 @@ function resolveFilePlatformAi(file, platform) {
|
|
|
27
27
|
return normalizeAiCommand(raw ?? process.env.AI_COMMAND ?? file.aiCommand, 'claude');
|
|
28
28
|
}
|
|
29
29
|
// Re-export file I/O and credential helpers from sub-modules
|
|
30
|
-
export { CONFIG_PATH, loadFileConfig, saveFileConfig, getClaudeConfigHome, loadClaudeSettingsEnv, saveClaudeSettingsEnv, normalizeAiCommand, hasCodexAuth, parseCommaSeparated, CLAUDE_AUTH_ENV_KEYS, refreshClaudeEnvToProcess, processEnvForNonClaudeCliChild, CODEX_AUTH_PATHS, } from './config/file-io.js';
|
|
31
|
-
import { loadFileConfig, normalizeAiCommand, hasCodexAuth, parseCommaSeparated, loadClaudeSettingsEnv, } from './config/file-io.js';
|
|
30
|
+
export { CONFIG_PATH, loadFileConfig, saveFileConfig, getClaudeConfigHome, getClaudeSdkRuntimeIssue, hasCodeBuddyAuthIndicators, loadClaudeSettingsEnv, saveClaudeSettingsEnv, normalizeAiCommand, hasCodexAuth, parseCommaSeparated, CLAUDE_AUTH_ENV_KEYS, refreshClaudeEnvToProcess, processEnvForNonClaudeCliChild, CODEX_AUTH_PATHS, } from './config/file-io.js';
|
|
31
|
+
import { getClaudeSdkRuntimeIssue, hasCodeBuddyAuthIndicators, loadFileConfig, normalizeAiCommand, hasCodexAuth, parseCommaSeparated, loadClaudeSettingsEnv, } from './config/file-io.js';
|
|
32
32
|
/** 检测是否需要交互式配置(无 token 且无环境变量) */
|
|
33
33
|
export function needsSetup() {
|
|
34
34
|
// 环境变量已提供任一平台的凭证,则认为已配置
|
|
@@ -275,6 +275,10 @@ export function loadConfig() {
|
|
|
275
275
|
].join('\n');
|
|
276
276
|
throw new Error(errorMsg);
|
|
277
277
|
}
|
|
278
|
+
const claudeRuntimeIssue = getClaudeSdkRuntimeIssue();
|
|
279
|
+
if (claudeRuntimeIssue) {
|
|
280
|
+
throw new Error(claudeRuntimeIssue);
|
|
281
|
+
}
|
|
278
282
|
}
|
|
279
283
|
// 7. 校验 Codex CLI(任一已启用渠道使用 codex 时)
|
|
280
284
|
if (toolsNeeded.has('codex')) {
|
|
@@ -353,6 +357,10 @@ export function loadConfig() {
|
|
|
353
357
|
throw new Error(installGuide);
|
|
354
358
|
}
|
|
355
359
|
}
|
|
360
|
+
if (!hasCodeBuddyAuthIndicators()) {
|
|
361
|
+
log.warn('CodeBuddy 模式:未检测到明确的登录态或 API Key。首次使用请先运行 codebuddy login;' +
|
|
362
|
+
'若使用自定义 Token,请在 CodeBuddy 设置或环境变量中配置。');
|
|
363
|
+
}
|
|
356
364
|
}
|
|
357
365
|
// 7. 日志与平台配置
|
|
358
366
|
const logDir = process.env.LOG_DIR ?? file.logDir ?? join(APP_HOME, 'logs');
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -274,7 +274,9 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
274
274
|
resolve();
|
|
275
275
|
},
|
|
276
276
|
}, {
|
|
277
|
-
model:
|
|
277
|
+
model: aiCommand === 'claude'
|
|
278
|
+
? (sessionManager.getModel(ctx.userId, ctx.threadId) ?? config.claudeModel)
|
|
279
|
+
: undefined,
|
|
278
280
|
chatId: ctx.chatId,
|
|
279
281
|
// 默认跳过权限确认,保持全自动执行(可通过 config 或环境变量关闭)
|
|
280
282
|
skipPermissions: config.skipPermissions ?? true,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WorkBuddy Centrifuge Client - WebSocket connection for WeChat KF messages
|
|
3
3
|
*/
|
|
4
|
-
import { Centrifuge } from 'centrifuge';
|
|
4
|
+
import { Centrifuge, SubscriptionState, } from 'centrifuge';
|
|
5
5
|
import { WebSocket } from 'ws';
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { createLogger } from '../logger.js';
|
|
@@ -17,7 +17,7 @@ export class WorkBuddyCentrifugeClient {
|
|
|
17
17
|
callbacks;
|
|
18
18
|
client = null;
|
|
19
19
|
sub = null;
|
|
20
|
-
extraSubs =
|
|
20
|
+
extraSubs = new Map();
|
|
21
21
|
state = 'disconnected';
|
|
22
22
|
processedMsgIds = new Set();
|
|
23
23
|
consecutiveErrors = 0;
|
|
@@ -94,10 +94,10 @@ export class WorkBuddyCentrifugeClient {
|
|
|
94
94
|
log.info(`${this.logPrefix} Stopping...`);
|
|
95
95
|
this.state = 'disconnected';
|
|
96
96
|
this.processedMsgIds.clear();
|
|
97
|
-
for (const sub of this.extraSubs) {
|
|
97
|
+
for (const sub of this.extraSubs.values()) {
|
|
98
98
|
sub.unsubscribe();
|
|
99
99
|
}
|
|
100
|
-
this.extraSubs
|
|
100
|
+
this.extraSubs.clear();
|
|
101
101
|
if (this.sub) {
|
|
102
102
|
this.sub.unsubscribe();
|
|
103
103
|
this.sub = null;
|
|
@@ -119,8 +119,28 @@ export class WorkBuddyCentrifugeClient {
|
|
|
119
119
|
log.warn(`${this.logPrefix} Cannot subscribe: client not initialized`);
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
|
+
const existing = this.extraSubs.get(channel);
|
|
123
|
+
if (existing) {
|
|
124
|
+
if (existing.state === SubscriptionState.Unsubscribed) {
|
|
125
|
+
log.info(`${this.logPrefix} Re-subscribing to additional channel: ${channel}`);
|
|
126
|
+
existing.subscribe();
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
log.debug(`${this.logPrefix} Additional channel already tracked: ${channel} (state=${existing.state})`);
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
122
133
|
log.info(`${this.logPrefix} Subscribing to additional channel: ${channel}`);
|
|
123
|
-
|
|
134
|
+
let sub;
|
|
135
|
+
try {
|
|
136
|
+
sub = this.client.newSubscription(channel, { token: subscriptionToken });
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
140
|
+
log.error(`${this.logPrefix} Failed to create extra subscription (${channel}): ${msg}`);
|
|
141
|
+
this.callbacks.onError?.(error instanceof Error ? error : new Error(msg));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
124
144
|
sub.on('publication', (ctx) => {
|
|
125
145
|
this.handlePublication(ctx.data);
|
|
126
146
|
});
|
|
@@ -130,7 +150,12 @@ export class WorkBuddyCentrifugeClient {
|
|
|
130
150
|
sub.on('subscribed', () => {
|
|
131
151
|
log.info(`${this.logPrefix} Extra channel subscribed: ${channel}`);
|
|
132
152
|
});
|
|
133
|
-
|
|
153
|
+
sub.on('unsubscribed', () => {
|
|
154
|
+
if (this.extraSubs.get(channel) === sub) {
|
|
155
|
+
this.extraSubs.delete(channel);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
this.extraSubs.set(channel, sub);
|
|
134
159
|
sub.subscribe();
|
|
135
160
|
}
|
|
136
161
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wu529778790/open-im",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.9-beta.0",
|
|
4
4
|
"description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
|
|
55
|
+
"@aws-sdk/client-s3": "^3.1035.0",
|
|
55
56
|
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
56
57
|
"centrifuge": "^5.5.3",
|
|
57
58
|
"dingtalk-stream": "^2.1.4",
|