UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

222 lines (221 loc) 9.14 kB
/** * RemoteCommandRepository — 远程指令队列 + 状态持久化 (Drizzle ORM) * * 封装 remote_commands 和 remote_state 两张表的所有数据访问, * 替代 remote.ts 路由中散落的 18+ 条内联 SQL。 * * Drizzle 迁移策略: * - INSERT / UPDATE / SELECT 使用 drizzle 类型安全 API * - 简单聚合使用 drizzle sql`` 表达式 * - 保留 raw prepared statements 用于有性能要求的高频定时器查询 * * @module repository/remote/RemoteCommandRepository */ import { and, desc, eq, isNotNull, sql } from 'drizzle-orm'; import { getDrizzle } from '../../infrastructure/database/drizzle/index.js'; import { remoteCommands, remoteState } from '../../infrastructure/database/drizzle/schema.js'; import Logger from '../../infrastructure/logging/Logger.js'; /** Unix timestamp in seconds */ function unixNow() { return Math.floor(Date.now() / 1000); } export class RemoteCommandRepository { #drizzle; #db; #logger; // 高频定时器查询保留预编译语句 #pendingTimeoutStmt; #runningTimeoutStmt; #countByStatusStmt; constructor(db, drizzle) { this.#db = db; this.#drizzle = drizzle ?? getDrizzle(); this.#logger = Logger.getInstance(); // 确保 remote_state 表存在(原 remote.ts 内联 CREATE) this.#db.exec(`CREATE TABLE IF NOT EXISTS remote_state (key TEXT PRIMARY KEY, value TEXT, updated_at INTEGER)`); // 预编译高频语句(定时器每 30 秒调用) this.#pendingTimeoutStmt = this.#db.prepare('UPDATE remote_commands SET status = ?, completed_at = ? WHERE status = ? AND created_at < ?'); this.#runningTimeoutStmt = this.#db.prepare('UPDATE remote_commands SET status = ?, completed_at = ? WHERE status = ? AND claimed_at < ?'); this.#countByStatusStmt = this.#db.prepare('SELECT COUNT(*) as c FROM remote_commands WHERE status = ?'); } // ═══════════════════════════════════════════════════ // 写入操作 // ═══════════════════════════════════════════════════ /** 写入新的远程指令到队列 */ enqueue(input) { this.#drizzle .insert(remoteCommands) .values({ id: input.id, source: input.source, chatId: input.chatId || '', messageId: input.messageId || '', userId: input.userId || '', userName: input.userName || 'lark_user', command: input.command, status: 'pending', createdAt: unixNow(), }) .run(); } /** * 认领一条 pending 指令(CAS: pending → running) * @returns 是否成功(0 changes = 已被认领或不存在) */ claim(id) { const result = this.#drizzle .update(remoteCommands) .set({ status: 'running', claimedAt: unixNow() }) .where(and(eq(remoteCommands.id, id), eq(remoteCommands.status, 'pending'))) .run(); return result.changes > 0; } /** 提交指令执行结果(running → completed/failed/...) */ complete(id, resultText, status = 'completed') { this.#drizzle .update(remoteCommands) .set({ status, result: resultText, completedAt: unixNow(), }) .where(eq(remoteCommands.id, id)) .run(); } /** * 批量取消所有 pending 指令(IDE 重连时 flush) * @returns 被取消的指令列表 */ flushPending() { const now = unixNow(); // 先查出所有 pending const pending = this.#drizzle .select({ id: remoteCommands.id, command: remoteCommands.command, createdAt: remoteCommands.createdAt, }) .from(remoteCommands) .where(eq(remoteCommands.status, 'pending')) .orderBy(remoteCommands.createdAt) .all(); if (pending.length === 0) { return []; } // 批量标记为 cancelled this.#drizzle .update(remoteCommands) .set({ status: 'cancelled', result: '🗑 IDE 重连时自动清理(积压指令)', completedAt: now, }) .where(eq(remoteCommands.status, 'pending')) .run(); return pending; } // ═══════════════════════════════════════════════════ // 查询操作 // ═══════════════════════════════════════════════════ /** 获取最早的一条 pending 指令 */ findFirstPending() { const rows = this.#drizzle .select() .from(remoteCommands) .where(eq(remoteCommands.status, 'pending')) .orderBy(remoteCommands.createdAt) .limit(1) .all(); return rows[0] ?? null; } /** 根据 ID 获取指令 */ findById(id) { const rows = this.#drizzle .select() .from(remoteCommands) .where(eq(remoteCommands.id, id)) .limit(1) .all(); return rows[0] ?? null; } /** 获取历史记录(按创建时间降序) */ getHistory(limit = 20) { return this.#drizzle .select() .from(remoteCommands) .orderBy(desc(remoteCommands.createdAt)) .limit(limit) .all(); } /** 获取各状态的指令计数(用于 /lark/status 诊断面板) */ getStatusCounts() { const counts = { pending: 0, running: 0, completed: 0, timeout: 0 }; for (const s of ['pending', 'running', 'completed', 'timeout']) { const row = this.#countByStatusStmt.get(s); counts[s] = row?.c || 0; } return counts; } /** 查找最近一次 claim 记录(用于 IDE 心跳检测) */ findRecentClaim() { const rows = this.#drizzle .select({ claimedAt: remoteCommands.claimedAt }) .from(remoteCommands) .where(isNotNull(remoteCommands.claimedAt)) .orderBy(desc(remoteCommands.claimedAt)) .limit(1) .all(); const row = rows[0]; return row?.claimedAt != null ? { claimedAt: row.claimedAt } : null; } /** 查找最近有 chatId 的指令(用于恢复活跃会话) */ findRecentChatId() { const rows = this.#drizzle .select({ chatId: remoteCommands.chatId }) .from(remoteCommands) .where(sql `${remoteCommands.chatId} != ''`) .orderBy(desc(remoteCommands.createdAt)) .limit(1) .all(); return rows[0]?.chatId || null; } // ═══════════════════════════════════════════════════ // 超时清理(定时器使用预编译语句,已验证高频安全) // ═══════════════════════════════════════════════════ /** * 清理超时的 pending 和 running 指令 * @param pendingTimeoutSec pending 状态超时秒数 * @param runningTimeoutSec running 状态超时秒数 * @returns 清理的总条数 */ cleanupTimeouts(pendingTimeoutSec, runningTimeoutSec) { const now = unixNow(); const r1 = this.#pendingTimeoutStmt.run('timeout', now, 'pending', now - pendingTimeoutSec); const r2 = this.#runningTimeoutStmt.run('timeout', now, 'running', now - runningTimeoutSec); return (r1.changes || 0) + (r2.changes || 0); } // ═══════════════════════════════════════════════════ // remote_state 键值存储 // ═══════════════════════════════════════════════════ /** 持久化键值对到 remote_state */ setState(key, value) { this.#drizzle .insert(remoteState) .values({ key, value, updatedAt: unixNow() }) .onConflictDoUpdate({ target: remoteState.key, set: { value, updatedAt: unixNow() }, }) .run(); } /** 从 remote_state 读取值 */ getState(key) { const rows = this.#drizzle .select({ value: remoteState.value }) .from(remoteState) .where(eq(remoteState.key, key)) .limit(1) .all(); return rows[0]?.value ?? null; } }