Fix timezone handling and message filtering
- Add TIMEZONE config using system timezone for cron expressions - Filter bot messages by content prefix instead of is_from_me (user shares WhatsApp account with bot) - Format messages as XML for cleaner agent parsing - Update schedule_task tool to clarify local time usage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,3 +16,7 @@ export const CONTAINER_TIMEOUT = parseInt(process.env.CONTAINER_TIMEOUT || '3000
|
||||
export const IPC_POLL_INTERVAL = 1000;
|
||||
|
||||
export const TRIGGER_PATTERN = new RegExp(`^@${ASSISTANT_NAME}\\b`, 'i');
|
||||
|
||||
// Timezone for scheduled tasks (cron expressions, etc.)
|
||||
// Uses system timezone by default
|
||||
export const TIMEZONE = process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
14
src/db.ts
14
src/db.ts
@@ -162,18 +162,19 @@ export function storeMessage(msg: proto.IWebMessageInfo, chatJid: string, isFrom
|
||||
.run(msgId, chatJid, sender, senderName, content, timestamp, isFromMe ? 1 : 0);
|
||||
}
|
||||
|
||||
export function getNewMessages(jids: string[], lastTimestamp: string): { messages: NewMessage[]; newTimestamp: string } {
|
||||
export function getNewMessages(jids: string[], lastTimestamp: string, botPrefix: string): { messages: NewMessage[]; newTimestamp: string } {
|
||||
if (jids.length === 0) return { messages: [], newTimestamp: lastTimestamp };
|
||||
|
||||
const placeholders = jids.map(() => '?').join(',');
|
||||
// Filter out bot's own messages by checking content prefix (not is_from_me, since user shares the account)
|
||||
const sql = `
|
||||
SELECT id, chat_jid, sender, sender_name, content, timestamp
|
||||
FROM messages
|
||||
WHERE timestamp > ? AND chat_jid IN (${placeholders})
|
||||
WHERE timestamp > ? AND chat_jid IN (${placeholders}) AND content NOT LIKE ?
|
||||
ORDER BY timestamp
|
||||
`;
|
||||
|
||||
const rows = db.prepare(sql).all(lastTimestamp, ...jids) as NewMessage[];
|
||||
const rows = db.prepare(sql).all(lastTimestamp, ...jids, `${botPrefix}:%`) as NewMessage[];
|
||||
|
||||
let newTimestamp = lastTimestamp;
|
||||
for (const row of rows) {
|
||||
@@ -183,14 +184,15 @@ export function getNewMessages(jids: string[], lastTimestamp: string): { message
|
||||
return { messages: rows, newTimestamp };
|
||||
}
|
||||
|
||||
export function getMessagesSince(chatJid: string, sinceTimestamp: string): NewMessage[] {
|
||||
export function getMessagesSince(chatJid: string, sinceTimestamp: string, botPrefix: string): NewMessage[] {
|
||||
// Filter out bot's own messages by checking content prefix
|
||||
const sql = `
|
||||
SELECT id, chat_jid, sender, sender_name, content, timestamp
|
||||
FROM messages
|
||||
WHERE chat_jid = ? AND timestamp > ?
|
||||
WHERE chat_jid = ? AND timestamp > ? AND content NOT LIKE ?
|
||||
ORDER BY timestamp
|
||||
`;
|
||||
return db.prepare(sql).all(chatJid, sinceTimestamp) as NewMessage[];
|
||||
return db.prepare(sql).all(chatJid, sinceTimestamp, `${botPrefix}:%`) as NewMessage[];
|
||||
}
|
||||
|
||||
export function createTask(task: Omit<ScheduledTask, 'last_run' | 'last_result'>): void {
|
||||
|
||||
22
src/index.ts
22
src/index.ts
@@ -16,7 +16,8 @@ import {
|
||||
DATA_DIR,
|
||||
TRIGGER_PATTERN,
|
||||
MAIN_GROUP_FOLDER,
|
||||
IPC_POLL_INTERVAL
|
||||
IPC_POLL_INTERVAL,
|
||||
TIMEZONE
|
||||
} from './config.js';
|
||||
import { RegisteredGroup, Session, NewMessage } from './types.js';
|
||||
import { initDatabase, storeMessage, storeChatMetadata, getNewMessages, getMessagesSince, getAllTasks, getTaskById, updateChatName, getAllChats, getLastGroupSync, setLastGroupSync } from './db.js';
|
||||
@@ -128,15 +129,18 @@ async function processMessage(msg: NewMessage): Promise<void> {
|
||||
|
||||
// Get all messages since last agent interaction so the session has full context
|
||||
const sinceTimestamp = lastAgentTimestamp[msg.chat_jid] || '';
|
||||
const missedMessages = getMessagesSince(msg.chat_jid, sinceTimestamp);
|
||||
const missedMessages = getMessagesSince(msg.chat_jid, sinceTimestamp, ASSISTANT_NAME);
|
||||
|
||||
const lines = missedMessages.map(m => {
|
||||
const d = new Date(m.timestamp);
|
||||
const date = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
const time = d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
||||
return `[${date} ${time}] ${m.sender_name}: ${m.content}`;
|
||||
// Escape XML special characters in content
|
||||
const escapeXml = (s: string) => s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
return `<message sender="${escapeXml(m.sender_name)}" time="${m.timestamp}">${escapeXml(m.content)}</message>`;
|
||||
});
|
||||
const prompt = lines.join('\n');
|
||||
const prompt = `<messages>\n${lines.join('\n')}\n</messages>`;
|
||||
|
||||
if (!prompt) return;
|
||||
|
||||
@@ -335,7 +339,7 @@ async function processTaskIpc(
|
||||
let nextRun: string | null = null;
|
||||
if (scheduleType === 'cron') {
|
||||
try {
|
||||
const interval = CronExpressionParser.parse(data.schedule_value);
|
||||
const interval = CronExpressionParser.parse(data.schedule_value, { tz: TIMEZONE });
|
||||
nextRun = interval.next().toISOString();
|
||||
} catch {
|
||||
logger.warn({ scheduleValue: data.schedule_value }, 'Invalid cron expression');
|
||||
@@ -512,7 +516,7 @@ async function startMessageLoop(): Promise<void> {
|
||||
while (true) {
|
||||
try {
|
||||
const jids = Object.keys(registeredGroups);
|
||||
const { messages } = getNewMessages(jids, lastTimestamp);
|
||||
const { messages } = getNewMessages(jids, lastTimestamp, ASSISTANT_NAME);
|
||||
|
||||
if (messages.length > 0) logger.info({ count: messages.length }, 'New messages');
|
||||
for (const msg of messages) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import pino from 'pino';
|
||||
import { CronExpressionParser } from 'cron-parser';
|
||||
import { getDueTasks, updateTaskAfterRun, logTaskRun, getTaskById, getAllTasks } from './db.js';
|
||||
import { ScheduledTask, RegisteredGroup } from './types.js';
|
||||
import { GROUPS_DIR, SCHEDULER_POLL_INTERVAL, DATA_DIR, MAIN_GROUP_FOLDER } from './config.js';
|
||||
import { GROUPS_DIR, SCHEDULER_POLL_INTERVAL, DATA_DIR, MAIN_GROUP_FOLDER, TIMEZONE } from './config.js';
|
||||
import { runContainerAgent, writeTasksSnapshot } from './container-runner.js';
|
||||
|
||||
const logger = pino({
|
||||
@@ -96,7 +96,7 @@ async function runTask(task: ScheduledTask, deps: SchedulerDependencies): Promis
|
||||
|
||||
let nextRun: string | null = null;
|
||||
if (task.schedule_type === 'cron') {
|
||||
const interval = CronExpressionParser.parse(task.schedule_value);
|
||||
const interval = CronExpressionParser.parse(task.schedule_value, { tz: TIMEZONE });
|
||||
nextRun = interval.next().toISOString();
|
||||
} else if (task.schedule_type === 'interval') {
|
||||
const ms = parseInt(task.schedule_value, 10);
|
||||
|
||||
Reference in New Issue
Block a user