feat(logs): populate send log detail and fix dedup re-claim condition

- dispatch: attach per-send detail (title/url/description/match/provider)
  so the admin log view shows message context, not just a bare ok flag
- idempotency: compare dedup expiry against claimed_at instead of the new
  expires_at so non-expired duplicates are rejected and expired keys re-claim
This commit is contained in:
RhenCloud 2026-08-18 10:29:10 +08:00
parent c6198e855f
commit c955db03c3
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
3 changed files with 14 additions and 2 deletions

View file

@ -6,6 +6,7 @@ import { log } from "../lib/log";
import { loadTranslations, t as translate, type Translations } from "../lib/i18n"; import { loadTranslations, t as translate, type Translations } from "../lib/i18n";
import type { SendRecord } from "../lib/send-log"; import type { SendRecord } from "../lib/send-log";
import { recordSendBatch } from "../lib/send-log-batch"; import { recordSendBatch } from "../lib/send-log-batch";
import { explainRoute } from "../config/schema";
import { messageTracker } from "../lib/message-tracker"; import { messageTracker } from "../lib/message-tracker";
import { import {
loadGroups, loadGroups,
@ -176,6 +177,15 @@ export async function dispatchEvent(
message.mentionRoleIds = route.discordRoleIds; message.mentionRoleIds = route.discordRoleIds;
} }
const detail: Record<string, unknown> = {
title: message.title,
url: message.url,
description: message.description?.slice(0, 500),
match: explainRoute(route),
provider: event.provider,
installationId: event.installationId,
};
for (const target of targets) { for (const target of targets) {
const targetStr = const targetStr =
target.platform === "telegram" target.platform === "telegram"
@ -196,6 +206,7 @@ export async function dispatchEvent(
deliveryId: string | undefined; deliveryId: string | undefined;
actor: string | undefined; actor: string | undefined;
action: string | undefined; action: string | undefined;
detail: Record<string, unknown>;
} = { } = {
ts: Date.now(), ts: Date.now(),
routeId: route.id, routeId: route.id,
@ -206,6 +217,7 @@ export async function dispatchEvent(
deliveryId: event.deliveryId, deliveryId: event.deliveryId,
actor: (event.payload.sender as { login?: string } | undefined)?.login, actor: (event.payload.sender as { login?: string } | undefined)?.login,
action: event.payload.action as string | undefined, action: event.payload.action as string | undefined,
detail,
}; };
const started = Date.now(); const started = Date.now();

View file

@ -43,7 +43,7 @@ export function d1IdempotencyStore(db: D1Database): IdempotencyStore {
`INSERT INTO dedup_keys (key, claimed_at, expires_at) VALUES (?, ?, ?) `INSERT INTO dedup_keys (key, claimed_at, expires_at) VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET ON CONFLICT(key) DO UPDATE SET
claimed_at = excluded.claimed_at, expires_at = excluded.expires_at claimed_at = excluded.claimed_at, expires_at = excluded.expires_at
WHERE dedup_keys.expires_at < excluded.expires_at`, WHERE dedup_keys.expires_at < excluded.claimed_at`,
) )
.bind(key, now, now + ttlSeconds * 1000) .bind(key, now, now + ttlSeconds * 1000)
.run(); .run();

View file

@ -50,7 +50,7 @@ function createMockD1(): {
dedupKeys.set(key, { claimedAt, expiresAt }); dedupKeys.set(key, { claimedAt, expiresAt });
return { success: true, meta: { changes: 1 } }; return { success: true, meta: { changes: 1 } };
} }
if (existing.expiresAt < expiresAt) { if (existing.expiresAt < claimedAt) {
dedupKeys.set(key, { claimedAt, expiresAt }); dedupKeys.set(key, { claimedAt, expiresAt });
return { success: true, meta: { changes: 1 } }; return { success: true, meta: { changes: 1 } };
} }