@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.
Files changed (55) hide show
  1. package/dist/access/access-control.js +1 -1
  2. package/dist/adapters/claude-sdk-adapter.js +94 -36
  3. package/dist/channels/capabilities.js +5 -0
  4. package/dist/cli.js +5 -2
  5. package/dist/commands/handler.d.ts +1 -2
  6. package/dist/commands/handler.js +6 -18
  7. package/dist/config-web-page-i18n.d.ts +12 -0
  8. package/dist/config-web-page-i18n.js +12 -0
  9. package/dist/config-web-page-script.js +1 -0
  10. package/dist/config-web-page-template.js +48 -1
  11. package/dist/config-web.js +110 -7
  12. package/dist/config.d.ts +25 -1
  13. package/dist/config.js +46 -0
  14. package/dist/constants.d.ts +2 -0
  15. package/dist/constants.js +2 -0
  16. package/dist/dingtalk/client.js +11 -3
  17. package/dist/dingtalk/event-handler.js +18 -3
  18. package/dist/dingtalk/message-sender.js +13 -0
  19. package/dist/feishu/event-handler.js +144 -10
  20. package/dist/index.js +26 -2
  21. package/dist/manager-control.js +7 -0
  22. package/dist/qq/client.js +111 -88
  23. package/dist/qq/event-handler.js +16 -2
  24. package/dist/qq/message-sender.js +11 -0
  25. package/dist/service-control.js +4 -0
  26. package/dist/session/session-manager.js +11 -1
  27. package/dist/setup.js +2 -1
  28. package/dist/shared/active-chats.d.ts +2 -2
  29. package/dist/shared/ai-task.js +13 -1
  30. package/dist/shared/chat-user-map.js +11 -0
  31. package/dist/shared/media-storage.js +27 -0
  32. package/dist/telegram/client.js +25 -3
  33. package/dist/telegram/event-handler.js +44 -8
  34. package/dist/telegram/message-sender.js +13 -0
  35. package/dist/wechat/auth/qclaw-api.js +1 -1
  36. package/dist/wechat/client.js +81 -4
  37. package/dist/wechat/event-handler.js +10 -3
  38. package/dist/wework/client.js +36 -14
  39. package/dist/wework/event-handler.js +39 -4
  40. package/dist/wework/message-sender.js +53 -21
  41. package/dist/workbuddy/centrifuge-client.d.ts +74 -0
  42. package/dist/workbuddy/centrifuge-client.js +272 -0
  43. package/dist/workbuddy/client.d.ts +27 -0
  44. package/dist/workbuddy/client.js +162 -0
  45. package/dist/workbuddy/event-handler.d.ts +11 -0
  46. package/dist/workbuddy/event-handler.js +118 -0
  47. package/dist/workbuddy/index.d.ts +8 -0
  48. package/dist/workbuddy/index.js +8 -0
  49. package/dist/workbuddy/message-sender.d.ts +16 -0
  50. package/dist/workbuddy/message-sender.js +51 -0
  51. package/dist/workbuddy/oauth.d.ts +114 -0
  52. package/dist/workbuddy/oauth.js +310 -0
  53. package/dist/workbuddy/types.d.ts +86 -0
  54. package/dist/workbuddy/types.js +4 -0
  55. 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
- try {
61
- cardHandle = await sendThinkingCard(chatId, toolId);
62
- }
63
- catch (err) {
64
- log.error('Failed to send thinking card:', err);
65
- return;
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).catch((e) => log.debug('Stream update failed (will retry on next update):', e?.message ?? e));
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
- /* ignore */
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");
@@ -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
- ws.send(JSON.stringify({ op: 1, d: seq }));
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
- const gatewayUrl = await getGatewayUrl(config);
151
- const token = await fetchAccessToken(config);
152
- await new Promise((resolve, reject) => {
153
- const socket = new WebSocket(gatewayUrl);
154
- ws = socket;
155
- let settled = false;
156
- const settle = (fn) => {
157
- if (settled)
158
- return;
159
- settled = true;
160
- fn();
161
- };
162
- socket.on("open", () => {
163
- log.info("QQ gateway connected");
164
- reconnectAttempt = 0;
165
- });
166
- socket.on("message", async (raw) => {
167
- try {
168
- const payload = JSON.parse(raw.toString());
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
- if (payload.op === 0 && payload.t === "RESUMED") {
202
- settle(resolve);
203
- return;
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
- const event = normalizeInboundEvent(payload);
206
- if (event && (event.content || (event.attachments?.length ?? 0) > 0)) {
207
- await handler(event);
231
+ catch (error) {
232
+ log.error("Failed to handle QQ gateway payload:", error);
208
233
  }
209
- }
210
- catch (error) {
211
- log.error("Failed to handle QQ gateway payload:", error);
212
- }
213
- });
214
- socket.on("error", (error) => {
215
- log.error("QQ gateway error:", error);
216
- settle(() => reject(error));
217
- });
218
- socket.on("close", (code, reason) => {
219
- clearTimers();
220
- ws = null;
221
- log.info(`QQ gateway closed: ${code} ${reason.toString()}`);
222
- if (stopped)
223
- return;
224
- if (code === 4004 || code === 4006 || code === 4007 || code === 4009) {
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
- }, delay);
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
- setTimeout(() => {
240
- settle(() => reject(new Error("QQ gateway ready timeout")));
241
- }, 15000);
242
- });
262
+ }
263
+ finally {
264
+ connecting = false;
265
+ }
243
266
  }
244
267
  export function getQQBot() {
245
268
  if (!client || !currentConfig) {
@@ -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
- const msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId);
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) };
@@ -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
- return realpath(resolved);
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;
@@ -247,7 +247,19 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
247
247
  startedAt: Date.now(),
248
248
  toolId: aiCommand,
249
249
  };
250
- startRun();
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)