feat(cron): suppressAgentReply flag for silent cron jobs

This commit is contained in:
matevip 2026-05-02 15:41:06 +08:00
parent 55f4ba1195
commit 25e93c4a89
2 changed files with 53 additions and 4 deletions

View File

@ -8,6 +8,7 @@ import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import vip.mate.audit.service.AuditEventService;
import vip.mate.cron.model.DeliveryConfig;
import java.util.List;
import java.util.Optional;
@ -40,6 +41,18 @@ public class CronDeliveryListener {
@Async("cronDeliveryExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
public void onCompleted(CronJobCompletedEvent ev) {
// RFC-03 Lane C1: explicit silent mode short-circuits delivery
// resolution. Tools already executed, the run row is already
// persisted only the agent's narrative reply is withheld from
// the channel. Status row stays NONE (same as "no strategy
// matched") so dashboards keep one canonical "not delivered"
// bucket.
DeliveryConfig dc = ev.job() != null ? ev.job().getDeliveryConfig() : null;
if (dc != null && dc.isAgentReplySuppressed()) {
log.debug("[CronDelivery] Job {} suppressAgentReply=true; skipping delivery",
ev.job().getId());
return;
}
Optional<CronResultDelivery> strategy = deliveries.stream()
.filter(d -> d.supports(ev.job()))
.findFirst();

View File

@ -33,30 +33,66 @@ public record DeliveryConfig(
* <p>Nullable for backwards compat with rows written before this
* field was added (V62 baseline).
*/
@Nullable String userId
@Nullable String userId,
/**
* RFC-03 Lane C1 when {@code TRUE}, {@code CronDeliveryListener}
* skips strategy resolution entirely and the run completes with
* {@code delivery_status='NONE'}. Tools still execute, the run row
* is still persisted, audit + token-usage all work only the
* agent's narrative reply is suppressed from the channel.
*
* <p>Use cases (QwenPaw #2452): noon health-check cron that just
* pokes a database and writes structured output, project-weekly
* report jobs that drop a file into a knowledge base, internal
* pipelines that don't need an IM-visible "I did the thing"
* trailing message.
*
* <p>{@code Boolean} (not {@code boolean}) so JSON deserialization
* of pre-V75 rows leaves the field {@code null} the listener
* treats {@code null} and {@code FALSE} identically (deliver as
* usual), preserving every existing job's behavior.
*/
@Nullable Boolean suppressAgentReply
) {
/** 3-arg legacy constructor preserved so older deserialized rows still work. */
public DeliveryConfig(@Nullable String targetId,
@Nullable String threadId,
@Nullable String accountId) {
this(targetId, threadId, accountId, null);
this(targetId, threadId, accountId, null, null);
}
/** 4-arg legacy constructor — pre-RFC-03 callers that already carry userId. */
public DeliveryConfig(@Nullable String targetId,
@Nullable String threadId,
@Nullable String accountId,
@Nullable String userId) {
this(targetId, threadId, accountId, userId, null);
}
/** Convert from the {@link ChannelTarget} carried on a {@code ChatOrigin}. */
public static DeliveryConfig from(@Nullable ChannelTarget t) {
if (t == null) return null;
return new DeliveryConfig(t.targetId(), t.threadId(), t.accountId(), null);
return new DeliveryConfig(t.targetId(), t.threadId(), t.accountId(), null, null);
}
/** Convert from {@link ChannelTarget} + the requester's senderId. */
public static DeliveryConfig from(@Nullable ChannelTarget t, @Nullable String userId) {
if (t == null) return null;
return new DeliveryConfig(t.targetId(), t.threadId(), t.accountId(), userId);
return new DeliveryConfig(t.targetId(), t.threadId(), t.accountId(), userId, null);
}
/** Convert back to a {@link ChannelTarget} for ChatOrigin reconstruction. */
public ChannelTarget toChannelTarget() {
return new ChannelTarget(targetId, threadId, accountId);
}
/**
* RFC-03 Lane C1 convenience predicate so listeners can short-circuit
* delivery resolution without unwrapping the {@code Boolean}. Treats
* {@code null} as {@code false} (the historical default).
*/
public boolean isAgentReplySuppressed() {
return Boolean.TRUE.equals(suppressAgentReply);
}
}