mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 03:33:43 +08:00
Replaces the prior ThreadLocal context plumbing with explicit Spring AI
ToolContext threading carried by an immutable ChatOrigin value object,
so a cron created from inside WeChat (or any IM channel) delivers its
results back to the originating channel.
Architecture
- ChatOrigin / ChannelTarget value objects + per-entry-point factories
(ChannelChatOriginFactory in vip.mate.channel, CronChatOriginFactory
in vip.mate.cron — symmetric, no cyclic deps).
- LocaleAwareToolCallback now forwards call(String, ToolContext) and
getToolMetadata so the decorator chain cannot silently drop the origin.
- AgentService 6-method overhaul + ChatOriginHolder bridge into
StateGraph buildInitialState which writes CHAT_ORIGIN; ActionNode +
StepExecutionNode forward it to ToolExecutionExecutor.
- ToolExecutionExecutor builds ToolContext per call; 8/8 tools migrated
(CronJobTool, WorkspacePathGuard, Video/Image/Browser/ReadFile/Music,
DelegateAgentTool with parent-origin inheritance).
- CronJobRunner + CronJobLifecycleService 3-segment REQUIRES_NEW model
(T1 startRun / no-tx runAgent / T2 finishRunAndPublish); ArchUnit
pins CronJobRunner as @Transactional-free.
- CronResultDelivery Strategy + AbstractCronResultDelivery Template
with SQL CAS idempotency on mate_cron_job_run.delivery_status —
replaces the prior process-local Caffeine TTL, cluster-safe.
- CronJobCompletedEvent + @Async @TransactionalEventListener(AFTER_COMMIT);
cronDeliveryExecutor (core=2, max=4, queue=1000, AbortPolicy + audit).
- CronRunStaleCleanup @Scheduled(5min) sweeps PENDING-15min and
status='running'-30min in one query each.
- CronJobRunner.wrapWithDeliveryGuard prepends a system note for
channel-bound crons to suppress hallucinated 'install CLI to send
WeChat' suggestions.
- ApprovalWorkflowService Memento: persist ChatOrigin snapshot on
create, restore on replay so cross-restart approvals keep channel
binding; ChannelMessageRouter + ChatController web-replay both prefer
the Memento and fall back to fresh-build.
- ChannelManager.sendToChannel 4-arg DeliveryOptions overload;
ChannelAdapter#proactiveSend default 4-arg pass-through; Slack
overrides for thread_ts and Telegram overrides for message_thread_id.
- CronJobs UI: read-only 'last delivery' badge driven by
CronJobMapper.selectListWithDeliveryStatus subquery.
Schema migrations V57/V58/V59 (V56 was already taken by an unrelated
provider migration — Flyway processes versions in order regardless of
gaps):
- V57: mate_cron_job_run delivery_status / target / error + composite
index (delivery_status, started_at) covering the cleanup sweep.
- V58: mate_cron_job channel_id (indexed) + delivery_config TEXT (JSON
via MyBatis Plus JacksonTypeHandler).
- V59: mate_tool_approval chat_origin TEXT (Memento).
All idempotent in both H2 (IF NOT EXISTS) and MySQL (INFORMATION_SCHEMA
guard + PREPARE).
ArchUnit guards (test scope, archunit-junit5 1.3.0):
- every concrete vip.mate.* ToolCallback must override
call(String, ToolContext) — pins the decorator-forward fix.
- CronJobRunner must NOT carry @Transactional on the class or any
method — pins the 3-segment lifecycle rule.
Tests: 32 new unit tests + 21 regression tests in touched areas, all
53 green:
- ChatOriginTest (6) — value-object invariants + JSON round-trip.
- LocaleAwareToolCallbackToolContextTest (2) — decorator forward.
- DeliveryConfigTest (4) — Jackson round-trip + forward-compat.
- ToolCallbackToolContextForwardArchTest (2) — both ArchUnit guards.
- CronJobRunnerDeliveryGuardTest (3) — channel-cron prefix injection.
- AbstractCronResultDeliveryTest (4) — claim CAS + concurrent CAS.
- ChannelCronResultDeliveryTest (6) — supports / doDeliver / errors.
- ApprovalReplayContinuityTest (5) — Memento round-trip + corrupt
payload fallback + unknown-field tolerance.
Refs: #25, #16
141 lines
5.5 KiB
Java
141 lines
5.5 KiB
Java
package vip.mate.approval;
|
||
|
||
import java.time.Instant;
|
||
|
||
/**
|
||
* 待审批记录(消息驱动版)
|
||
* <p>
|
||
* 不再持有 CompletableFuture,状态流转由 status 字段驱动。
|
||
* 包含工具调用重放所需的全部信息。
|
||
*/
|
||
public class PendingApproval {
|
||
|
||
private final String pendingId;
|
||
private final String conversationId;
|
||
private final String userId;
|
||
private final String toolName;
|
||
private final String toolArguments;
|
||
private final String reason;
|
||
private final Instant createdAt;
|
||
|
||
// === 状态 ===
|
||
// pending → approved → consumed / denied / timeout / superseded
|
||
private volatile String status;
|
||
|
||
// === 重放相关字段 ===
|
||
|
||
/** 发起审批的渠道类型 */
|
||
private String channelType;
|
||
|
||
/** 发送者名称(审计日志) */
|
||
private String requesterName;
|
||
|
||
/** 回复目标标识(飞书 chatId、钉钉 conversationId 等) */
|
||
private String replyTarget;
|
||
|
||
/** 完整的 tool call 载荷(JSON),用于 replay 重放 */
|
||
private String toolCallPayload;
|
||
|
||
/** 同一轮中其他被阻塞的 tool calls(JSON 数组) */
|
||
private String siblingToolCalls;
|
||
|
||
/** Agent ID,重放时需要知道用哪个 Agent */
|
||
private String agentId;
|
||
|
||
/** 审批解决时间 */
|
||
private Instant resolvedAt;
|
||
|
||
/** 审批解决者 userId */
|
||
private String resolvedBy;
|
||
|
||
// === 增强字段(Phase 2: 结构化风险信息)===
|
||
|
||
/** Guard findings JSON(结构化风险发现列表) */
|
||
private String findingsJson;
|
||
|
||
/** 最高风险等级 */
|
||
private String maxSeverity;
|
||
|
||
/** 风险摘要 */
|
||
private String summary;
|
||
|
||
/**
|
||
* RFC-063r §2.12: serialized {@code ChatOrigin} snapshot captured when
|
||
* this approval was created. Lets cross-process / cross-restart replays
|
||
* (the user approves hours later from a different node) restore the
|
||
* original channel binding so the replayed tool call still delivers
|
||
* back to the correct channel. Persisted into
|
||
* {@code mate_tool_approval.chat_origin}.
|
||
*/
|
||
private String chatOrigin;
|
||
|
||
public PendingApproval(String pendingId, String conversationId, String userId,
|
||
String toolName, String toolArguments, String reason) {
|
||
this.pendingId = pendingId;
|
||
this.conversationId = conversationId;
|
||
this.userId = userId;
|
||
this.toolName = toolName;
|
||
this.toolArguments = toolArguments;
|
||
this.reason = reason;
|
||
this.createdAt = Instant.now();
|
||
this.status = "pending";
|
||
}
|
||
|
||
/**
|
||
* INTERNAL — recovery constructor for {@code ApprovalWorkflowService.recoverFromDb}.
|
||
* Preserves the persisted {@code createdAt} and {@code status} so TTL/GC keep working
|
||
* across JVM restarts. Do not use from business paths.
|
||
*/
|
||
PendingApproval(String pendingId, String conversationId, String userId,
|
||
String toolName, String toolArguments, String reason,
|
||
Instant createdAt, String status) {
|
||
this.pendingId = pendingId;
|
||
this.conversationId = conversationId;
|
||
this.userId = userId;
|
||
this.toolName = toolName;
|
||
this.toolArguments = toolArguments;
|
||
this.reason = reason;
|
||
this.createdAt = createdAt;
|
||
this.status = status;
|
||
}
|
||
|
||
// === Getters ===
|
||
|
||
public String getPendingId() { return pendingId; }
|
||
public String getConversationId() { return conversationId; }
|
||
public String getUserId() { return userId; }
|
||
public String getToolName() { return toolName; }
|
||
public String getToolArguments() { return toolArguments; }
|
||
public String getReason() { return reason; }
|
||
public Instant getCreatedAt() { return createdAt; }
|
||
public String getStatus() { return status; }
|
||
public String getChannelType() { return channelType; }
|
||
public String getRequesterName() { return requesterName; }
|
||
public String getReplyTarget() { return replyTarget; }
|
||
public String getToolCallPayload() { return toolCallPayload; }
|
||
public String getSiblingToolCalls() { return siblingToolCalls; }
|
||
public String getAgentId() { return agentId; }
|
||
public Instant getResolvedAt() { return resolvedAt; }
|
||
public String getResolvedBy() { return resolvedBy; }
|
||
public String getFindingsJson() { return findingsJson; }
|
||
public String getMaxSeverity() { return maxSeverity; }
|
||
public String getSummary() { return summary; }
|
||
public String getChatOrigin() { return chatOrigin; }
|
||
|
||
// === Setters ===
|
||
|
||
public void setStatus(String status) { this.status = status; }
|
||
public void setChannelType(String channelType) { this.channelType = channelType; }
|
||
public void setRequesterName(String requesterName) { this.requesterName = requesterName; }
|
||
public void setReplyTarget(String replyTarget) { this.replyTarget = replyTarget; }
|
||
public void setToolCallPayload(String toolCallPayload) { this.toolCallPayload = toolCallPayload; }
|
||
public void setSiblingToolCalls(String siblingToolCalls) { this.siblingToolCalls = siblingToolCalls; }
|
||
public void setAgentId(String agentId) { this.agentId = agentId; }
|
||
public void setResolvedAt(Instant resolvedAt) { this.resolvedAt = resolvedAt; }
|
||
public void setResolvedBy(String resolvedBy) { this.resolvedBy = resolvedBy; }
|
||
public void setFindingsJson(String findingsJson) { this.findingsJson = findingsJson; }
|
||
public void setMaxSeverity(String maxSeverity) { this.maxSeverity = maxSeverity; }
|
||
public void setSummary(String summary) { this.summary = summary; }
|
||
public void setChatOrigin(String chatOrigin) { this.chatOrigin = chatOrigin; }
|
||
}
|