mateclaw/mateclaw-server/src/main/java/vip/mate/channel/ChannelChatOriginFactory.java
matevip 7894a50067 fix(tool): emit absolute download URLs for streaming-generated files (#164)
Streaming chat ran render tools on an async thread with no bound request,
so download links lost their host and arrived without a domain. Resolve the
host on the request thread and carry it through ChatOrigin/ToolContext;
falls back to a configurable public-base-url, then a relative path.
2026-06-07 15:03:48 +08:00

78 lines
3.5 KiB
Java

package vip.mate.channel;
import org.springframework.stereotype.Component;
import vip.mate.agent.context.ChannelTarget;
import vip.mate.agent.context.ChatOrigin;
import vip.mate.channel.model.ChannelEntity;
/**
* RFC-063r §2.2: factory that translates an inbound channel message into a
* {@link ChatOrigin}. Lives in {@code vip.mate.channel} (not in
* {@code vip.mate.agent.context}) so that the dependency direction stays
* {@code channel → agent} and never the reverse.
*/
@Component
public class ChannelChatOriginFactory {
/**
* Build a {@link ChatOrigin} for a channel-originated message.
*
* @param channel channel entity (non-null) — provides id + workspaceId
* @param message inbound message (non-null) — provides senderId + reply target
* @param conversationId resolved conversation id (channel-scoped)
* @param workspaceBasePath workspace activity directory; null = unrestricted
*/
public ChatOrigin from(ChannelEntity channel,
ChannelMessage message,
String conversationId,
String workspaceBasePath) {
ChannelTarget target = new ChannelTarget(
resolveTargetId(message),
/* threadId */ null, // adapters fill via ChannelMessage extension fields when available
/* accountId */ null);
return new ChatOrigin(
/* agentId */ null,
/* conversationId */ conversationId,
/* requesterId */ message.getSenderId(),
/* workspaceId */ channel.getWorkspaceId(),
/* workspaceBasePath */ workspaceBasePath,
/* channelId */ channel.getId(),
/* channelTarget */ target,
/* cronOrigin */ false,
/* senderName */ message.getSenderName(),
/* channelType */ message.getChannelType() != null
? message.getChannelType()
: channel.getChannelType(),
/* chatId */ message.getChatId(),
/* baseUrl */ null); // IM origins have no request host; rely on public-base-url config
}
/**
* Resolve the IM target id used for proactive sends.
*
* <p><b>Critical</b>: must NOT use {@link ChannelMessage#getReplyToken()} —
* for DingTalk the reply token encodes a {@code sessionWebhook} URL that
* expires ~90 minutes after the inbound message. A cron persisted with an
* expired sessionWebhook fails proactive delivery with 401/403 forever
* after the window lapses.
*
* <p>Resolution order — both fields are stable identifiers across all
* supported channels:
* <ol>
* <li>{@code chatId} — group / channel / room identifier; preferred so
* cron messages land in the same conversation the user triggered
* the cron from</li>
* <li>{@code senderId} — user identifier; fallback for private chats
* where {@code chatId} is null. DingTalk's {@code proactiveSend}
* routes a userId through the Robot API ({@code oToMessages/batchSend}),
* which works indefinitely.</li>
* </ol>
*/
private String resolveTargetId(ChannelMessage message) {
if (message.getChatId() != null && !message.getChatId().isBlank()) {
return message.getChatId();
}
return message.getSenderId();
}
}