@wu529778790/open-im 1.8.1-beta.2 → 1.8.1-beta.21
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/access/access-control.js +1 -1
- package/dist/adapters/claude-sdk-adapter.js +94 -36
- package/dist/channels/capabilities.js +5 -0
- package/dist/cli.js +5 -2
- package/dist/commands/handler.d.ts +1 -2
- package/dist/commands/handler.js +6 -18
- package/dist/config-web-page-i18n.d.ts +12 -0
- package/dist/config-web-page-i18n.js +12 -0
- package/dist/config-web-page-script.js +1 -0
- package/dist/config-web-page-template.js +48 -1
- package/dist/config-web.js +110 -7
- package/dist/config.d.ts +25 -1
- package/dist/config.js +46 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/dingtalk/client.js +11 -3
- package/dist/dingtalk/event-handler.js +18 -3
- package/dist/dingtalk/message-sender.js +13 -0
- package/dist/feishu/event-handler.js +144 -10
- package/dist/index.js +26 -2
- package/dist/manager-control.js +7 -0
- package/dist/qq/client.js +111 -88
- package/dist/qq/event-handler.js +16 -2
- package/dist/qq/message-sender.js +11 -0
- package/dist/service-control.js +4 -0
- package/dist/session/session-manager.js +11 -1
- package/dist/setup.js +2 -1
- package/dist/shared/active-chats.d.ts +2 -2
- package/dist/shared/ai-task.js +13 -1
- package/dist/shared/chat-user-map.js +11 -0
- package/dist/shared/media-storage.js +27 -0
- package/dist/telegram/client.js +25 -3
- package/dist/telegram/event-handler.js +44 -8
- package/dist/telegram/message-sender.js +13 -0
- package/dist/wechat/auth/qclaw-api.js +1 -1
- package/dist/wechat/client.js +81 -4
- package/dist/wechat/event-handler.js +10 -3
- package/dist/wework/client.js +36 -14
- package/dist/wework/event-handler.js +39 -4
- package/dist/wework/message-sender.js +53 -21
- package/dist/workbuddy/centrifuge-client.d.ts +74 -0
- package/dist/workbuddy/centrifuge-client.js +272 -0
- package/dist/workbuddy/client.d.ts +27 -0
- package/dist/workbuddy/client.js +162 -0
- package/dist/workbuddy/event-handler.d.ts +11 -0
- package/dist/workbuddy/event-handler.js +118 -0
- package/dist/workbuddy/index.d.ts +8 -0
- package/dist/workbuddy/index.js +8 -0
- package/dist/workbuddy/message-sender.d.ts +16 -0
- package/dist/workbuddy/message-sender.js +51 -0
- package/dist/workbuddy/oauth.d.ts +114 -0
- package/dist/workbuddy/oauth.js +310 -0
- package/dist/workbuddy/types.d.ts +86 -0
- package/dist/workbuddy/types.js +4 -0
- package/package.json +4 -2
|
@@ -17,6 +17,99 @@ import { buildSavedMediaPrompt } from '../shared/media-analysis-prompt.js';
|
|
|
17
17
|
import { buildMediaContext } from '../shared/media-context.js';
|
|
18
18
|
import { buildProgressNote } from '../shared/message-note.js';
|
|
19
19
|
const log = createLogger('FeishuHandler');
|
|
20
|
+
/**
|
|
21
|
+
* 从异常中提取飞书 API 错误码
|
|
22
|
+
*/
|
|
23
|
+
function extractFeishuErrorCode(err) {
|
|
24
|
+
const e = err;
|
|
25
|
+
if (e?.response?.data?.code)
|
|
26
|
+
return e.response.data.code;
|
|
27
|
+
if (e?.code)
|
|
28
|
+
return e.code;
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 根据错误码判断是否为权限不足
|
|
33
|
+
*/
|
|
34
|
+
function isPermissionError(err) {
|
|
35
|
+
const code = extractFeishuErrorCode(err);
|
|
36
|
+
if (!code) {
|
|
37
|
+
// 非标准错误:检查 message 中是否包含权限关键词
|
|
38
|
+
const msg = err?.message ?? String(err);
|
|
39
|
+
return /permission|权限|scope|not authorized|no access|forbidden/i.test(msg);
|
|
40
|
+
}
|
|
41
|
+
// 飞书常见权限错误码
|
|
42
|
+
return [
|
|
43
|
+
99991400, // 权限不足
|
|
44
|
+
99991401, // 没有API权限
|
|
45
|
+
99991663, // 应用未获取 scope
|
|
46
|
+
99991672, // 应用未开通相关能力
|
|
47
|
+
99991670, // 应用未上架/未授权
|
|
48
|
+
99991668, // 应用可见性限制
|
|
49
|
+
].includes(code);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 构建飞书权限配置指引消息
|
|
53
|
+
*/
|
|
54
|
+
function buildPermissionGuideMessage(err) {
|
|
55
|
+
const code = extractFeishuErrorCode(err);
|
|
56
|
+
const codeHint = code ? ` (错误码: ${code})` : '';
|
|
57
|
+
return [
|
|
58
|
+
'⚠️ **飞书应用权限不足,无法发送消息**' + codeHint,
|
|
59
|
+
'',
|
|
60
|
+
'请按以下步骤开通所需权限:',
|
|
61
|
+
'',
|
|
62
|
+
'**1. 进入飞书开放平台**',
|
|
63
|
+
'👉 https://open.feishu.cn/app',
|
|
64
|
+
'',
|
|
65
|
+
'**2. 找到你的应用,进入「权限管理」**',
|
|
66
|
+
'',
|
|
67
|
+
'**3. 开通以下权限(搜索权限名称添加):**',
|
|
68
|
+
'• `im:message` — 获取与发送单聊、群组消息',
|
|
69
|
+
'• `im:message:send_as_bot` — 以应用身份发消息',
|
|
70
|
+
'• `im:resource` — 获取与上传图片或文件资源',
|
|
71
|
+
'• `im:chat` — 获取群组信息',
|
|
72
|
+
'',
|
|
73
|
+
'**4. 如需使用卡片打字机效果,还需开通:**',
|
|
74
|
+
'• `cardkit:card` — CardKit 卡片管理',
|
|
75
|
+
'',
|
|
76
|
+
'**5. 发布版本**',
|
|
77
|
+
'权限修改后需点击「创建版本」→「发布」,管理员审批后生效。',
|
|
78
|
+
'',
|
|
79
|
+
'📖 详细文档:https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO04yNxkDN',
|
|
80
|
+
].join('\n');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 发送权限错误提示,依次尝试:卡片 → 纯文本 → open_id
|
|
84
|
+
*/
|
|
85
|
+
async function sendPermissionFallback(chatId, guide) {
|
|
86
|
+
// 1. 先尝试 sendTextReply(发卡片消息)
|
|
87
|
+
try {
|
|
88
|
+
await sendTextReply(chatId, guide);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
log.warn('Card-based reply failed, falling back to plain text:', err);
|
|
93
|
+
}
|
|
94
|
+
// 2. 降级为纯文本消息
|
|
95
|
+
try {
|
|
96
|
+
const client = (await import('./client.js')).getClient();
|
|
97
|
+
const plainGuide = guide.replace(/\*\*/g, '').replace(/`/g, '');
|
|
98
|
+
await client.im.message.create({
|
|
99
|
+
data: {
|
|
100
|
+
receive_id: chatId,
|
|
101
|
+
msg_type: 'text',
|
|
102
|
+
content: JSON.stringify({ text: plainGuide }),
|
|
103
|
+
},
|
|
104
|
+
params: { receive_id_type: 'chat_id' },
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
log.warn('Plain text reply also failed:', err);
|
|
110
|
+
}
|
|
111
|
+
log.error('All fallback methods failed to send permission guide');
|
|
112
|
+
}
|
|
20
113
|
async function downloadFeishuMessageResource(client, messageId, fileKey, type, options) {
|
|
21
114
|
const targetPath = createMediaTargetPath(options?.fallbackExtension ?? 'bin', options?.basenameHint ?? fileKey);
|
|
22
115
|
const response = await client.im.messageResource.get({
|
|
@@ -57,19 +150,59 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
57
150
|
const toolId = aiCommand;
|
|
58
151
|
// 使用 CardKit 打字机效果(80ms 节流,约 12 次/秒,比 patch 5 QPS 更流畅)
|
|
59
152
|
let cardHandle;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
153
|
+
const MAX_SEND_RETRIES = 3;
|
|
154
|
+
for (let attempt = 1; attempt <= MAX_SEND_RETRIES; attempt++) {
|
|
155
|
+
try {
|
|
156
|
+
cardHandle = await sendThinkingCard(chatId, toolId);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
const isRetryable = err && typeof err === 'object' && 'code' in err &&
|
|
161
|
+
(err.code === 'ETIMEDOUT' || err.code === 'ECONNRESET' || err.code === 'ECONNREFUSED');
|
|
162
|
+
if (isRetryable && attempt < MAX_SEND_RETRIES) {
|
|
163
|
+
log.warn(`sendThinkingCard attempt ${attempt}/${MAX_SEND_RETRIES} failed (${err.code}), retrying...`);
|
|
164
|
+
await new Promise((r) => setTimeout(r, 1000 * attempt));
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
log.error(`Failed to send thinking card after ${attempt} attempts:`, err);
|
|
168
|
+
// 检测是否为飞书权限不足
|
|
169
|
+
if (isPermissionError(err)) {
|
|
170
|
+
const guide = buildPermissionGuideMessage(err);
|
|
171
|
+
await sendPermissionFallback(chatId, guide).catch((err) => {
|
|
172
|
+
log.warn('Permission fallback send failed:', err);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
try {
|
|
177
|
+
await sendTextReply(chatId, '启动 AI 处理失败,请重试。');
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
log.warn('Failed to send startup error reply:', err);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
66
185
|
}
|
|
67
186
|
const { messageId: msgId, cardId } = cardHandle;
|
|
68
187
|
const stopTyping = startTypingLoop(chatId);
|
|
69
188
|
const taskKey = `${userId}:${cardId}`;
|
|
189
|
+
let consecutiveStreamErrors = 0;
|
|
190
|
+
const MAX_STREAM_ERRORS = 5;
|
|
70
191
|
const streamUpdate = (content, toolNote) => {
|
|
192
|
+
if (consecutiveStreamErrors >= MAX_STREAM_ERRORS)
|
|
193
|
+
return; // 停止尝试
|
|
71
194
|
const note = buildProgressNote(toolNote);
|
|
72
|
-
streamContentUpdate(cardId, content, note).
|
|
195
|
+
streamContentUpdate(cardId, content, note).then(() => {
|
|
196
|
+
consecutiveStreamErrors = 0;
|
|
197
|
+
}).catch((e) => {
|
|
198
|
+
consecutiveStreamErrors++;
|
|
199
|
+
if (consecutiveStreamErrors >= MAX_STREAM_ERRORS) {
|
|
200
|
+
log.warn(`Stream update failed ${consecutiveStreamErrors} times consecutively, giving up: ${e?.message ?? e}`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
log.debug('Stream update failed (will retry on next update):', e?.message ?? e);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
73
206
|
};
|
|
74
207
|
await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'feishu', taskKey }, prompt, toolAdapter, {
|
|
75
208
|
throttleMs: CARDKIT_THROTTLE_MS,
|
|
@@ -105,7 +238,8 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
105
238
|
try {
|
|
106
239
|
obj = JSON.parse(raw);
|
|
107
240
|
}
|
|
108
|
-
catch {
|
|
241
|
+
catch (err) {
|
|
242
|
+
log.debug('Failed to parse action value as JSON:', err);
|
|
109
243
|
return null;
|
|
110
244
|
}
|
|
111
245
|
}
|
|
@@ -158,8 +292,8 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
158
292
|
}
|
|
159
293
|
actionData = parsed;
|
|
160
294
|
}
|
|
161
|
-
catch {
|
|
162
|
-
|
|
295
|
+
catch (err) {
|
|
296
|
+
log.debug('Failed to parse card action data:', err);
|
|
163
297
|
}
|
|
164
298
|
if (actionData?.action === 'stop' && actionData.card_id) {
|
|
165
299
|
const cardId = actionData.card_id;
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,8 @@ import { setupWeWorkHandlers } from "./wework/event-handler.js";
|
|
|
23
23
|
import { sendProactiveTextReply as sendWeWorkTextReply } from "./wework/message-sender.js";
|
|
24
24
|
import { initDingTalk, stopDingTalk, formatDingTalkInitError } from "./dingtalk/client.js";
|
|
25
25
|
import { setupDingTalkHandlers } from "./dingtalk/event-handler.js";
|
|
26
|
+
import { initWorkBuddy, stopWorkBuddy } from "./workbuddy/client.js";
|
|
27
|
+
import { setupWorkBuddyHandlers } from "./workbuddy/event-handler.js";
|
|
26
28
|
import { initAdapters, cleanupAdapters } from "./adapters/registry.js";
|
|
27
29
|
import { SessionManager } from "./session/session-manager.js";
|
|
28
30
|
import { loadActiveChats, getActiveChatId, flushActiveChats, } from "./shared/active-chats.js";
|
|
@@ -34,8 +36,8 @@ const require = createRequire(import.meta.url);
|
|
|
34
36
|
const { version: APP_VERSION } = require("../package.json");
|
|
35
37
|
const log = createLogger("Main");
|
|
36
38
|
async function sendLifecycleNotification(platform, message) {
|
|
37
|
-
// DingTalk 不支持主动发消息(OpenAPI 需 robotCode 等,易报 robot 不存在),跳过启动/关闭通知
|
|
38
|
-
if (platform === "dingtalk")
|
|
39
|
+
// DingTalk 和 WorkBuddy 不支持主动发消息(OpenAPI 需 robotCode 等,易报 robot 不存在),跳过启动/关闭通知
|
|
40
|
+
if (platform === "dingtalk" || platform === "workbuddy")
|
|
39
41
|
return;
|
|
40
42
|
const telegramChatId = getActiveChatId("telegram");
|
|
41
43
|
const feishuChatId = getActiveChatId("feishu");
|
|
@@ -174,6 +176,7 @@ export async function main() {
|
|
|
174
176
|
let wechatHandle = null;
|
|
175
177
|
let weworkHandle = null;
|
|
176
178
|
let dingtalkHandle = null;
|
|
179
|
+
let workbuddyHandle = null;
|
|
177
180
|
// Track successfully initialized platforms
|
|
178
181
|
const successfulPlatforms = [];
|
|
179
182
|
if (config.enabledPlatforms.includes("telegram")) {
|
|
@@ -237,6 +240,16 @@ export async function main() {
|
|
|
237
240
|
log.error("Failed to initialize DingTalk:", formatDingTalkInitError(err));
|
|
238
241
|
}
|
|
239
242
|
}
|
|
243
|
+
if (config.enabledPlatforms.includes("workbuddy")) {
|
|
244
|
+
try {
|
|
245
|
+
workbuddyHandle = setupWorkBuddyHandlers(config, sessionManager);
|
|
246
|
+
await initWorkBuddy(config, workbuddyHandle.handleEvent);
|
|
247
|
+
successfulPlatforms.push("workbuddy");
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
log.error("Failed to initialize WorkBuddy:", err);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
240
253
|
// Require at least one platform to start successfully
|
|
241
254
|
if (successfulPlatforms.length === 0) {
|
|
242
255
|
throw new Error("No platforms initialized successfully. Service cannot start.");
|
|
@@ -283,6 +296,8 @@ export async function main() {
|
|
|
283
296
|
stopWeWork();
|
|
284
297
|
dingtalkHandle?.stop();
|
|
285
298
|
stopDingTalk();
|
|
299
|
+
workbuddyHandle?.stop();
|
|
300
|
+
stopWorkBuddy();
|
|
286
301
|
sessionManager.destroy();
|
|
287
302
|
cleanupAdapters();
|
|
288
303
|
flushActiveChats();
|
|
@@ -291,6 +306,15 @@ export async function main() {
|
|
|
291
306
|
};
|
|
292
307
|
process.on("SIGINT", () => shutdown().catch(() => process.exit(1)));
|
|
293
308
|
process.on("SIGTERM", () => shutdown().catch(() => process.exit(1)));
|
|
309
|
+
// Global error handlers to prevent unhandled crashes
|
|
310
|
+
process.on("unhandledRejection", (reason) => {
|
|
311
|
+
log.error("Unhandled Promise rejection:", reason);
|
|
312
|
+
});
|
|
313
|
+
process.on("uncaughtException", (err) => {
|
|
314
|
+
log.error("Uncaught exception (process will exit):", err);
|
|
315
|
+
closeLogger();
|
|
316
|
+
process.exit(1);
|
|
317
|
+
});
|
|
294
318
|
}
|
|
295
319
|
const isEntry = process.argv[1]?.replace(/\\/g, "/").endsWith("/index.js") ||
|
|
296
320
|
process.argv[1]?.replace(/\\/g, "/").endsWith("/index.ts");
|
package/dist/manager-control.js
CHANGED
|
@@ -6,6 +6,10 @@ import { APP_HOME } from "./constants.js";
|
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
const PID_FILE = join(APP_HOME, "open-im.pid");
|
|
8
8
|
const READY_FILE = join(APP_HOME, "open-im.ready");
|
|
9
|
+
function logError(prefix, err) {
|
|
10
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11
|
+
process.stderr.write(`[manager-control] ${prefix} ${msg}\n`);
|
|
12
|
+
}
|
|
9
13
|
function getManagerEntry() {
|
|
10
14
|
const extension = extname(fileURLToPath(import.meta.url));
|
|
11
15
|
if (extension === ".ts") {
|
|
@@ -106,6 +110,9 @@ export async function startManagerProcess(cwd) {
|
|
|
106
110
|
env: process.env,
|
|
107
111
|
windowsHide: process.platform === "win32",
|
|
108
112
|
});
|
|
113
|
+
child.on("error", (err) => {
|
|
114
|
+
logError("Manager process spawn failed:", err);
|
|
115
|
+
});
|
|
109
116
|
child.unref();
|
|
110
117
|
if (!child.pid) {
|
|
111
118
|
throw new Error("Failed to start manager process.");
|
package/dist/qq/client.js
CHANGED
|
@@ -17,6 +17,7 @@ let stopped = false;
|
|
|
17
17
|
let seq = null;
|
|
18
18
|
let sessionId = null;
|
|
19
19
|
let reconnectAttempt = 0;
|
|
20
|
+
let connecting = false; // 防止并发 connectWebSocket
|
|
20
21
|
let currentConfig = null;
|
|
21
22
|
let currentHandler = null;
|
|
22
23
|
let tokenState = null;
|
|
@@ -143,103 +144,125 @@ function startHeartbeat(intervalMs) {
|
|
|
143
144
|
heartbeatTimer = setInterval(() => {
|
|
144
145
|
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
145
146
|
return;
|
|
146
|
-
|
|
147
|
+
try {
|
|
148
|
+
ws.send(JSON.stringify({ op: 1, d: seq }));
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
log.warn('QQ heartbeat send failed:', err);
|
|
152
|
+
}
|
|
147
153
|
}, intervalMs);
|
|
148
154
|
}
|
|
149
155
|
async function connectWebSocket(config, handler) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (typeof payload.s === "number")
|
|
170
|
-
seq = payload.s;
|
|
171
|
-
if (payload.op === 10) {
|
|
172
|
-
const heartbeatInterval = Number(payload.d?.heartbeat_interval ?? 30000);
|
|
173
|
-
startHeartbeat(heartbeatInterval);
|
|
174
|
-
socket.send(JSON.stringify({
|
|
175
|
-
op: sessionId ? 6 : 2,
|
|
176
|
-
d: sessionId
|
|
177
|
-
? {
|
|
178
|
-
token: `QQBot ${token}`,
|
|
179
|
-
session_id: sessionId,
|
|
180
|
-
seq,
|
|
181
|
-
}
|
|
182
|
-
: {
|
|
183
|
-
token: `QQBot ${token}`,
|
|
184
|
-
intents: INTENTS.GROUP_AND_C2C |
|
|
185
|
-
INTENTS.DIRECT_MESSAGE |
|
|
186
|
-
INTENTS.PUBLIC_GUILD_MESSAGES,
|
|
187
|
-
properties: {
|
|
188
|
-
os: process.platform,
|
|
189
|
-
browser: "open-im",
|
|
190
|
-
device: "open-im",
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
}));
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (payload.op === 0 && payload.t === "READY") {
|
|
197
|
-
sessionId = String(payload.d?.session_id ?? "");
|
|
198
|
-
settle(resolve);
|
|
156
|
+
// 防止并发连接
|
|
157
|
+
if (connecting) {
|
|
158
|
+
log.warn("QQ gateway connection already in progress");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
connecting = true;
|
|
162
|
+
try {
|
|
163
|
+
const gatewayUrl = await getGatewayUrl(config);
|
|
164
|
+
const token = await fetchAccessToken(config);
|
|
165
|
+
await new Promise((resolve, reject) => {
|
|
166
|
+
const socket = new WebSocket(gatewayUrl);
|
|
167
|
+
ws = socket;
|
|
168
|
+
let settled = false;
|
|
169
|
+
let readyTimeoutId = setTimeout(() => {
|
|
170
|
+
readyTimeoutId = null;
|
|
171
|
+
settle(() => reject(new Error("QQ gateway ready timeout")));
|
|
172
|
+
}, 15000);
|
|
173
|
+
const settle = (fn) => {
|
|
174
|
+
if (settled)
|
|
199
175
|
return;
|
|
176
|
+
settled = true;
|
|
177
|
+
if (readyTimeoutId) {
|
|
178
|
+
clearTimeout(readyTimeoutId);
|
|
179
|
+
readyTimeoutId = null;
|
|
200
180
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
181
|
+
fn();
|
|
182
|
+
};
|
|
183
|
+
socket.on("open", () => {
|
|
184
|
+
log.info("QQ gateway connected");
|
|
185
|
+
reconnectAttempt = 0;
|
|
186
|
+
});
|
|
187
|
+
socket.on("message", async (raw) => {
|
|
188
|
+
try {
|
|
189
|
+
const payload = JSON.parse(raw.toString());
|
|
190
|
+
if (typeof payload.s === "number")
|
|
191
|
+
seq = payload.s;
|
|
192
|
+
if (payload.op === 10) {
|
|
193
|
+
const heartbeatInterval = Number(payload.d?.heartbeat_interval ?? 30000);
|
|
194
|
+
startHeartbeat(heartbeatInterval);
|
|
195
|
+
socket.send(JSON.stringify({
|
|
196
|
+
op: sessionId ? 6 : 2,
|
|
197
|
+
d: sessionId
|
|
198
|
+
? {
|
|
199
|
+
token: `QQBot ${token}`,
|
|
200
|
+
session_id: sessionId,
|
|
201
|
+
seq,
|
|
202
|
+
}
|
|
203
|
+
: {
|
|
204
|
+
token: `QQBot ${token}`,
|
|
205
|
+
intents: INTENTS.GROUP_AND_C2C |
|
|
206
|
+
INTENTS.DIRECT_MESSAGE |
|
|
207
|
+
INTENTS.PUBLIC_GUILD_MESSAGES,
|
|
208
|
+
properties: {
|
|
209
|
+
os: process.platform,
|
|
210
|
+
browser: "open-im",
|
|
211
|
+
device: "open-im",
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
}));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (payload.op === 0 && payload.t === "READY") {
|
|
218
|
+
sessionId = String(payload.d?.session_id ?? "");
|
|
219
|
+
settle(resolve);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (payload.op === 0 && payload.t === "RESUMED") {
|
|
223
|
+
settle(resolve);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const event = normalizeInboundEvent(payload);
|
|
227
|
+
if (event && (event.content || (event.attachments?.length ?? 0) > 0)) {
|
|
228
|
+
await handler(event);
|
|
229
|
+
}
|
|
204
230
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
await handler(event);
|
|
231
|
+
catch (error) {
|
|
232
|
+
log.error("Failed to handle QQ gateway payload:", error);
|
|
208
233
|
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
log.error("
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
tokenState = null;
|
|
226
|
-
sessionId = null;
|
|
227
|
-
seq = null;
|
|
228
|
-
}
|
|
229
|
-
const delay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
230
|
-
reconnectAttempt += 1;
|
|
231
|
-
reconnectTimer = setTimeout(() => {
|
|
232
|
-
if (currentConfig && currentHandler) {
|
|
233
|
-
connectWebSocket(currentConfig, currentHandler).catch((err) => {
|
|
234
|
-
log.error("QQ reconnect failed:", err);
|
|
235
|
-
});
|
|
234
|
+
});
|
|
235
|
+
socket.on("error", (error) => {
|
|
236
|
+
log.error("QQ gateway error:", error);
|
|
237
|
+
settle(() => reject(error));
|
|
238
|
+
});
|
|
239
|
+
socket.on("close", (code, reason) => {
|
|
240
|
+
settle(() => { }); // 清理 ready timeout
|
|
241
|
+
clearTimers();
|
|
242
|
+
ws = null;
|
|
243
|
+
log.info(`QQ gateway closed: ${code} ${reason.toString()}`);
|
|
244
|
+
if (stopped)
|
|
245
|
+
return;
|
|
246
|
+
if (code === 4004 || code === 4006 || code === 4007 || code === 4009) {
|
|
247
|
+
tokenState = null;
|
|
248
|
+
sessionId = null;
|
|
249
|
+
seq = null;
|
|
236
250
|
}
|
|
237
|
-
|
|
251
|
+
const delay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
252
|
+
reconnectAttempt += 1;
|
|
253
|
+
reconnectTimer = setTimeout(() => {
|
|
254
|
+
if (currentConfig && currentHandler) {
|
|
255
|
+
connectWebSocket(currentConfig, currentHandler).catch((err) => {
|
|
256
|
+
log.error("QQ reconnect failed:", err);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}, delay);
|
|
260
|
+
});
|
|
238
261
|
});
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
connecting = false;
|
|
265
|
+
}
|
|
243
266
|
}
|
|
244
267
|
export function getQQBot() {
|
|
245
268
|
if (!client || !currentConfig) {
|
package/dist/qq/event-handler.js
CHANGED
|
@@ -62,7 +62,8 @@ async function buildAttachmentPrompt(event) {
|
|
|
62
62
|
: "bin",
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
catch {
|
|
65
|
+
catch (err) {
|
|
66
|
+
log.warn('Failed to download QQ media attachment:', err);
|
|
66
67
|
localPath = undefined;
|
|
67
68
|
}
|
|
68
69
|
}
|
|
@@ -144,7 +145,20 @@ export function setupQQHandlers(config, sessionManager) {
|
|
|
144
145
|
? sessionManager.getSessionIdForConv(userId, convId, aiCommand)
|
|
145
146
|
: undefined;
|
|
146
147
|
const toolId = aiCommand;
|
|
147
|
-
|
|
148
|
+
let msgId;
|
|
149
|
+
try {
|
|
150
|
+
msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
log.error("Failed to send thinking message:", err);
|
|
154
|
+
try {
|
|
155
|
+
await sendTextReply(chatId, "启动 AI 处理失败,请重试。");
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
log.warn('Failed to send startup error reply:', err);
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
148
162
|
const stopTyping = startTypingLoop();
|
|
149
163
|
const taskKey = `${userId}:${msgId}`;
|
|
150
164
|
await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: "qq", taskKey }, prompt, toolAdapter, {
|
|
@@ -7,6 +7,17 @@ import { buildDirectoryMessage } from "../shared/system-messages.js";
|
|
|
7
7
|
const log = createLogger("QQSender");
|
|
8
8
|
const MAX_QQ_MESSAGE_LENGTH = 1500;
|
|
9
9
|
const pendingReplies = new Map();
|
|
10
|
+
// Periodic cleanup of orphaned pending replies
|
|
11
|
+
const PENDING_MAX_AGE_MS = 10 * 60 * 1000; // 10 minutes
|
|
12
|
+
setInterval(() => {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
for (const [id, state] of pendingReplies) {
|
|
15
|
+
// pendingReplies don't have timestamps, but we can clear old ones based on size
|
|
16
|
+
if (pendingReplies.size > 100) {
|
|
17
|
+
pendingReplies.delete(id);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}, PENDING_MAX_AGE_MS);
|
|
10
21
|
function parseChatTarget(chatId) {
|
|
11
22
|
if (chatId.startsWith("group:")) {
|
|
12
23
|
return { kind: "group", id: chatId.slice("group:".length) };
|
package/dist/service-control.js
CHANGED
|
@@ -93,6 +93,10 @@ export function startBackgroundService(cwd) {
|
|
|
93
93
|
env: process.env,
|
|
94
94
|
windowsHide: process.platform === "win32",
|
|
95
95
|
});
|
|
96
|
+
child.on("error", (err) => {
|
|
97
|
+
// Spawn failure (ENOENT etc.) — report via stderr since logger may not be initialized
|
|
98
|
+
process.stderr.write(`[service-control] Spawn failed: ${err.message}\n`);
|
|
99
|
+
});
|
|
96
100
|
child.unref();
|
|
97
101
|
if (!child.pid) {
|
|
98
102
|
throw new Error("Failed to start background service.");
|
|
@@ -236,7 +236,17 @@ export class SessionManager {
|
|
|
236
236
|
const resolved = resolveWorkDirInput(baseDir, targetDir);
|
|
237
237
|
if (!existsSync(resolved))
|
|
238
238
|
throw new Error(`目录不存在: \`${resolved}\``);
|
|
239
|
-
|
|
239
|
+
const real = await realpath(resolved);
|
|
240
|
+
// Block access to sensitive system directories
|
|
241
|
+
const blockedPrefixes = process.platform === 'win32'
|
|
242
|
+
? ['C:\\Windows', 'C:\\Program Files', 'C:\\Program Files (x86)', 'C:\\ProgramData']
|
|
243
|
+
: ['/etc', '/proc', '/sys', '/dev', '/boot', '/root', '/sbin', '/usr/sbin'];
|
|
244
|
+
for (const prefix of blockedPrefixes) {
|
|
245
|
+
if (real.toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
246
|
+
throw new Error(`不允许访问系统目录: \`${real}\``);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return real;
|
|
240
250
|
}
|
|
241
251
|
load(previousDefaultWorkDir) {
|
|
242
252
|
try {
|
package/dist/setup.js
CHANGED
|
@@ -940,8 +940,9 @@ const PLATFORM_LABELS = {
|
|
|
940
940
|
wework: "企业微信",
|
|
941
941
|
dingtalk: "钉钉",
|
|
942
942
|
wechat: "微信(测试中)",
|
|
943
|
+
workbuddy: "WorkBuddy",
|
|
943
944
|
};
|
|
944
|
-
const ALL_PLATFORMS = ["telegram", "feishu", "qq", "wework", "dingtalk", "wechat"];
|
|
945
|
+
const ALL_PLATFORMS = ["telegram", "feishu", "qq", "wework", "dingtalk", "wechat", "workbuddy"];
|
|
945
946
|
/**
|
|
946
947
|
* 启动时让用户选择要启用的平台(无论单通道还是多通道)
|
|
947
948
|
* 显示全部 4 个平台,已配置的预选;若用户选择未配置的,引导运行 init
|
|
@@ -6,8 +6,8 @@ export interface DingTalkActiveTarget {
|
|
|
6
6
|
updatedAt: number;
|
|
7
7
|
}
|
|
8
8
|
export declare function loadActiveChats(): void;
|
|
9
|
-
export declare function getActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework'): string | undefined;
|
|
10
|
-
export declare function setActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework', chatId: string): void;
|
|
9
|
+
export declare function getActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy'): string | undefined;
|
|
10
|
+
export declare function setActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy', chatId: string): void;
|
|
11
11
|
export declare function getDingTalkActiveTarget(): DingTalkActiveTarget | undefined;
|
|
12
12
|
export declare function setDingTalkActiveTarget(target: Omit<DingTalkActiveTarget, 'updatedAt'>): void;
|
|
13
13
|
export declare function flushActiveChats(): void;
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -247,7 +247,19 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
247
247
|
startedAt: Date.now(),
|
|
248
248
|
toolId: aiCommand,
|
|
249
249
|
};
|
|
250
|
-
|
|
250
|
+
try {
|
|
251
|
+
startRun();
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
if (!settled) {
|
|
255
|
+
settled = true;
|
|
256
|
+
cleanup();
|
|
257
|
+
log.error(`[AITask] Synchronous error in startRun: ${err}`);
|
|
258
|
+
platformAdapter.sendError(`内部错误:${err instanceof Error ? err.message : String(err)}`).catch(() => { });
|
|
259
|
+
resolve();
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
251
263
|
platformAdapter.onTaskReady(taskState);
|
|
252
264
|
});
|
|
253
265
|
}
|
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
*/
|
|
5
5
|
const chatToUser = new Map();
|
|
6
6
|
const chatToPlatform = new Map();
|
|
7
|
+
// Periodic cleanup to prevent unbounded growth (keep last 1000 entries)
|
|
8
|
+
const CHAT_MAP_MAX_SIZE = 1000;
|
|
9
|
+
setInterval(() => {
|
|
10
|
+
if (chatToUser.size > CHAT_MAP_MAX_SIZE) {
|
|
11
|
+
const keysToDelete = [...chatToUser.keys()].slice(0, chatToUser.size - CHAT_MAP_MAX_SIZE);
|
|
12
|
+
for (const key of keysToDelete) {
|
|
13
|
+
chatToUser.delete(key);
|
|
14
|
+
chatToPlatform.delete(key);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}, 60 * 60 * 1000); // Check every hour
|
|
7
18
|
export function setChatUser(chatId, userId, platform) {
|
|
8
19
|
chatToUser.set(chatId, userId);
|
|
9
20
|
if (platform)
|