Add built-in scheduler with group-scoped tasks
- Custom nanoclaw MCP server with scheduling tools (schedule_task, list_tasks, get_task, update_task, pause/resume/cancel_task, send_message) - Tasks run as full agents in their group's context - Support for cron, interval, and one-time schedules - Task run logging with duration and results - Main channel has Bash access for admin tasks (query DB, manage groups) - Other groups restricted to file operations only - Updated docs and requirements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
117
src/db.ts
117
src/db.ts
@@ -2,7 +2,7 @@ import Database from 'better-sqlite3';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { proto } from '@whiskeysockets/baileys';
|
||||
import { NewMessage } from './types.js';
|
||||
import { NewMessage, ScheduledTask, TaskRunLog } from './types.js';
|
||||
import { STORE_DIR } from './config.js';
|
||||
|
||||
let db: Database.Database;
|
||||
@@ -30,6 +30,34 @@ export function initDatabase(): void {
|
||||
FOREIGN KEY (chat_jid) REFERENCES chats(jid)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scheduled_tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
group_folder TEXT NOT NULL,
|
||||
chat_jid TEXT NOT NULL,
|
||||
prompt TEXT NOT NULL,
|
||||
schedule_type TEXT NOT NULL,
|
||||
schedule_value TEXT NOT NULL,
|
||||
next_run TEXT,
|
||||
last_run TEXT,
|
||||
last_result TEXT,
|
||||
status TEXT DEFAULT 'active',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_next_run ON scheduled_tasks(next_run);
|
||||
CREATE INDEX IF NOT EXISTS idx_status ON scheduled_tasks(status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_run_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL,
|
||||
run_at TEXT NOT NULL,
|
||||
duration_ms INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
result TEXT,
|
||||
error TEXT,
|
||||
FOREIGN KEY (task_id) REFERENCES scheduled_tasks(id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_run_logs ON task_run_logs(task_id, run_at);
|
||||
`);
|
||||
|
||||
// Add sender_name column if it doesn't exist (migration for existing DBs)
|
||||
@@ -89,3 +117,90 @@ export function getMessagesSince(chatJid: string, sinceTimestamp: string): NewMe
|
||||
`;
|
||||
return db.prepare(sql).all(chatJid, sinceTimestamp) as NewMessage[];
|
||||
}
|
||||
|
||||
// Scheduled Tasks
|
||||
|
||||
export function createTask(task: Omit<ScheduledTask, 'last_run' | 'last_result'>): void {
|
||||
db.prepare(`
|
||||
INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, schedule_type, schedule_value, next_run, status, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
task.id,
|
||||
task.group_folder,
|
||||
task.chat_jid,
|
||||
task.prompt,
|
||||
task.schedule_type,
|
||||
task.schedule_value,
|
||||
task.next_run,
|
||||
task.status,
|
||||
task.created_at
|
||||
);
|
||||
}
|
||||
|
||||
export function getTaskById(id: string): ScheduledTask | undefined {
|
||||
return db.prepare('SELECT * FROM scheduled_tasks WHERE id = ?').get(id) as ScheduledTask | undefined;
|
||||
}
|
||||
|
||||
export function getTasksForGroup(groupFolder: string): ScheduledTask[] {
|
||||
return db.prepare('SELECT * FROM scheduled_tasks WHERE group_folder = ? ORDER BY created_at DESC').all(groupFolder) as ScheduledTask[];
|
||||
}
|
||||
|
||||
export function getAllTasks(): ScheduledTask[] {
|
||||
return db.prepare('SELECT * FROM scheduled_tasks ORDER BY created_at DESC').all() as ScheduledTask[];
|
||||
}
|
||||
|
||||
export function updateTask(id: string, updates: Partial<Pick<ScheduledTask, 'prompt' | 'schedule_type' | 'schedule_value' | 'next_run' | 'status'>>): void {
|
||||
const fields: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
|
||||
if (updates.prompt !== undefined) { fields.push('prompt = ?'); values.push(updates.prompt); }
|
||||
if (updates.schedule_type !== undefined) { fields.push('schedule_type = ?'); values.push(updates.schedule_type); }
|
||||
if (updates.schedule_value !== undefined) { fields.push('schedule_value = ?'); values.push(updates.schedule_value); }
|
||||
if (updates.next_run !== undefined) { fields.push('next_run = ?'); values.push(updates.next_run); }
|
||||
if (updates.status !== undefined) { fields.push('status = ?'); values.push(updates.status); }
|
||||
|
||||
if (fields.length === 0) return;
|
||||
|
||||
values.push(id);
|
||||
db.prepare(`UPDATE scheduled_tasks SET ${fields.join(', ')} WHERE id = ?`).run(...values);
|
||||
}
|
||||
|
||||
export function deleteTask(id: string): void {
|
||||
db.prepare('DELETE FROM scheduled_tasks WHERE id = ?').run(id);
|
||||
db.prepare('DELETE FROM task_run_logs WHERE task_id = ?').run(id);
|
||||
}
|
||||
|
||||
export function getDueTasks(): ScheduledTask[] {
|
||||
const now = new Date().toISOString();
|
||||
return db.prepare(`
|
||||
SELECT * FROM scheduled_tasks
|
||||
WHERE status = 'active' AND next_run IS NOT NULL AND next_run <= ?
|
||||
ORDER BY next_run
|
||||
`).all(now) as ScheduledTask[];
|
||||
}
|
||||
|
||||
export function updateTaskAfterRun(id: string, nextRun: string | null, lastResult: string): void {
|
||||
const now = new Date().toISOString();
|
||||
db.prepare(`
|
||||
UPDATE scheduled_tasks
|
||||
SET next_run = ?, last_run = ?, last_result = ?, status = CASE WHEN ? IS NULL THEN 'completed' ELSE status END
|
||||
WHERE id = ?
|
||||
`).run(nextRun, now, lastResult, nextRun, id);
|
||||
}
|
||||
|
||||
export function logTaskRun(log: TaskRunLog): void {
|
||||
db.prepare(`
|
||||
INSERT INTO task_run_logs (task_id, run_at, duration_ms, status, result, error)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(log.task_id, log.run_at, log.duration_ms, log.status, log.result, log.error);
|
||||
}
|
||||
|
||||
export function getTaskRunLogs(taskId: string, limit = 10): TaskRunLog[] {
|
||||
return db.prepare(`
|
||||
SELECT task_id, run_at, duration_ms, status, result, error
|
||||
FROM task_run_logs
|
||||
WHERE task_id = ?
|
||||
ORDER BY run_at DESC
|
||||
LIMIT ?
|
||||
`).all(taskId, limit) as TaskRunLog[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user