Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/media/media-download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@ export async function downloadMediaFromItem(
deps: {
cdnBaseUrl: string;
saveMedia: SaveMediaFn;
/** Subdirectory under the media store (e.g. "wecom/<agentId>/inbound"). Defaults to "inbound". */
subdir?: string;
log: (msg: string) => void;
errLog: (msg: string) => void;
label: string;
},
): Promise<WeixinInboundMediaOpts> {
const { cdnBaseUrl, saveMedia, log, errLog, label } = deps;
const subdir = deps.subdir ?? "inbound";
const result: WeixinInboundMediaOpts = {};

if (item.type === MessageItemType.IMAGE) {
Expand All @@ -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) {
Expand All @@ -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}`);
Expand All @@ -113,7 +116,7 @@ export async function downloadMediaFromItem(
const saved = await saveMedia(
buf,
mime,
"inbound",
subdir,
WEIXIN_MEDIA_MAX_BYTES,
fileItem.file_name ?? undefined,
);
Expand All @@ -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) {
Expand Down
59 changes: 33 additions & 26 deletions src/messaging/process-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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/<agentId>/inbound
const mediaSubdir = route.agentId
? `wecom/${route.agentId}/inbound`
: "inbound";

const mediaOpts: WeixinInboundMediaOpts = {};

// Find the first downloadable media item (priority: IMAGE > VIDEO > FILE > VOICE).
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down