diff --git a/src/media/media-download.ts b/src/media/media-download.ts index 0ad7b19..21be066 100644 --- a/src/media/media-download.ts +++ b/src/media/media-download.ts @@ -29,12 +29,15 @@ export async function downloadMediaFromItem( deps: { cdnBaseUrl: string; saveMedia: SaveMediaFn; + /** Subdirectory under the media store (e.g. "wecom//inbound"). Defaults to "inbound". */ + subdir?: string; log: (msg: string) => void; errLog: (msg: string) => void; label: string; }, ): Promise { const { cdnBaseUrl, saveMedia, log, errLog, label } = deps; + const subdir = deps.subdir ?? "inbound"; const result: WeixinInboundMediaOpts = {}; if (item.type === MessageItemType.IMAGE) { @@ -61,7 +64,7 @@ export async function downloadMediaFromItem( `${label} image-plain`, img.media.full_url, ); - const saved = await saveMedia(buf, undefined, "inbound", WEIXIN_MEDIA_MAX_BYTES); + const saved = await saveMedia(buf, undefined, subdir, WEIXIN_MEDIA_MAX_BYTES); result.decryptedPicPath = saved.path; logger.debug(`${label} image saved: ${saved.path}`); } catch (err) { @@ -83,12 +86,12 @@ export async function downloadMediaFromItem( logger.debug(`${label} voice: decrypted ${silkBuf.length} bytes, attempting silk transcode`); const wavBuf = await silkToWav(silkBuf); if (wavBuf) { - const saved = await saveMedia(wavBuf, "audio/wav", "inbound", WEIXIN_MEDIA_MAX_BYTES); + const saved = await saveMedia(wavBuf, "audio/wav", subdir, WEIXIN_MEDIA_MAX_BYTES); result.decryptedVoicePath = saved.path; result.voiceMediaType = "audio/wav"; logger.debug(`${label} voice: saved WAV to ${saved.path}`); } else { - const saved = await saveMedia(silkBuf, "audio/silk", "inbound", WEIXIN_MEDIA_MAX_BYTES); + const saved = await saveMedia(silkBuf, "audio/silk", subdir, WEIXIN_MEDIA_MAX_BYTES); result.decryptedVoicePath = saved.path; result.voiceMediaType = "audio/silk"; logger.debug(`${label} voice: silk transcode unavailable, saved raw SILK to ${saved.path}`); @@ -113,7 +116,7 @@ export async function downloadMediaFromItem( const saved = await saveMedia( buf, mime, - "inbound", + subdir, WEIXIN_MEDIA_MAX_BYTES, fileItem.file_name ?? undefined, ); @@ -136,7 +139,7 @@ export async function downloadMediaFromItem( `${label} video`, videoItem.media.full_url, ); - const saved = await saveMedia(buf, "video/mp4", "inbound", WEIXIN_MEDIA_MAX_BYTES); + const saved = await saveMedia(buf, "video/mp4", subdir, WEIXIN_MEDIA_MAX_BYTES); result.decryptedVideoPath = saved.path; logger.debug(`${label} video: saved to ${saved.path}`); } catch (err) { diff --git a/src/messaging/process-message.ts b/src/messaging/process-message.ts index c102af5..7df047f 100644 --- a/src/messaging/process-message.ts +++ b/src/messaging/process-message.ts @@ -60,8 +60,8 @@ function extractTextBody(itemList?: import("../api/types.js").MessageItem[]): st } /** - * Process a single inbound message: route → download media → dispatch reply. - * Extracted from the monitor loop to keep monitoring and message handling separate. + * Process a single inbound message: resolve route → download media → authorize → dispatch reply. + * Route is resolved first so agentId can isolate media per-agent in the media store. */ export async function processOneMessage( full: WeixinMessage, @@ -107,6 +107,35 @@ export async function processOneMessage( ); } + // Resolve agent route early so we can use agentId for per-agent media isolation. + const senderId = full.from_user_id ?? ""; + const route = deps.channelRuntime.routing.resolveAgentRoute({ + cfg: deps.config, + channel: "openclaw-weixin", + accountId: deps.accountId, + peer: { kind: "direct", id: senderId }, + }); + logger.debug( + `resolveAgentRoute: agentId=${route.agentId ?? "(none)"} sessionKey=${route.sessionKey ?? "(none)"} mainSessionKey=${route.mainSessionKey ?? "(none)"}`, + ); + if (!route.agentId) { + logger.error( + `resolveAgentRoute: no agentId resolved for peer=${senderId} accountId=${deps.accountId} — message will not be dispatched`, + ); + } + + if (debug) { + debugTrace.push( + "── 路由 ──", + `│ route: agent=${route.agentId ?? "none"} session=${route.sessionKey ?? "none"}`, + ); + } + + // Per-agent media isolation: wecom//inbound + const mediaSubdir = route.agentId + ? `wecom/${route.agentId}/inbound` + : "inbound"; + const mediaOpts: WeixinInboundMediaOpts = {}; // Find the first downloadable media item (priority: IMAGE > VIDEO > FILE > VOICE). @@ -145,6 +174,7 @@ export async function processOneMessage( const downloaded = await downloadMediaFromItem(mediaItem, { cdnBaseUrl: deps.cdnBaseUrl, saveMedia: deps.channelRuntime.media.saveMediaBuffer, + subdir: mediaSubdir, log: deps.log, errLog: deps.errLog, label, @@ -166,8 +196,6 @@ export async function processOneMessage( const rawBody = ctx.Body?.trim() ?? ""; ctx.CommandBody = rawBody; - const senderId = full.from_user_id ?? ""; - const { senderAllowedForCommands, commandAuthorized } = await resolveSenderCommandAuthorizationWithRuntime({ cfg: deps.config, @@ -208,30 +236,9 @@ export async function processOneMessage( if (debug) { debugTrace.push( - "── 鉴权 & 路由 ──", + "── 鉴权 ──", `│ auth: cmdAuthorized=${String(commandAuthorized)} senderAllowed=${String(senderAllowedForCommands)}`, ); - } - - const route = deps.channelRuntime.routing.resolveAgentRoute({ - cfg: deps.config, - channel: "openclaw-weixin", - accountId: deps.accountId, - peer: { kind: "direct", id: ctx.To }, - }); - logger.debug( - `resolveAgentRoute: agentId=${route.agentId ?? "(none)"} sessionKey=${route.sessionKey ?? "(none)"} mainSessionKey=${route.mainSessionKey ?? "(none)"}`, - ); - if (!route.agentId) { - logger.error( - `resolveAgentRoute: no agentId resolved for peer=${ctx.To} accountId=${deps.accountId} — message will not be dispatched`, - ); - } - - if (debug) { - debugTrace.push( - `│ route: agent=${route.agentId ?? "none"} session=${route.sessionKey ?? "none"}`, - ); debugTs.preDispatch = Date.now(); } // Propagate the resolved session key into ctx so dispatchReplyFromConfig uses