refactor: split message runtime state

This commit is contained in:
Eyejoker
2026-03-23 06:17:17 +09:00
parent ff1d2c76d4
commit d4b19ab950
3 changed files with 668 additions and 518 deletions

View File

@@ -0,0 +1,122 @@
import {
getLatestMessageSeqAtOrBefore,
getLastHumanMessageTimestamp,
isPairedRoomJid,
} from './db.js';
import { filterProcessableMessages } from './bot-message-filter.js';
import { isTriggerAllowed, loadSenderAllowlist } from './sender-allowlist.js';
import { type Channel, type NewMessage, type RegisteredGroup } from './types.js';
const BOT_COLLABORATION_WINDOW_MS = 12 * 60 * 60 * 1000;
export function normalizeStoredSeqCursor(
cursor: string | undefined,
chatJid?: string,
): string {
if (!cursor) return '0';
if (/^\d+$/.test(cursor.trim())) return cursor.trim();
return String(getLatestMessageSeqAtOrBefore(cursor, chatJid));
}
export function advanceLastAgentCursor(
lastAgentTimestamps: Record<string, string>,
saveState: () => void,
chatJid: string,
cursorOrTimestamp: string | number,
): void {
if (typeof cursorOrTimestamp === 'number') {
lastAgentTimestamps[chatJid] = String(cursorOrTimestamp);
} else {
lastAgentTimestamps[chatJid] = normalizeStoredSeqCursor(
cursorOrTimestamp,
chatJid,
);
}
saveState();
}
export function createImplicitContinuationTracker(idleTimeout: number) {
const implicitContinuationUntil = new Map<string, number>();
return {
open(chatJid: string): void {
if (idleTimeout <= 0) return;
implicitContinuationUntil.set(chatJid, Date.now() + idleTimeout);
},
has(chatJid: string, messages: NewMessage[]): boolean {
const until = implicitContinuationUntil.get(chatJid);
if (!until) return false;
if (Date.now() > until) {
implicitContinuationUntil.delete(chatJid);
return false;
}
return messages.some((message) => message.is_from_me !== true);
},
};
}
export function shouldSkipBotOnlyCollaboration(
chatJid: string,
messages: NewMessage[],
): boolean {
if (isPairedRoomJid(chatJid)) return false;
const allFromBots = messages.every(
(message) => message.is_from_me || !!message.is_bot_message,
);
if (!allFromBots) return false;
const lastHuman = getLastHumanMessageTimestamp(chatJid);
if (!lastHuman) return true;
return (
Date.now() - new Date(lastHuman).getTime() > BOT_COLLABORATION_WINDOW_MS
);
}
export function hasAllowedTrigger(opts: {
chatJid: string;
messages: NewMessage[];
group: RegisteredGroup;
triggerPattern: RegExp;
hasImplicitContinuationWindow: (chatJid: string, messages: NewMessage[]) => boolean;
}): boolean {
const { chatJid, messages, group, triggerPattern, hasImplicitContinuationWindow } =
opts;
if (group.isMain === true || group.requiresTrigger === false) {
return true;
}
const allowlistCfg = loadSenderAllowlist();
const hasTrigger = messages.some(
(message) =>
triggerPattern.test(message.content.trim()) &&
(message.is_from_me ||
isTriggerAllowed(chatJid, message.sender, allowlistCfg)),
);
return hasTrigger || hasImplicitContinuationWindow(chatJid, messages);
}
export function getProcessableMessages(
chatJid: string,
messages: Parameters<typeof filterProcessableMessages>[0],
channel?: Channel,
) {
return filterProcessableMessages(
messages,
isPairedRoomJid(chatJid),
channel?.isOwnMessage?.bind(channel),
);
}
export function filterLoopingPairedBotMessages(
chatJid: string,
messages: Parameters<typeof filterProcessableMessages>[0],
failureText: string,
) {
if (!isPairedRoomJid(chatJid)) return messages;
return messages.filter(
(message) =>
!(message.is_bot_message && message.content.trim() === failureText),
);
}