package vip.mate.cron.delivery; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.messages.AssistantMessage; import vip.mate.channel.ChannelMessageRenderer; import vip.mate.cron.model.CronJobEntity; import vip.mate.dashboard.model.CronJobRunEntity; import vip.mate.dashboard.repository.CronJobRunMapper; import java.util.List; /** * RFC-063r §2.6.1: Template-Method base for {@link CronResultDelivery}. * *
Owns the cross-strategy invariants: *
Concrete subclasses implement only * {@link #doDeliver(CronJobEntity, AssistantMessage, CronJobRunEntity)}. */ @Slf4j public abstract class AbstractCronResultDelivery implements CronResultDelivery { /** * Default platform message-length cap used when the bound channel type * is unknown — matches Telegram's 4096 ceiling, the most permissive * among the small-cap platforms in {@link ChannelMessageRenderer#PLATFORM_LIMITS}. * RFC-063r §2.11 will refine this in PR-4 by passing per-channel limits * through {@code DeliveryOptions}. */ private static final int DEFAULT_RENDER_MAX_LEN = 4096; private final CronJobRunMapper runMapper; protected AbstractCronResultDelivery(CronJobRunMapper runMapper) { this.runMapper = runMapper; } @Override public final DeliveryOutcome deliver(CronJobEntity job, AssistantMessage result, CronJobRunEntity run) { if (!claimRun(run)) { return DeliveryOutcome.skipped("already-claimed-by-other-instance"); } try { DeliveryOutcome outcome = doDeliver(job, result, run); markDelivered(run, outcome); return outcome; } catch (Exception e) { markNotDelivered(run, e); throw e; } } /** Strategy's actual delivery work — invoked after CAS success. */ protected abstract DeliveryOutcome doDeliver( CronJobEntity job, AssistantMessage result, CronJobRunEntity run); /** * RFC-063r §2.11: filter thinking + tool-call markers and join the * platform-truncated segments into a single string. Concrete strategies * call this before handing the result to the channel-specific send call. * *
Null-safe — null/empty {@link AssistantMessage} returns "" so
* adapters never NPE.
*/
protected String renderForChannel(AssistantMessage msg, Long channelId) {
if (msg == null) return "";
String text = msg.getText() != null ? msg.getText() : "";
if (text.isEmpty()) return "";
try {
List SQL semantics gotcha: {@code IN (...)} never matches NULL. Legacy
* rows from before V57 (pre-RFC) may have null delivery_status, so the
* predicate explicitly tests {@code IS NULL OR IN (NONE, PENDING)} via
* a nested OR group rather than putting null inside the IN list.
*/
private boolean claimRun(CronJobRunEntity run) {
return runMapper.update(null, new LambdaUpdateWrapper