mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 19:23:42 +08:00
User-reported field issues + a deeper code audit revealed multiple overlapping bugs in the prior cron-channel delivery change. This fixes all six. #1 — Concurrency race on ToolExecutionExecutor (root cause of 'sometimes succeeds, sometimes fails' tool calls). The volatile instance fields currentRequesterId / currentWorkspaceBasePath / currentChatOrigin were shared by every conversation routed through the same per-agent executor; one user mid-build-loop while another's execute() overwrote the field would cross-contaminate the captured values into PreparedToolCall. Fix: kill the instance fields, thread origin/requester/workspace as method params straight into PreparedToolCall snapshot. Comment pins the rule so it cannot regress. #2 — CHAT_ORIGIN missing from KeyStrategyFactory (latent timebomb, masked by spring-ai-alibaba-graph-core's non-filtering builder path). Without an addStrategy registration, multi-node state merges in long ReAct / Plan-Execute loops drop the key, ActionNode reads ChatOrigin.EMPTY, and the cron persists with channel_id=NULL. Also caught 4 more keys that were latently unregistered: WORKSPACE_BASE_PATH, STOP_REQUESTED, RETURN_DIRECT_TRIGGERED, DIRECT_TOOL_OUTPUTS. All five now registered in both ReAct and Plan-Execute factories. #3 — CronJobs UI didn't surface channel binding. CronJobDTO carried channelId / deliveryConfig but the list page never rendered them. Added: (a) 'channel' column on list page, (b) channel + targetId rows in the detail modal, (c) backend batch-loads channel names via ChannelMapper.selectBatchIds so the column shows the human-readable name, (d) i18n keys (zh + en), (e) channelName field on TS CronJob type. #4a — DingTalk targetId expiry. ChannelChatOriginFactory.resolveTargetId used to prefer ChannelMessage.replyToken which for DingTalk encodes a sessionWebhook URL that expires ~90 minutes after the inbound message. Cron persisted with that webhook then dies with 401/403 and marks NOT_DELIVERED forever. Fix: prefer the stable chatId, fall back to senderId — both work indefinitely via DingTalk's Robot API. #4b — Scheduler pool exhaustion under long LLM. CronJobService's ThreadPoolTaskScheduler ran with poolSize=4 AND the LLM call lived on the scheduler thread. Four concurrent crons saturated the pool and the 5th silently missed its tick. Fix: keep scheduler tiny (it just fires triggers) and offload runAgent to a dedicated virtual-thread executor (cron-execute-* threads). LLM workload is I/O-bound — virtual threads scale to thousands at trivial cost. #5 — Minor latent bugs: - AbstractCronResultDelivery.claimRun used .in(... 'NONE','PENDING',null), but SQL IN never matches NULL. Rewrote as IS NULL OR IN (NONE,PENDING) so legacy pre-V57 rows can still claim. - CronDeliveryListener.onCompletedRaw was an empty @EventListener with a wrong-headed comment about test fallbackExecution. Removed. - CronJobTool.resolveAgentId silently returned 1L when origin lacked an agentId — would silently bind to whatever agent #1 happens to be. Replaced with explicit error so wiring bugs surface immediately instead of producing scheduled-but-never-runs crons. State-key registration guard. New StateKeyRegistrationCoverageTest scans MateClawStateKeys via reflection and parses AgentGraphBuilder.java to extract every .addStrategy(MateClawStateKeys.X, ...). Asserts every non-_NODE constant appears in at least one factory. Caught the 4 unregistered keys above on first run; will catch any future 'forgot to register' regression. Tests: 33 unit/arch tests + 27 regression in touched areas — all green. Vue typecheck clean. Refs: #25, #16
71 lines
3.1 KiB
Java
71 lines
3.1 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);
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
}
|