1539 lines
52 KiB
Markdown
1539 lines
52 KiB
Markdown
---
|
||
version: 2.0
|
||
last-updated: 2026-03-09
|
||
status: design
|
||
---
|
||
|
||
# 08 — Heartbeat 与 Cron 定时任务
|
||
|
||
## 一、概述
|
||
|
||
QimingClaw V2 需要**定时任务**能力来支持:
|
||
|
||
1. **Heartbeat(心跳)** - 定期检查服务器连接、同步状态、执行后台维护
|
||
2. **Cron Jobs(定时任务)** - 用户定义的定时执行任务(提醒、定时报告、自动化等)
|
||
|
||
### 服务器已有能力
|
||
|
||
Qiming 服务器已有定时会话任务 API:
|
||
|
||
| 端点 | 能力 |
|
||
|------|------|
|
||
| `POST /api/agent/task/create` | 创建定时会话 |
|
||
| `POST /api/agent/task/update` | 更新定时会话 |
|
||
| `POST /api/agent/task/list` | 查询定时会话列表 |
|
||
| `POST /api/agent/task/cancel/:id` | 取消定时会话 |
|
||
| `GET /api/agent/task/cron/list` | 可选定时范围 |
|
||
| `GET /api/task/cron/list` | 可选定时范围 - 任务 |
|
||
|
||
客户端将复用这些 API,并提供:
|
||
- 本地任务缓存和离线查看
|
||
- 更丰富的任务类型(提醒、脚本、Webhook)
|
||
- Heartbeat 心跳检查能力
|
||
|
||
```
|
||
┌────────────────────────────────────────────────────────────────────────┐
|
||
│ 定时任务架构 │
|
||
├────────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||
│ │ Heartbeat (心跳) │ │
|
||
│ │ - 周期: 30s ~ 5min (可配置) │ │
|
||
│ │ - 职责: 连接检查、状态同步、后台维护 │ │
|
||
│ │ - 触发: 系统自动 │ │
|
||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||
│ │ Cron Jobs (定时任务) │ │
|
||
│ │ - 周期: 用户定义 (cron 表达式) │ │
|
||
│ │ - 职责: 定时提醒、定时报告、自动化任务 │ │
|
||
│ │ - 触发: 用户创建 / 服务器同步 │ │
|
||
│ │ - 复用服务器 /api/agent/task/* API │ │
|
||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||
│ │ Scheduler Core │ │
|
||
│ │ - 统一调度引擎 │ │
|
||
│ │ - 任务持久化 (SQLite) │ │
|
||
│ │ - 错过任务恢复 │ │
|
||
│ │ - 与服务器同步 │ │
|
||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 二、Heartbeat(心跳)
|
||
|
||
### 2.1 设计目标
|
||
|
||
Heartbeat 是**轻量级周期性检查**,用于:
|
||
|
||
1. **服务器连接健康检查** - 检测与 Qiming Server 的连接状态
|
||
2. **配置同步状态检查** - 检查 MCP、Skills、模型等配置是否需要同步
|
||
3. **后台维护** - 清理过期缓存、更新状态、执行低优先级任务
|
||
4. **用户提醒** - 检查是否有新的通知、消息等
|
||
|
||
### 2.2 架构设计
|
||
|
||
```
|
||
┌────────────────────────────────────────────────────────────────────────┐
|
||
│ Heartbeat 架构 │
|
||
├────────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||
│ │ Main Process │ │ HeartbeatEngine │ │ HeartbeatTasks │ │
|
||
│ │ │────▶│ │────▶│ │ │
|
||
│ │ - 应用启动 │ │ - 定时器管理 │ │ - 检查任务列表 │ │
|
||
│ │ - 窗口状态 │ │ - 防重复执行 │ │ - 执行检查逻辑 │ │
|
||
│ │ - 系统 tray │ │ - 错误恢复 │ │ - 通知 UI │ │
|
||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||
│ │ Heartbeat Tasks (可配置) │ │
|
||
│ │ │ │
|
||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ │
|
||
│ │ │ Server │ │ Config │ │ Maintenance │ │ Notify │ │ │
|
||
│ │ │ Health │ │ Sync Check │ │ Tasks │ │ Check │ │ │
|
||
│ │ │ Check │ │ │ │ │ │ │ │ │
|
||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │ │
|
||
│ │ │ │
|
||
│ │ 每次心跳可选择执行哪些任务(根据上次执行时间间隔) │ │
|
||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.3 HeartbeatEngine 实现
|
||
|
||
```typescript
|
||
/**
|
||
* 心跳引擎
|
||
*
|
||
* 设计原则:
|
||
* - 轻量级,不阻塞主线程
|
||
* - 可配置检查间隔和任务
|
||
* - 支持动态启用/禁用
|
||
* - 错误自动恢复
|
||
*/
|
||
class HeartbeatEngine {
|
||
private intervalMs: number;
|
||
private timer: NodeJS.Timeout | null = null;
|
||
private running = false;
|
||
private lastCheckAt: number = 0;
|
||
private tasks: Map<string, HeartbeatTask> = new Map();
|
||
private taskStates: Map<string, TaskState> = new Map();
|
||
|
||
constructor(config: HeartbeatConfig) {
|
||
this.intervalMs = config.intervalMs || 60000; // 默认 1 分钟
|
||
this.registerDefaultTasks();
|
||
}
|
||
|
||
/**
|
||
* 注册默认心跳任务
|
||
*/
|
||
private registerDefaultTasks(): void {
|
||
// 服务器健康检查 - 每次心跳都执行
|
||
this.registerTask({
|
||
id: "server-health",
|
||
name: "服务器健康检查",
|
||
priority: 1,
|
||
minIntervalMs: 0, // 每次都执行
|
||
execute: async (context) => {
|
||
return await this.checkServerHealth(context);
|
||
},
|
||
});
|
||
|
||
// 配置同步检查 - 每 5 分钟
|
||
this.registerTask({
|
||
id: "config-sync",
|
||
name: "配置同步检查",
|
||
priority: 2,
|
||
minIntervalMs: 5 * 60 * 1000,
|
||
execute: async (context) => {
|
||
return await this.checkConfigSync(context);
|
||
},
|
||
});
|
||
|
||
// 会话同步检查 - 每 2 分钟
|
||
this.registerTask({
|
||
id: "session-sync",
|
||
name: "会话同步检查",
|
||
priority: 3,
|
||
minIntervalMs: 2 * 60 * 1000,
|
||
execute: async (context) => {
|
||
return await this.checkSessionSync(context);
|
||
},
|
||
});
|
||
|
||
// 后台维护 - 每 10 分钟
|
||
this.registerTask({
|
||
id: "maintenance",
|
||
name: "后台维护",
|
||
priority: 10,
|
||
minIntervalMs: 10 * 60 * 1000,
|
||
execute: async (context) => {
|
||
return await this.runMaintenance(context);
|
||
},
|
||
});
|
||
|
||
// 通知检查 - 每次心跳
|
||
this.registerTask({
|
||
id: "notify-check",
|
||
name: "通知检查",
|
||
priority: 5,
|
||
minIntervalMs: 0,
|
||
execute: async (context) => {
|
||
return await this.checkNotifications(context);
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 注册心跳任务
|
||
*/
|
||
registerTask(task: HeartbeatTask): void {
|
||
this.tasks.set(task.id, task);
|
||
this.taskStates.set(task.id, {
|
||
lastRunAt: 0,
|
||
lastResult: null,
|
||
errorCount: 0,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 启动心跳
|
||
*/
|
||
start(): void {
|
||
if (this.running) return;
|
||
|
||
this.running = true;
|
||
this.timer = setInterval(() => {
|
||
this.tick().catch((err) => {
|
||
console.error("[Heartbeat] Tick error:", err);
|
||
});
|
||
}, this.intervalMs);
|
||
|
||
// 立即执行一次
|
||
this.tick().catch(console.error);
|
||
|
||
console.log(`[Heartbeat] Started with interval ${this.intervalMs}ms`);
|
||
}
|
||
|
||
/**
|
||
* 停止心跳
|
||
*/
|
||
stop(): void {
|
||
if (this.timer) {
|
||
clearInterval(this.timer);
|
||
this.timer = null;
|
||
}
|
||
this.running = false;
|
||
console.log("[Heartbeat] Stopped");
|
||
}
|
||
|
||
/**
|
||
* 执行一次心跳检查
|
||
*/
|
||
private async tick(): Promise<HeartbeatResult> {
|
||
const now = Date.now();
|
||
const context: HeartbeatContext = {
|
||
now,
|
||
lastCheckAt: this.lastCheckAt,
|
||
appState: this.getAppState(),
|
||
};
|
||
|
||
const results: Record<string, TaskResult> = {};
|
||
|
||
// 按优先级排序执行
|
||
const sortedTasks = Array.from(this.tasks.values())
|
||
.sort((a, b) => a.priority - b.priority);
|
||
|
||
for (const task of sortedTasks) {
|
||
const state = this.taskStates.get(task.id)!;
|
||
|
||
// 检查是否需要执行(根据最小间隔)
|
||
if (task.minIntervalMs > 0 && now - state.lastRunAt < task.minIntervalMs) {
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
const result = await task.execute(context);
|
||
results[task.id] = result;
|
||
state.lastRunAt = now;
|
||
state.lastResult = result;
|
||
state.errorCount = 0;
|
||
|
||
// 如果任务有需要通知的内容
|
||
if (result.notify) {
|
||
this.notifyUI(task.id, result);
|
||
}
|
||
} catch (error) {
|
||
state.errorCount++;
|
||
state.lastResult = { error: String(error) };
|
||
results[task.id] = { error: String(error) };
|
||
console.error(`[Heartbeat] Task ${task.id} error:`, error);
|
||
}
|
||
}
|
||
|
||
this.lastCheckAt = now;
|
||
|
||
return {
|
||
timestamp: now,
|
||
results,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取应用状态
|
||
*/
|
||
private getAppState(): AppState {
|
||
// 返回当前应用状态(窗口是否可见、是否空闲等)
|
||
return {
|
||
windowVisible: true, // 从 BrowserWindow 获取
|
||
idle: false, // 从 powerMonitor 获取
|
||
online: navigator.onLine,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 通知 UI
|
||
*/
|
||
private notifyUI(taskId: string, result: TaskResult): void {
|
||
// 通过 IPC 或 EventEmitter 通知渲染进程
|
||
const { BrowserWindow } = require("electron");
|
||
const win = BrowserWindow.getAllWindows()[0];
|
||
if (win) {
|
||
win.webContents.send("heartbeat:notify", { taskId, result });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 服务器健康检查
|
||
*/
|
||
private async checkServerHealth(context: HeartbeatContext): Promise<TaskResult> {
|
||
const qimingClient = getQimingClient();
|
||
|
||
if (!qimingClient || !qimingClient.isConnected()) {
|
||
return {
|
||
status: "disconnected",
|
||
notify: { type: "warning", message: "服务器连接已断开" },
|
||
};
|
||
}
|
||
|
||
try {
|
||
const start = Date.now();
|
||
const health = await qimingClient.request("GET", "/api/health");
|
||
const latency = Date.now() - start;
|
||
|
||
return {
|
||
status: "connected",
|
||
latency,
|
||
version: health.version,
|
||
};
|
||
} catch (error) {
|
||
return {
|
||
status: "error",
|
||
error: String(error),
|
||
notify: { type: "error", message: "服务器健康检查失败" },
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 配置同步检查
|
||
*/
|
||
private async checkConfigSync(context: HeartbeatContext): Promise<TaskResult> {
|
||
const syncService = getQimingSyncService();
|
||
const status = syncService.getSyncStatus();
|
||
|
||
// 检查是否有需要同步的配置
|
||
const pendingChanges = status.pendingChanges;
|
||
if (pendingChanges > 0) {
|
||
// 触发同步
|
||
await syncService.syncAll();
|
||
return {
|
||
status: "synced",
|
||
changes: pendingChanges,
|
||
notify: { type: "info", message: `已同步 ${pendingChanges} 个配置变更` },
|
||
};
|
||
}
|
||
|
||
return { status: "uptodate" };
|
||
}
|
||
|
||
/**
|
||
* 会话同步检查
|
||
*/
|
||
private async checkSessionSync(context: HeartbeatContext): Promise<TaskResult> {
|
||
const chatEngine = getChatEngine();
|
||
const pendingMessages = chatEngine.getPendingMessages();
|
||
|
||
if (pendingMessages.length > 0) {
|
||
await chatEngine.syncMessages();
|
||
return {
|
||
status: "synced",
|
||
count: pendingMessages.length,
|
||
};
|
||
}
|
||
|
||
return { status: "uptodate" };
|
||
}
|
||
|
||
/**
|
||
* 后台维护
|
||
*/
|
||
private async runMaintenance(context: HeartbeatContext): Promise<TaskResult> {
|
||
const tasks: string[] = [];
|
||
|
||
// 1. 清理过期缓存
|
||
await this.cleanupExpiredCache();
|
||
tasks.push("cache-cleanup");
|
||
|
||
// 2. 清理临时文件
|
||
await this.cleanupTempFiles();
|
||
tasks.push("temp-cleanup");
|
||
|
||
// 3. 检查更新
|
||
if (context.appState.idle) {
|
||
await this.checkForUpdates();
|
||
tasks.push("update-check");
|
||
}
|
||
|
||
return { status: "done", tasks };
|
||
}
|
||
|
||
/**
|
||
* 通知检查
|
||
*/
|
||
private async checkNotifications(context: HeartbeatContext): Promise<TaskResult> {
|
||
const qimingClient = getQimingClient();
|
||
|
||
if (!qimingClient?.isConnected()) {
|
||
return { status: "skipped" };
|
||
}
|
||
|
||
try {
|
||
const notifications = await qimingClient.request(
|
||
"GET",
|
||
"/api/notifications/pending",
|
||
);
|
||
|
||
if (notifications.length > 0) {
|
||
return {
|
||
status: "found",
|
||
count: notifications.length,
|
||
notify: {
|
||
type: "notification",
|
||
data: notifications,
|
||
},
|
||
};
|
||
}
|
||
|
||
return { status: "empty" };
|
||
} catch (error) {
|
||
return { status: "error", error: String(error) };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取心跳状态
|
||
*/
|
||
getStatus(): HeartbeatStatus {
|
||
return {
|
||
running: this.running,
|
||
intervalMs: this.intervalMs,
|
||
lastCheckAt: this.lastCheckAt,
|
||
tasks: Object.fromEntries(this.taskStates),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 动态调整间隔
|
||
*/
|
||
setInterval(ms: number): void {
|
||
this.intervalMs = ms;
|
||
if (this.running) {
|
||
this.stop();
|
||
this.start();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== 类型定义 ====================
|
||
|
||
interface HeartbeatConfig {
|
||
intervalMs?: number; // 心跳间隔,默认 60000 (1分钟)
|
||
tasks?: HeartbeatTask[]; // 自定义任务
|
||
}
|
||
|
||
interface HeartbeatTask {
|
||
id: string;
|
||
name: string;
|
||
priority: number; // 执行优先级,数字越小越先执行
|
||
minIntervalMs: number; // 最小执行间隔,0 表示每次都执行
|
||
execute: (context: HeartbeatContext) => Promise<TaskResult>;
|
||
}
|
||
|
||
interface HeartbeatContext {
|
||
now: number;
|
||
lastCheckAt: number;
|
||
appState: AppState;
|
||
}
|
||
|
||
interface AppState {
|
||
windowVisible: boolean;
|
||
idle: boolean;
|
||
online: boolean;
|
||
}
|
||
|
||
interface TaskState {
|
||
lastRunAt: number;
|
||
lastResult: TaskResult | null;
|
||
errorCount: number;
|
||
}
|
||
|
||
interface TaskResult {
|
||
status?: string;
|
||
error?: string;
|
||
notify?: NotifyPayload;
|
||
[key: string]: any;
|
||
}
|
||
|
||
interface NotifyPayload {
|
||
type: "info" | "warning" | "error" | "notification";
|
||
message?: string;
|
||
data?: any;
|
||
}
|
||
|
||
interface HeartbeatResult {
|
||
timestamp: number;
|
||
results: Record<string, TaskResult>;
|
||
}
|
||
|
||
interface HeartbeatStatus {
|
||
running: boolean;
|
||
intervalMs: number;
|
||
lastCheckAt: number;
|
||
tasks: Record<string, TaskState>;
|
||
}
|
||
```
|
||
|
||
### 2.4 Heartbeat 与 UI 交互
|
||
|
||
```typescript
|
||
// Main Process
|
||
ipcMain.handle("heartbeat:getStatus", () => {
|
||
return heartbeatEngine.getStatus();
|
||
});
|
||
|
||
ipcMain.handle("heartbeat:setInterval", (_, ms: number) => {
|
||
heartbeatEngine.setInterval(ms);
|
||
return { success: true };
|
||
});
|
||
|
||
// Renderer Process
|
||
ipcRenderer.on("heartbeat:notify", (_, { taskId, result }) => {
|
||
if (result.notify) {
|
||
switch (result.notify.type) {
|
||
case "warning":
|
||
showToast.warning(result.notify.message);
|
||
break;
|
||
case "error":
|
||
showToast.error(result.notify.message);
|
||
break;
|
||
case "info":
|
||
showToast.info(result.notify.message);
|
||
break;
|
||
case "notification":
|
||
showNotifications(result.notify.data);
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 三、Cron Jobs(定时任务)
|
||
|
||
### 3.1 设计目标
|
||
|
||
Cron Jobs 是**用户定义的定时任务**,用于:
|
||
|
||
1. **定时提醒** - "每天 9:00 提醒我开会"
|
||
2. **定时报告** - "每周一 8:00 发送周报"
|
||
3. **自动化任务** - "每小时检查一次某个服务状态"
|
||
4. **与 Agent 结合** - "每天 18:00 让 Agent 总结今天的工作"
|
||
|
||
### 3.2 架构设计
|
||
|
||
```
|
||
┌────────────────────────────────────────────────────────────────────────┐
|
||
│ Cron Jobs 架构 │
|
||
├────────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||
│ │ CronScheduler (调度引擎) │ │
|
||
│ │ │ │
|
||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||
│ │ │ cron-parser │ │ job-queue │ │ executor │ │ │
|
||
│ │ │ 解析表达式 │ │ 任务队列 │ │ 执行器 │ │ │
|
||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ - 支持 cron 表达式 │ │
|
||
│ │ - 支持一次性任务 (at) │ │
|
||
│ │ - 持久化到 SQLite │ │
|
||
│ │ - 错过任务恢复 (应用关闭期间) │ │
|
||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||
│ │ Job Types (任务类型) │ │
|
||
│ │ │ │
|
||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||
│ │ │ Reminder │ │ Agent Task │ │ Custom │ │ │
|
||
│ │ │ 提醒任务 │ │ Agent 任务 │ │ 自定义任务 │ │ │
|
||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ ┌─────────────┐ ┌─────────────┐ │ │
|
||
│ │ │ Webhook │ │ Script │ │ │
|
||
│ │ │ HTTP 回调 │ │ 脚本执行 │ │ │
|
||
│ │ └─────────────┘ └─────────────┘ │ │
|
||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||
│ │ Server Sync (服务器同步) │ │
|
||
│ │ │ │
|
||
│ │ - 从服务器同步定时任务定义 │ │
|
||
│ │ - 任务执行结果推送到服务器 │ │
|
||
│ │ - 支持多设备任务同步 │ │
|
||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.3 CronScheduler 实现
|
||
|
||
```typescript
|
||
import { CronJob } from "cron";
|
||
import { getDb } from "../db";
|
||
|
||
/**
|
||
* Cron 调度器
|
||
*
|
||
* 设计原则:
|
||
* - 支持 cron 表达式和一次性任务
|
||
* - 持久化存储,应用重启后恢复
|
||
* - 错过任务自动执行(可配置)
|
||
* - 与服务器同步
|
||
*/
|
||
class CronScheduler {
|
||
private jobs: Map<string, CronJob> = new Map();
|
||
private db: Database;
|
||
|
||
constructor() {
|
||
this.db = getDb();
|
||
this.initDb();
|
||
this.loadJobsFromDb();
|
||
}
|
||
|
||
/**
|
||
* 初始化数据库表
|
||
*/
|
||
private initDb(): void {
|
||
this.db.exec(`
|
||
CREATE TABLE IF NOT EXISTS cron_jobs (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
description TEXT,
|
||
cron_expression TEXT,
|
||
run_at TEXT, -- 一次性任务的执行时间 (ISO 8601)
|
||
timezone TEXT DEFAULT 'Asia/Shanghai',
|
||
job_type TEXT NOT NULL, -- reminder, agent, webhook, script, custom
|
||
job_config TEXT, -- JSON 配置
|
||
enabled INTEGER DEFAULT 1,
|
||
last_run_at INTEGER,
|
||
next_run_at INTEGER,
|
||
created_at INTEGER,
|
||
updated_at INTEGER,
|
||
synced_to_server INTEGER DEFAULT 0,
|
||
server_job_id TEXT
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_cron_jobs_next_run ON cron_jobs(next_run_at);
|
||
CREATE INDEX IF NOT EXISTS idx_cron_jobs_enabled ON cron_jobs(enabled);
|
||
`);
|
||
}
|
||
|
||
/**
|
||
* 从数据库加载任务
|
||
*/
|
||
private loadJobsFromDb(): void {
|
||
const jobs = this.db.prepare(`
|
||
SELECT * FROM cron_jobs WHERE enabled = 1
|
||
`).all() as CronJobRecord[];
|
||
|
||
for (const job of jobs) {
|
||
this.scheduleJob(job);
|
||
}
|
||
|
||
// 检查错过的任务
|
||
this.recoverMissedJobs();
|
||
}
|
||
|
||
/**
|
||
* 创建定时任务
|
||
*/
|
||
async createJob(input: CreateJobInput): Promise<CronJobRecord> {
|
||
const id = input.id || this.generateId();
|
||
const now = Date.now();
|
||
|
||
// 计算下次执行时间
|
||
const nextRunAt = this.calculateNextRun(input);
|
||
|
||
const job: CronJobRecord = {
|
||
id,
|
||
name: input.name,
|
||
description: input.description,
|
||
cron_expression: input.cronExpression || null,
|
||
run_at: input.runAt ? new Date(input.runAt).toISOString() : null,
|
||
timezone: input.timezone || "Asia/Shanghai",
|
||
job_type: input.jobType,
|
||
job_config: JSON.stringify(input.jobConfig),
|
||
enabled: 1,
|
||
last_run_at: null,
|
||
next_run_at: nextRunAt?.getTime() || null,
|
||
created_at: now,
|
||
updated_at: now,
|
||
synced_to_server: 0,
|
||
server_job_id: null,
|
||
};
|
||
|
||
// 保存到数据库
|
||
this.db.prepare(`
|
||
INSERT INTO cron_jobs (
|
||
id, name, description, cron_expression, run_at, timezone,
|
||
job_type, job_config, enabled, last_run_at, next_run_at,
|
||
created_at, updated_at, synced_to_server, server_job_id
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
`).run(
|
||
job.id, job.name, job.description, job.cron_expression, job.run_at,
|
||
job.timezone, job.job_type, job.job_config, job.enabled, job.last_run_at,
|
||
job.next_run_at, job.created_at, job.updated_at, job.synced_to_server,
|
||
job.server_job_id
|
||
);
|
||
|
||
// 如果启用,加入调度
|
||
if (job.enabled) {
|
||
this.scheduleJob(job);
|
||
}
|
||
|
||
// 同步到服务器
|
||
this.syncJobToServer(job);
|
||
|
||
return job;
|
||
}
|
||
|
||
/**
|
||
* 更新定时任务
|
||
*/
|
||
async updateJob(id: string, updates: UpdateJobInput): Promise<CronJobRecord | null> {
|
||
const existing = this.db.prepare("SELECT * FROM cron_jobs WHERE id = ?").get(id) as CronJobRecord;
|
||
if (!existing) return null;
|
||
|
||
// 停止旧的调度
|
||
this.stopJob(id);
|
||
|
||
const now = Date.now();
|
||
const updated = { ...existing, ...updates, updated_at: now };
|
||
|
||
// 重新计算下次执行时间
|
||
if (updates.cronExpression !== undefined || updates.runAt !== undefined) {
|
||
updated.next_run_at = this.calculateNextRun({
|
||
cronExpression: updated.cron_expression,
|
||
runAt: updated.run_at,
|
||
})?.getTime() || null;
|
||
}
|
||
|
||
// 更新数据库
|
||
this.db.prepare(`
|
||
UPDATE cron_jobs SET
|
||
name = ?, description = ?, cron_expression = ?, run_at = ?,
|
||
timezone = ?, job_type = ?, job_config = ?, enabled = ?,
|
||
last_run_at = ?, next_run_at = ?, updated_at = ?, synced_to_server = 0
|
||
WHERE id = ?
|
||
`).run(
|
||
updated.name, updated.description, updated.cron_expression, updated.run_at,
|
||
updated.timezone, updated.job_type, updated.job_config, updated.enabled,
|
||
updated.last_run_at, updated.next_run_at, updated.updated_at, id
|
||
);
|
||
|
||
// 如果启用,重新调度
|
||
if (updated.enabled) {
|
||
this.scheduleJob(updated);
|
||
}
|
||
|
||
// 同步到服务器
|
||
this.syncJobToServer(updated);
|
||
|
||
return updated;
|
||
}
|
||
|
||
/**
|
||
* 删除定时任务
|
||
*/
|
||
async deleteJob(id: string): Promise<boolean> {
|
||
// 停止调度
|
||
this.stopJob(id);
|
||
|
||
// 从数据库删除
|
||
const result = this.db.prepare("DELETE FROM cron_jobs WHERE id = ?").run(id);
|
||
|
||
// 通知服务器
|
||
this.deleteJobFromServer(id);
|
||
|
||
return result.changes > 0;
|
||
}
|
||
|
||
/**
|
||
* 调度任务
|
||
*/
|
||
private scheduleJob(job: CronJobRecord): void {
|
||
// 如果已在调度中,先停止
|
||
this.stopJob(job.id);
|
||
|
||
const jobConfig = JSON.parse(job.job_config || "{}");
|
||
|
||
if (job.cron_expression) {
|
||
// 周期性任务
|
||
const cronJob = new CronJob(
|
||
job.cron_expression,
|
||
() => this.executeJob(job),
|
||
null,
|
||
true, // start
|
||
job.timezone
|
||
);
|
||
this.jobs.set(job.id, cronJob);
|
||
} else if (job.run_at) {
|
||
// 一次性任务
|
||
const runAt = new Date(job.run_at);
|
||
const delay = runAt.getTime() - Date.now();
|
||
|
||
if (delay > 0) {
|
||
const timeout = setTimeout(() => {
|
||
this.executeJob(job);
|
||
// 执行后删除一次性任务
|
||
this.deleteJob(job.id);
|
||
}, delay);
|
||
this.jobs.set(job.id, timeout as any);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 停止任务
|
||
*/
|
||
private stopJob(id: string): void {
|
||
const job = this.jobs.get(id);
|
||
if (job) {
|
||
if (job instanceof CronJob) {
|
||
job.stop();
|
||
} else {
|
||
clearTimeout(job);
|
||
}
|
||
this.jobs.delete(id);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行任务
|
||
*/
|
||
private async executeJob(job: CronJobRecord): Promise<JobResult> {
|
||
const jobConfig = JSON.parse(job.job_config || "{}");
|
||
const startTime = Date.now();
|
||
|
||
try {
|
||
let result: any;
|
||
|
||
switch (job.job_type) {
|
||
case "reminder":
|
||
result = await this.executeReminder(job, jobConfig);
|
||
break;
|
||
case "agent":
|
||
result = await this.executeAgentTask(job, jobConfig);
|
||
break;
|
||
case "webhook":
|
||
result = await this.executeWebhook(job, jobConfig);
|
||
break;
|
||
case "script":
|
||
result = await this.executeScript(job, jobConfig);
|
||
break;
|
||
case "custom":
|
||
result = await this.executeCustom(job, jobConfig);
|
||
break;
|
||
default:
|
||
throw new Error(`Unknown job type: ${job.job_type}`);
|
||
}
|
||
|
||
const executionTime = Date.now() - startTime;
|
||
|
||
// 更新执行记录
|
||
this.db.prepare(`
|
||
UPDATE cron_jobs SET
|
||
last_run_at = ?,
|
||
next_run_at = ?,
|
||
updated_at = ?
|
||
WHERE id = ?
|
||
`).run(
|
||
startTime,
|
||
this.calculateNextRun({ cronExpression: job.cron_expression })?.getTime() || null,
|
||
Date.now(),
|
||
job.id
|
||
);
|
||
|
||
// 推送执行结果到服务器
|
||
this.pushJobResultToServer(job.id, {
|
||
success: true,
|
||
result,
|
||
executionTime,
|
||
executedAt: startTime,
|
||
});
|
||
|
||
return { success: true, result, executionTime };
|
||
} catch (error) {
|
||
const executionTime = Date.now() - startTime;
|
||
|
||
// 推送错误结果到服务器
|
||
this.pushJobResultToServer(job.id, {
|
||
success: false,
|
||
error: String(error),
|
||
executionTime,
|
||
executedAt: startTime,
|
||
});
|
||
|
||
return { success: false, error: String(error), executionTime };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行提醒任务
|
||
*/
|
||
private async executeReminder(job: CronJobRecord, config: ReminderConfig): Promise<any> {
|
||
const { title, message, sound, actions } = config;
|
||
|
||
// 显示系统通知
|
||
const { Notification } = require("electron");
|
||
new Notification({
|
||
title,
|
||
body: message,
|
||
sound: sound !== false,
|
||
actions: actions?.map((a: any) => ({ type: "button", text: a.label })),
|
||
}).show();
|
||
|
||
return { notified: true, title };
|
||
}
|
||
|
||
/**
|
||
* 执行 Agent 任务
|
||
*/
|
||
private async executeAgentTask(job: CronJobRecord, config: AgentTaskConfig): Promise<any> {
|
||
const { agentId, prompt, context } = config;
|
||
|
||
// 获取 Agent 引擎
|
||
const chatEngine = getChatEngine();
|
||
|
||
// 创建会话并执行
|
||
const session = await chatEngine.createSession({
|
||
agentId,
|
||
title: `[定时任务] ${job.name}`,
|
||
});
|
||
|
||
const response = await chatEngine.sendMessage(session.id, prompt, {
|
||
context,
|
||
});
|
||
|
||
return {
|
||
sessionId: session.id,
|
||
response: response.content,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 执行 Webhook
|
||
*/
|
||
private async executeWebhook(job: CronJobRecord, config: WebhookConfig): Promise<any> {
|
||
const { url, method = "POST", headers, body } = config;
|
||
|
||
const response = await fetch(url, {
|
||
method,
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
...headers,
|
||
},
|
||
body: body ? JSON.stringify(body) : undefined,
|
||
});
|
||
|
||
return {
|
||
status: response.status,
|
||
data: await response.text(),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 执行脚本
|
||
*/
|
||
private async executeScript(job: CronJobRecord, config: ScriptConfig): Promise<any> {
|
||
const { language, code, timeout = 30000 } = config;
|
||
|
||
// 根据语言选择执行器
|
||
const { spawn } = require("child_process");
|
||
|
||
let command: string;
|
||
let args: string[];
|
||
|
||
switch (language) {
|
||
case "bash":
|
||
command = "bash";
|
||
args = ["-c", code];
|
||
break;
|
||
case "node":
|
||
command = "node";
|
||
args = ["-e", code];
|
||
break;
|
||
case "python":
|
||
command = "python3";
|
||
args = ["-c", code];
|
||
break;
|
||
default:
|
||
throw new Error(`Unsupported language: ${language}`);
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const proc = spawn(command, args, { timeout });
|
||
let stdout = "";
|
||
let stderr = "";
|
||
|
||
proc.stdout.on("data", (data: Buffer) => { stdout += data.toString(); });
|
||
proc.stderr.on("data", (data: Buffer) => { stderr += data.toString(); });
|
||
|
||
proc.on("close", (code: number) => {
|
||
resolve({ exitCode: code, stdout, stderr });
|
||
});
|
||
|
||
proc.on("error", reject);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 执行自定义任务
|
||
*/
|
||
private async executeCustom(job: CronJobRecord, config: any): Promise<any> {
|
||
// 通过事件系统让外部处理器执行
|
||
const handlers = this.customHandlers.get(job.job_type);
|
||
if (handlers) {
|
||
for (const handler of handlers) {
|
||
await handler(job, config);
|
||
}
|
||
}
|
||
return { handled: true };
|
||
}
|
||
|
||
/**
|
||
* 计算下次执行时间
|
||
*/
|
||
private calculateNextRun(input: { cronExpression?: string; runAt?: string | Date }): Date | null {
|
||
if (input.cronExpression) {
|
||
const cronJob = new CronJob(input.cronExpression, () => {}, null, false);
|
||
return cronJob.nextDate().toJSDate();
|
||
}
|
||
if (input.runAt) {
|
||
return new Date(input.runAt);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 恢复错过的任务
|
||
*/
|
||
private recoverMissedJobs(): void {
|
||
const now = Date.now();
|
||
const missedJobs = this.db.prepare(`
|
||
SELECT * FROM cron_jobs
|
||
WHERE enabled = 1 AND next_run_at IS NOT NULL AND next_run_at < ?
|
||
`).all(now) as CronJobRecord[];
|
||
|
||
for (const job of missedJobs) {
|
||
console.log(`[CronScheduler] Recovering missed job: ${job.name}`);
|
||
this.executeJob(job).catch(console.error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从服务器同步任务
|
||
*/
|
||
async syncFromServer(): Promise<SyncResult> {
|
||
const qimingClient = getQimingClient();
|
||
if (!qimingClient?.isConnected()) {
|
||
return { success: false, pulled: 0, pushed: 0, conflicts: [], error: "Not connected" };
|
||
}
|
||
|
||
// 获取服务器上的任务列表
|
||
const serverJobs = await qimingClient.request(
|
||
"GET",
|
||
"/api/scheduler/jobs",
|
||
);
|
||
|
||
let pulled = 0;
|
||
let pushed = 0;
|
||
const conflicts: SyncConflict[] = [];
|
||
|
||
for (const serverJob of serverJobs) {
|
||
const localJob = this.db.prepare(
|
||
"SELECT * FROM cron_jobs WHERE server_job_id = ?"
|
||
).get(serverJob.id) as CronJobRecord | undefined;
|
||
|
||
if (!localJob) {
|
||
// 服务器有,本地没有 → 创建
|
||
await this.createJobFromServer(serverJob);
|
||
pulled++;
|
||
} else if (localJob.updated_at > serverJob.updatedAt) {
|
||
// 本地更新 → 推送到服务器
|
||
await this.syncJobToServer(localJob);
|
||
pushed++;
|
||
} else if (localJob.updated_at < serverJob.updatedAt) {
|
||
// 服务器更新 → 更新本地
|
||
await this.updateJobFromServer(localJob.id, serverJob);
|
||
pulled++;
|
||
}
|
||
}
|
||
|
||
return { success: true, pulled, pushed, conflicts };
|
||
}
|
||
|
||
/**
|
||
* 获取所有任务
|
||
*/
|
||
getJobs(filter?: JobFilter): CronJobRecord[] {
|
||
let sql = "SELECT * FROM cron_jobs";
|
||
const params: any[] = [];
|
||
|
||
if (filter?.enabled !== undefined) {
|
||
sql += " WHERE enabled = ?";
|
||
params.push(filter.enabled ? 1 : 0);
|
||
}
|
||
|
||
return this.db.prepare(sql).all(...params) as CronJobRecord[];
|
||
}
|
||
|
||
/**
|
||
* 获取单个任务
|
||
*/
|
||
getJob(id: string): CronJobRecord | null {
|
||
return this.db.prepare("SELECT * FROM cron_jobs WHERE id = ?").get(id) as CronJobRecord | null;
|
||
}
|
||
|
||
/**
|
||
* 手动触发任务
|
||
*/
|
||
async triggerJob(id: string): Promise<JobResult> {
|
||
const job = this.getJob(id);
|
||
if (!job) {
|
||
throw new Error(`Job ${id} not found`);
|
||
}
|
||
return this.executeJob(job);
|
||
}
|
||
|
||
private generateId(): string {
|
||
return `job_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||
}
|
||
}
|
||
|
||
// ==================== 类型定义 ====================
|
||
|
||
interface CronJobRecord {
|
||
id: string;
|
||
name: string;
|
||
description: string | null;
|
||
cron_expression: string | null;
|
||
run_at: string | null;
|
||
timezone: string;
|
||
job_type: "reminder" | "agent" | "webhook" | "script" | "custom";
|
||
job_config: string;
|
||
enabled: number;
|
||
last_run_at: number | null;
|
||
next_run_at: number | null;
|
||
created_at: number;
|
||
updated_at: number;
|
||
synced_to_server: number;
|
||
server_job_id: string | null;
|
||
}
|
||
|
||
interface CreateJobInput {
|
||
id?: string;
|
||
name: string;
|
||
description?: string;
|
||
cronExpression?: string; // cron 表达式
|
||
runAt?: string | Date; // 一次性任务执行时间
|
||
timezone?: string;
|
||
jobType: CronJobRecord["job_type"];
|
||
jobConfig: ReminderConfig | AgentTaskConfig | WebhookConfig | ScriptConfig | any;
|
||
}
|
||
|
||
interface UpdateJobInput {
|
||
name?: string;
|
||
description?: string;
|
||
cronExpression?: string;
|
||
runAt?: string | Date;
|
||
timezone?: string;
|
||
jobConfig?: any;
|
||
enabled?: boolean;
|
||
}
|
||
|
||
interface ReminderConfig {
|
||
title: string;
|
||
message: string;
|
||
sound?: boolean;
|
||
actions?: { label: string; action: string }[];
|
||
}
|
||
|
||
interface AgentTaskConfig {
|
||
agentId: number;
|
||
prompt: string;
|
||
context?: Record<string, any>;
|
||
}
|
||
|
||
interface WebhookConfig {
|
||
url: string;
|
||
method?: "GET" | "POST" | "PUT" | "DELETE";
|
||
headers?: Record<string, string>;
|
||
body?: any;
|
||
}
|
||
|
||
interface ScriptConfig {
|
||
language: "bash" | "node" | "python";
|
||
code: string;
|
||
timeout?: number;
|
||
}
|
||
|
||
interface JobResult {
|
||
success: boolean;
|
||
result?: any;
|
||
error?: string;
|
||
executionTime: number;
|
||
}
|
||
|
||
interface JobFilter {
|
||
enabled?: boolean;
|
||
type?: string;
|
||
}
|
||
```
|
||
|
||
### 3.4 IPC 接口
|
||
|
||
```typescript
|
||
// Cron Scheduler IPC
|
||
'cron:list' → CronJobRecord[] // 获取任务列表
|
||
'cron:get' → CronJobRecord // 获取单个任务
|
||
'cron:create' → CronJobRecord // 创建任务
|
||
'cron:update' → CronJobRecord // 更新任务
|
||
'cron:delete' → { success: boolean } // 删除任务
|
||
'cron:trigger' → JobResult // 手动触发
|
||
'cron:sync' → SyncResult // 从服务器同步
|
||
'cron:status' → SchedulerStatus // 调度器状态
|
||
```
|
||
|
||
---
|
||
|
||
## 四、Heartbeat vs Cron 对比
|
||
|
||
| 特性 | Heartbeat | Cron Jobs |
|
||
|------|-----------|-----------|
|
||
| **触发方式** | 系统自动,固定间隔 | 用户定义,cron 表达式 |
|
||
| **用途** | 健康检查、后台维护、状态同步 | 定时提醒、定时任务、自动化 |
|
||
| **任务类型** | 预定义的检查任务 | 用户创建的任意任务 |
|
||
| **持久化** | 无需(状态在内存) | 需要(SQLite 持久化) |
|
||
| **服务器同步** | 状态同步 | 任务定义 + 执行结果同步 |
|
||
| **错过处理** | 跳过,等下次 | 可配置恢复执行 |
|
||
| **UI 控制** | 可调整间隔 | 完整 CRUD |
|
||
|
||
---
|
||
|
||
## 五、与服务器同步
|
||
|
||
### 5.1 Heartbeat 同步
|
||
|
||
```typescript
|
||
// 心跳检查时,同步状态到服务器
|
||
interface HeartbeatSyncPayload {
|
||
timestamp: number;
|
||
clientId: string;
|
||
status: {
|
||
serverHealth: "connected" | "disconnected" | "error";
|
||
lastSyncAt: number;
|
||
pendingChanges: number;
|
||
activeSessions: number;
|
||
};
|
||
}
|
||
|
||
// 服务器可选择性推送通知
|
||
// /api/heartbeat POST
|
||
```
|
||
|
||
### 5.2 Cron Jobs 同步
|
||
|
||
```typescript
|
||
// ==================== 服务器已有 API ====================
|
||
// POST /api/agent/task/create // 创建定时会话
|
||
// POST /api/agent/task/update // 更新定时会话
|
||
// POST /api/agent/task/list // 查询定时会话列表
|
||
// POST /api/agent/task/cancel/:id // 取消定时会话
|
||
// GET /api/agent/task/cron/list // 可选定时范围
|
||
// GET /api/task/cron/list // 可选定时范围 - 任务
|
||
|
||
// ==================== 客户端同步策略 ====================
|
||
|
||
/**
|
||
* CronSyncAdapter - 与服务器定时任务同步
|
||
*
|
||
* 复用服务器已有的定时任务 API,客户端提供:
|
||
* 1. 任务定义的本地缓存和离线查看
|
||
* 2. 任务执行结果的本地记录
|
||
* 3. 更丰富的任务类型支持(提醒、脚本等)
|
||
*/
|
||
class CronSyncAdapter {
|
||
constructor(
|
||
private qimingClient: QimingApiClient,
|
||
private cronScheduler: CronScheduler,
|
||
) {}
|
||
|
||
/**
|
||
* 从服务器同步定时会话任务
|
||
*/
|
||
async syncFromServer(agentId?: number): Promise<SyncResult> {
|
||
// 1. 获取服务器定时任务列表
|
||
const serverTasks = await this.qimingClient.request(
|
||
"POST",
|
||
"/api/agent/task/list",
|
||
{
|
||
agentId,
|
||
taskStatus: "EXECUTING", // 获取执行中的任务
|
||
},
|
||
);
|
||
|
||
let pulled = 0;
|
||
let pushed = 0;
|
||
|
||
// 2. 同步到本地
|
||
for (const serverTask of serverTasks) {
|
||
const localTask = this.cronScheduler.getJob(
|
||
this.getLocalJobId(serverTask.id),
|
||
);
|
||
|
||
if (!localTask) {
|
||
// 服务器有,本地没有 → 创建本地任务
|
||
await this.cronScheduler.createJob({
|
||
id: this.getLocalJobId(serverTask.id),
|
||
name: serverTask.topic,
|
||
description: serverTask.summary,
|
||
cronExpression: serverTask.taskCron,
|
||
jobType: "agent",
|
||
jobConfig: {
|
||
agentId: serverTask.agentId,
|
||
conversationId: serverTask.id,
|
||
topic: serverTask.topic,
|
||
summary: serverTask.summary,
|
||
},
|
||
serverJobId: String(serverTask.taskId),
|
||
});
|
||
pulled++;
|
||
}
|
||
}
|
||
|
||
// 3. 推送本地新增的任务到服务器
|
||
const localOnly = this.cronScheduler.getJobs({ synced: false });
|
||
for (const local of localOnly) {
|
||
if (local.job_type === "agent") {
|
||
// 只推送 Agent 类型任务
|
||
await this.qimingClient.request("POST", "/api/agent/task/create", {
|
||
agentId: local.job_config.agentId,
|
||
topic: local.name,
|
||
summary: local.description,
|
||
taskCron: local.cron_expression,
|
||
});
|
||
pushed++;
|
||
}
|
||
}
|
||
|
||
return { success: true, pulled, pushed, conflicts: [] };
|
||
}
|
||
|
||
/**
|
||
* 获取服务器支持的 cron 预设
|
||
*/
|
||
async getCronPresets(): Promise<TaskCronInfo[]> {
|
||
return await this.qimingClient.request("GET", "/api/agent/task/cron/list");
|
||
}
|
||
|
||
/**
|
||
* 创建定时会话任务(直接调用服务器 API)
|
||
*/
|
||
async createServerTask(input: CreateTimedConversationTaskDto): Promise<TimedConversationTaskInfo> {
|
||
const result = await this.qimingClient.request(
|
||
"POST",
|
||
"/api/agent/task/create",
|
||
input,
|
||
);
|
||
|
||
// 同时创建本地任务用于缓存
|
||
await this.cronScheduler.createJob({
|
||
id: this.getLocalJobId(result.id),
|
||
name: input.topic,
|
||
description: input.summary,
|
||
cronExpression: input.taskCron,
|
||
jobType: "agent",
|
||
jobConfig: {
|
||
agentId: input.agentId,
|
||
conversationId: result.id,
|
||
topic: input.topic,
|
||
summary: input.summary,
|
||
},
|
||
serverJobId: result.taskId,
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 取消定时会话任务
|
||
*/
|
||
async cancelServerTask(conversationId: number): Promise<void> {
|
||
await this.qimingClient.request(
|
||
"POST",
|
||
`/api/agent/task/cancel/${conversationId}`,
|
||
);
|
||
|
||
// 同时删除本地任务
|
||
this.cronScheduler.deleteJob(this.getLocalJobId(conversationId));
|
||
}
|
||
|
||
private getLocalJobId(serverId: number): string {
|
||
return `server_task_${serverId}`;
|
||
}
|
||
}
|
||
|
||
// ==================== 类型定义(来自服务器)====================
|
||
|
||
interface TaskCronInfo {
|
||
typeName: string; // 类型:每天/每周/每月
|
||
items: TaskCronItemDto[];
|
||
}
|
||
|
||
interface TaskCronItemDto {
|
||
cron: string; // cron 表达式
|
||
desc: string; // 描述
|
||
}
|
||
|
||
interface CreateTimedConversationTaskDto {
|
||
agentId: number;
|
||
devMode?: boolean;
|
||
id?: number;
|
||
summary: string; // 任务内容
|
||
taskCron: string; // cron 表达式
|
||
topic: string; // 任务主题
|
||
}
|
||
|
||
interface TimedConversationTaskInfo {
|
||
id?: number;
|
||
agentId?: number;
|
||
agent?: AgentDetailDto;
|
||
topic?: string;
|
||
summary?: string;
|
||
taskCron?: string;
|
||
taskCronDesc?: string;
|
||
taskId?: string;
|
||
taskStatus?: TaskStatus;
|
||
created?: string;
|
||
modified?: string;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 六、UI 设计
|
||
|
||
### Heartbeat 状态页
|
||
|
||
```
|
||
┌─ 系统状态 ────────────────────────────────────────────────────────────┐
|
||
│ │
|
||
│ 心跳间隔: 1 分钟 [调整] [暂停] │
|
||
│ 上次检查: 30 秒前 │
|
||
│ │
|
||
│ ── 检查任务状态 ──────────────────────────────────────────────────── │
|
||
│ │
|
||
│ ✓ 服务器健康检查 已连接 · 延迟 45ms · 版本 2.1.0 │
|
||
│ ✓ 配置同步检查 已同步 · 无待同步变更 │
|
||
│ ✓ 会话同步检查 已同步 · 0 条待同步消息 │
|
||
│ ○ 后台维护 等待中 · 下次执行: 5 分钟后 │
|
||
│ ✓ 通知检查 无新通知 │
|
||
│ │
|
||
└───────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Cron Jobs 管理页
|
||
|
||
```
|
||
┌─ 定时任务 ────────────────────────────────────────────────────────────┐
|
||
│ │
|
||
│ [+ 创建任务] [从服务器同步] 共 5 个任务 │
|
||
│ │
|
||
│ ── 任务列表 ──────────────────────────────────────────────────────── │
|
||
│ │
|
||
│ ☑ 早会提醒 每天 09:00 提醒 [编辑] [删除] [立即执行] │
|
||
│ "记得参加每日早会" │
|
||
│ │
|
||
│ ☑ 周报生成 每周一 18:00 Agent [编辑] [删除] [立即执行] │
|
||
│ "请生成本周工作总结" │
|
||
│ │
|
||
│ ☑ 服务健康检查 每小时 Webhook [编辑] [删除] [立即执行] │
|
||
│ POST https://api.example.com/health │
|
||
│ │
|
||
│ ☐ 日志清理 每天 00:00 脚本 [编辑] [删除] [立即执行] │
|
||
│ ✗ 已禁用 │
|
||
│ │
|
||
│ ── 创建/编辑任务 ────────────────────────────────────────────────────│
|
||
│ │
|
||
│ 名称: [________________] │
|
||
│ 类型: ○ 提醒 ○ Agent 任务 ○ Webhook ○ 脚本 │
|
||
│ │
|
||
│ ── 提醒配置 ────────────────────── │
|
||
│ 标题: [________________] │
|
||
│ 内容: [________________] │
|
||
│ ☑ 播放提示音 │
|
||
│ │
|
||
│ ── 调度时间 ────────────────────── │
|
||
│ ○ 周期性: [选择预设: 每天/每周/每月/自定义 cron] │
|
||
│ [0 9 * * *] 预览: 每天 09:00 │
|
||
│ │
|
||
│ ○ 一次性: [选择日期时间: 2026-03-10 14:30] │
|
||
│ │
|
||
│ 时区: Asia/Shanghai │
|
||
│ │
|
||
│ [取消] [保存并同步到服务器] │
|
||
│ │
|
||
└───────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 相关文档
|
||
|
||
- [总体架构](./01-ARCHITECTURE.md)
|
||
- [会话管理](./04-SESSION-CHAT.md)
|
||
- [Channel 多渠道接入](./05-CHANNELS.md)
|
||
- [Agent 自我进化](./06-SELF-EVOLUTION.md)
|