diff --git a/index.js b/index.js index 7c793ab..040366b 100755 --- a/index.js +++ b/index.js @@ -40,10 +40,18 @@ function registerWebhookTarget(target) { const key = normalizeWebhookPath(target.path); const entry = { ...target, path: key }; + + // Remove existing entries for the same appId to prevent Map growth on auto-restart const existing = webhookTargets.get(key) ?? []; - webhookTargets.set(key, [...existing, entry]); + const filtered = existing.filter(e => e.account.id !== target.account.id); + + webhookTargets.set(key, [...filtered, entry]); + + console.log(`[DEBUG] Webhook target registered. Key: ${key}, AppId: ${target.account.id}, Total targets for key: ${filtered.length + 1}`); + return () => { - const updated = (webhookTargets.get(key) ?? []).filter((e) => e !== entry); + const current = webhookTargets.get(key) ?? []; + const updated = current.filter((e) => e !== entry); if (updated.length > 0) { webhookTargets.set(key, updated); } else { @@ -169,6 +177,8 @@ token: app.token || "", encodingAesKey: app.encodingAesKey || "", webhookPath: app.webhookPath || normalizeWebhookPath(`/webhooks/wecom/${app.id}`), + corpId: app.corpId || "", + corpSecret: app.corpSecret || "", config: app, // Pass the individual app config for `isSelfBuiltApp` etc. isSelfBuiltApp: app.isSelfBuiltApp === true, }; @@ -272,27 +282,35 @@ }, gateway: { startAccount: async (ctx) => { - // This startAccount will now be called for each configured application (identified by accountId) - const appAccount = ctx.account; // 'account' here is already resolved by resolveAccount and contains individual app config + try { + console.log("[DEBUG] startAccount called for appId:", ctx.accountId); + const appAccount = ctx.account; - logger.info("WeCom gateway starting application", { - appId: appAccount.id, - webhookPath: appAccount.webhookPath, - isSelfBuiltApp: appAccount.isSelfBuiltApp, - }); + console.log("[DEBUG] appAccount data:", JSON.stringify(appAccount)); - const unregister = registerWebhookTarget({ - path: appAccount.webhookPath, - account: appAccount, - config: ctx.cfg, // Full OpenClaw config - }); + const unregister = registerWebhookTarget({ + path: appAccount.webhookPath, + account: appAccount, + config: ctx.cfg, + }); - return { - shutdown: async () => { - logger.info("WeCom gateway shutting down application", { appId: appAccount.id }); - unregister(); - }, - }; + console.log("[DEBUG] startAccount succeeded for appId:", ctx.accountId); + + let resolveTask; + const task = new Promise((resolve) => { resolveTask = resolve; }); + + return { + shutdown: async () => { + console.log("[DEBUG] shutting down appId:", ctx.accountId); + unregister(); + resolveTask(); + }, + task, + }; + } catch (err) { + console.error("[DEBUG] startAccount FAILED for appId:", ctx.accountId, err); + throw err; + } }, }, }; @@ -304,6 +322,7 @@ async function wecomHttpHandler(req, res) { const url = new URL(req.url || "", "http://localhost"); const path = normalizeWebhookPath(url.pathname); + console.log("[DEBUG] wecomHttpHandler received request:", req.method, path); const targets = webhookTargets.get(path); if (!targets || targets.length === 0) { @@ -375,137 +394,139 @@ const isAIBot = !appAccount.isSelfBuiltApp; // Use app-specific flag - if (isAIBot) { - if (result.stream) { // AI Bot stream refresh - const streamId = result.stream.id; - const stream = streamManager.getStream(streamId); - - if (!stream) { - logger.warn("Stream not found for refresh", { streamId, appId: appAccount.id }); - const streamResponse = webhook.buildStreamResponse( - streamId, - "会话已过期", - true, - timestamp, - nonce, - ); - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(streamResponse); - return true; - } - - const meta = streamMeta.get(streamId); - if (meta?.mainResponseDone && !stream.finished) { - const idleMs = Date.now() - stream.updatedAt; - if (idleMs > 10000) { - logger.info("WeCom: closing stream due to idle timeout", { streamId, idleMs, appId: appAccount.id }); - try { - await streamManager.finishStream(streamId); - } catch (err) { - logger.error("WeCom: failed to finish stream", { streamId, error: err.message, appId: appAccount.id }); + if (isAIBot) { + if (result.stream) { // AI Bot stream refresh + const streamId = result.stream.id; + const stream = streamManager.getStream(streamId); + + if (!stream) { + logger.warn("Stream not found for refresh", { streamId, appId: appAccount.id }); + const streamResponse = webhook.buildStreamResponse( + streamId, + "会话已过期", + true, + timestamp, + nonce, + ); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(streamResponse); + return true; } - } - } - - const streamResponse = webhook.buildStreamResponse( - streamId, - stream.content, - stream.finished, - timestamp, - nonce, - stream.finished && stream.msgItem.length > 0 ? { msgItem: stream.msgItem } : {}, - ); - - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(streamResponse); - - logger.debug("Stream refresh response sent", { - streamId, - contentLength: stream.content.length, - finished: stream.finished, - appId: appAccount.id, - }); - - if (stream.finished) { - setTimeout(() => { - streamManager.deleteStream(streamId); - streamMeta.delete(streamId); - }, 30 * 1000); - } - return true; - - } else { // AI Bot initial message/event - const streamId = `stream_${crypto.randomUUID()}`; - streamManager.createStream(streamId); - streamManager.appendStream(streamId, THINKING_PLACEHOLDER); - const streamResponse = webhook.buildStreamResponse(streamId, THINKING_PLACEHOLDER, false, timestamp, nonce); - - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(streamResponse); - - logger.info("Stream initiated (AI Bot)", { - streamId, - from: message.fromUser, - appId: appAccount.id, - }); - - processInboundMessage({ - message, - streamId, - timestamp, - nonce, - account: appAccount, - config: fullConfig, - }).catch(async (err) => { - logger.error("WeCom AI Bot message processing failed", { error: err.message, appId: appAccount.id }); - await handleStreamError(streamId, getMessageStreamKey(message), "处理消息时出错,请稍后再试。"); - }); - return true; - } - - // Self-Built App: passive XML reply - logger.info("Self-Built App message/event received, processing for passive reply", { + + const meta = streamMeta.get(streamId); + if (meta?.mainResponseDone && !stream.finished) { + const idleMs = Date.now() - stream.updatedAt; + if (idleMs > 10000) { + logger.info("WeCom: closing stream due to idle timeout", { streamId, idleMs, appId: appAccount.id }); + try { + await streamManager.finishStream(streamId); + } catch (err) { + logger.error("WeCom: failed to finish stream", { streamId, error: err.message, appId: appAccount.id }); + } + } + } + + const streamResponse = webhook.buildStreamResponse( + streamId, + stream.content, + stream.finished, + timestamp, + nonce, + stream.finished && stream.msgItem.length > 0 ? { msgItem: stream.msgItem } : {}, + ); + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(streamResponse); + + logger.debug("Stream refresh response sent", { + streamId, + contentLength: stream.content.length, + finished: stream.finished, + appId: appAccount.id, + }); + + if (stream.finished) { + setTimeout(() => { + streamManager.deleteStream(streamId); + streamMeta.delete(streamId); + }, 30 * 1000); + } + return true; + + } else { // AI Bot initial message/event + const streamId = `stream_${crypto.randomUUID()}`; + streamManager.createStream(streamId); + streamManager.appendStream(streamId, THINKING_PLACEHOLDER); + const streamResponse = webhook.buildStreamResponse(streamId, THINKING_PLACEHOLDER, false, timestamp, nonce); + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(streamResponse); + + logger.info("Stream initiated (AI Bot)", { + streamId, from: message.fromUser, appId: appAccount.id, }); - // Ensure isSelfBuiltAppRequest is set explicitly - message.isSelfBuiltAppRequest = true; - - const processingResult = await processInboundMessage({ + processInboundMessage({ message, - streamId: undefined, + streamId, timestamp, nonce, account: appAccount, config: fullConfig, - }).catch(async (err) => { logger.error("WeCom Self-Built App message processing failed", { error: err.message, appId: appAccount.id }); - return { passiveReplyXml: webhook.buildPassiveReplyXml( - message.fromUser, - appAccount.id, - "text", - "处理消息时出错,请稍后再试。", - Math.floor(Date.now() / 1000), - nonce, - )}; - }); - - if (processingResult?.passiveReplyXml) { + }).catch(async (err) => { + logger.error("WeCom AI Bot message processing failed", { error: err.message, appId: appAccount.id }); + await handleStreamError(streamId, getMessageStreamKey(message), "处理消息时出错,请稍后再试。"); + }); + return true; + } + } else { + // Self-Built App: passive XML reply + logger.info("Self-Built App message/event received, processing for passive reply", { + from: message.fromUser, + appId: appAccount.id, + }); + + // Ensure isSelfBuiltAppRequest is set explicitly + message.isSelfBuiltAppRequest = true; + + const processingResult = await processInboundMessage({ + message, + streamId: undefined, + timestamp, + nonce, + account: appAccount, + config: fullConfig, + }).catch(async (err) => { + logger.error("WeCom Self-Built App message processing failed", { error: err.message, appId: appAccount.id }); + return { + passiveReplyXml: webhook.buildPassiveReplyXml( + message.fromUser, + appAccount.id, + "text", + "处理消息时出错,请稍后再试。", + Math.floor(Date.now() / 1000), + nonce, + ) + }; + }); + + if (processingResult?.passiveReplyXml) { logger.info("WeCom Self-Built: returning passive XML reply", { replyXmlPreview: processingResult.passiveReplyXml.substring(0, 100), appId: appAccount.id }); res.writeHead(200, { "Content-Type": "application/xml" }); res.end(processingResult.passiveReplyXml); return true; - } else { + } else { logger.info("WeCom Self-Built: no immediate passive XML reply, returning success.", { appId: appAccount.id }); res.writeHead(200, { "Content-Type": "text/plain" }); res.end("success"); return true; + } } - } - } - - res.writeHead(405, { "Content-Type": "text/plain" }); - res.end("Method Not Allowed"); + } + + res.writeHead(405, { "Content-Type": "text/plain" }); res.end("Method Not Allowed"); return true; }