Skip to content

Commit f6e6014

Browse files
committed
数据库维护自动停启PM2网关
1 parent 74896af commit f6e6014

9 files changed

Lines changed: 323 additions & 28 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ bun run dev -- db check # 检查数据库完整性与业务统计
5252
- 数据库初始化时启用 WAL、创建 `gateway_lock` 并自动执行 `drizzle/` migrations。
5353
- Gateway 用 SQLite `BEGIN IMMEDIATE` + `gateway_lock` 保证单实例;Dashboard 默认只监听 `127.0.0.1:4680`
5454
- Gateway 仅在 Worker、Scheduler、Watchdog 和 Dashboard 启动完成后写入 `gateway_lock.ready_at`;PM2 `online` 不能单独作为就绪依据,进程 PID 必须匹配新鲜 ready 锁。
55-
- 数据库检查、备份、清空和恢复统一经过 `DatabaseMaintenanceService`;CLI 清空/恢复必须显式确认并拒绝活跃 Gateway 或运行中任务,清空/恢复前必须自动创建校验通过的安全备份。
55+
- 数据库检查、备份、清空和恢复统一经过 `DatabaseMaintenanceService`;CLI 清空/恢复必须显式确认并拒绝运行中任务,且只可自动停启 PID 与当前数据库新鲜 ready 锁一致的 PM2 Gateway;前台或无法确认归属的进程必须拒绝误杀。清空/恢复前必须自动创建校验通过的安全备份,默认在操作失败时也恢复原 Gateway 状态,`--keep-stopped` 除外
5656
- Dashboard 清空只能豁免当前 Gateway PID,仍必须服务端确认、拒绝运行中任务并在同一事务内先备份后删除;不得恢复为直接 `DELETE` 三张表的路由实现。
5757
- Dashboard 的浏览器写请求必须通过同源检查,数据库字符串进入 HTML 前必须调用 `esc`;API 的 ID、状态和配置不得直接断言类型。
5858

README.md

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ Documentation: [current architecture](docs/architecture.md) · [operations and t
1313

1414
### Quick Install
1515

16-
Add the plugin to `~/.config/opencode/opencode.json`:
16+
Install the CLI globally:
17+
18+
```bash
19+
npm install -g opencode-supertask
20+
```
21+
22+
Then add the plugin to `~/.config/opencode/opencode.json`:
1723

1824
```json
1925
{
@@ -36,13 +42,14 @@ The plugin never installs global dependencies by itself. Without a running Gatew
3642
2. Remove `"opencode-supertask"` from `~/.config/opencode/opencode.json`
3743
3. Restart OpenCode
3844

39-
To clear all task data safely, stop the Gateway and use the backup-first database command:
45+
To clear all task data safely, use the backup-first database command:
4046

4147
```bash
42-
pm2 stop supertask-gateway
4348
supertask db clear --confirm CLEAR
4449
```
4550

51+
If the matching Gateway is managed by PM2, the CLI stops it first and restarts it after maintenance.
52+
4653
### CLI Install / Uninstall
4754

4855
```bash
@@ -114,11 +121,11 @@ supertask template enable --id 1
114121
# Database maintenance
115122
supertask db check
116123
supertask db backup [--output /path/to/tasks-backup.db]
117-
supertask db clear --confirm CLEAR
118-
supertask db restore --from /path/to/tasks-backup.db --confirm RESTORE
124+
supertask db clear --confirm CLEAR [--keep-stopped]
125+
supertask db restore --from /path/to/tasks-backup.db --confirm RESTORE [--keep-stopped]
119126
```
120127

121-
`db backup` creates and validates a standalone SQLite snapshot. `db clear` and `db restore` refuse to run while another Gateway or task is active; both create a safety backup before changing data. Stop the PM2 Gateway before using the destructive CLI commands.
128+
`db backup` creates and validates a standalone SQLite snapshot. For `db clear` and `db restore`, the CLI automatically stops a PM2 Gateway whose PID matches the current database's fresh ready lock, then restores its previous running state. Both commands create a safety backup before changing data and still refuse active tasks or an unverified/foreground Gateway. Use `--keep-stopped` to leave a previously running PM2 Gateway stopped.
122129

123130
### Duration Format
124131

@@ -214,7 +221,13 @@ SuperTask 是一个基于 SQLite 的 AI Agent 任务调度系统,专为 [OpenC
214221

215222
### 安装
216223

217-
`~/.config/opencode/opencode.json` 中添加:
224+
先安装全局 CLI:
225+
226+
```bash
227+
npm install -g opencode-supertask
228+
```
229+
230+
然后在 `~/.config/opencode/opencode.json` 中添加:
218231

219232
```json
220233
{
@@ -240,10 +253,11 @@ supertask gateway # 前台运行:不需要 pm2
240253
安全清理所有任务数据:
241254

242255
```bash
243-
pm2 stop supertask-gateway
244256
supertask db clear --confirm CLEAR
245257
```
246258

259+
若当前数据库对应的 Gateway 由 PM2 管理,CLI 会自动停止并在维护结束后恢复运行。
260+
247261
### 定时任务
248262

249263
支持友好的时间格式:
@@ -274,11 +288,11 @@ supertask template add --type cron --cron "0 9 * * *" ...
274288
```bash
275289
supertask db check
276290
supertask db backup [--output /path/to/tasks-backup.db]
277-
supertask db clear --confirm CLEAR
278-
supertask db restore --from /path/to/tasks-backup.db --confirm RESTORE
291+
supertask db clear --confirm CLEAR [--keep-stopped]
292+
supertask db restore --from /path/to/tasks-backup.db --confirm RESTORE [--keep-stopped]
279293
```
280294

281-
`db backup` 会生成并校验可独立恢复的 SQLite 快照。CLI 清空和恢复会拒绝活跃 Gateway 或运行中任务,并在修改数据前自动保留安全备份;执行这两个危险命令前应先停止 PM2 Gateway。
295+
`db backup` 会生成并校验可独立恢复的 SQLite 快照。CLI 清空和恢复会先确认 PM2 PID 与当前数据库的新鲜 ready 锁一致,再自动停止并按原状态重启 Gateway;操作失败时也会尝试恢复 Gateway。它们仍会拒绝运行中任务、前台 Gateway 或无法确认归属的进程,并在修改数据前自动保留安全备份。传入 `--keep-stopped` 可让原本运行的 PM2 Gateway 保持停止
282296

283297
### 数据位置
284298

docs/architecture.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> 状态:当前有效
44
> 最后核对:2026-07-15
5-
> 适用版本:源码 `main` 分支,当前开发基线 0.1.24
5+
> 适用版本:源码 `main` 分支,当前开发基线 0.1.25
66
77
## 结论与适用边界
88

@@ -124,14 +124,14 @@ pending / running / failed ──cancel──> cancelled
124124
| 持久化 | SQLite WAL | 零服务依赖,适合单机队列 | 多机消费、高写入并发或 HA 时 |
125125
| 配置错误 | 校验失败并拒绝启动 | 避免错误配置被默认值掩盖 | 不改变;可增加更友好的诊断 |
126126
| 调度补偿 | 不回放全部错过周期 | 避免恢复时瞬间制造积压 | 业务明确要求补账并定义上限时 |
127-
| 数据库维护 | Service 统一检查、备份、清空和恢复 | 避免 CLI、Dashboard 各自实现不完整的危险 SQL | 存储迁移到外部数据库时重做维护协议 |
127+
| 数据库维护 | Service 统一数据安全;CLI 编排匹配当前 ready 锁的 PM2 Gateway 自动停启 | 避免各入口实现不完整的危险 SQL,并防止维护命令误停另一套实例 | 存储迁移到外部数据库时重做维护协议 |
128128
| Dashboard | 嵌入 Gateway,仅监听 loopback | 少一个常驻进程,默认不暴露网络面 | 需要公网/团队访问时拆分并加鉴权 |
129129

130130
## 安全边界
131131

132132
Dashboard 只绑定 `127.0.0.1`,浏览器写请求检查 `Sec-Fetch-Site` 和同源 `Origin`,数据库字符串输出到 HTML 前转义。这是本机可信用户边界,不是完整鉴权系统;不要通过反向代理或端口转发直接暴露到不可信网络。
133133

134-
数据库危险操作集中在 `DatabaseMaintenanceService`:备份通过 SQLite 序列化得到一致快照,并转换成无需 WAL sidecar 的独立文件;清空在 `BEGIN IMMEDIATE` 内完成备份和三张业务表删除;恢复先校验来源、备份当前库,再暂存、替换、迁移和复检。CLI 清空/恢复拒绝活跃 Gateway 和运行中任务,Dashboard 清空只允许当前 Gateway 且仍拒绝运行中任务。恢复会清理运行时锁,并把快照中的遗留运行状态收敛为可重新调度的状态。
134+
数据库危险操作集中在 `DatabaseMaintenanceService`:备份通过 SQLite 序列化得到一致快照,并转换成无需 WAL sidecar 的独立文件;清空在 `BEGIN IMMEDIATE` 内完成备份和三张业务表删除;恢复先校验来源、备份当前库,再暂存、替换、迁移和复检。CLI 层只对“PM2 PID 与当前数据库新鲜 ready 锁一致”的 Gateway 自动停启,并在操作失败时恢复原运行状态;其他活跃 Gateway 仍由 Service 拒绝,避免误杀另一套实例。Dashboard 清空只允许当前 Gateway 且仍拒绝运行中任务。恢复会清理运行时锁,并把快照中的遗留运行状态收敛为可重新调度的状态。
135135

136136
## 已知限制与后续触发点
137137

docs/operations.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -198,24 +198,22 @@ supertask db backup --output ~/supertask-backup/tasks.db
198198
清空全部任务、执行记录和调度模板:
199199

200200
```bash
201-
pm2 stop supertask-gateway
202201
supertask db clear --confirm CLEAR
203-
pm2 start supertask-gateway
204202
```
205203

206-
`db clear` 会拒绝活跃 Gateway 和任何 `running` 任务/执行记录,在一个 `BEGIN IMMEDIATE` 事务内先生成 `pre-clear` 备份,再删除三张业务表;任一步失败都会回滚事务。它保留数据库结构、迁移记录、配置和自增序列。Dashboard 的“清空数据库”复用同一服务,要求服务端确认并拒绝运行中任务;因为 Dashboard 本身位于当前 Gateway 内,它只豁免当前 Gateway PID,不豁免其他进程。
204+
`db clear` 会先核对 PM2 进程 PID 与当前数据库的新鲜 ready 锁;匹配时自动优雅停止 Gateway,维护结束后按原状态重启并等待新的 ready 锁,数据库操作失败时也会尝试恢复运行。传入 `--keep-stopped` 会让原本运行的 PM2 Gateway 保持停止。前台 Gateway、陈旧锁或无法确认归属的进程不会被自动终止,命令会拒绝操作并要求人工停止。
205+
206+
清空仍会拒绝任何 `running` 任务/执行记录,在一个 `BEGIN IMMEDIATE` 事务内先生成 `pre-clear` 备份,再删除三张业务表;任一步失败都会回滚事务。它保留数据库结构、迁移记录、配置和自增序列。若数据库维护已完成但 PM2 重启失败,CLI 会明确报告“数据库维护已完成”,此时按错误提示检查 `pm2 logs supertask-gateway`,不要重复清空。Dashboard 的“清空数据库”复用同一服务,要求服务端确认并拒绝运行中任务;因为 Dashboard 本身位于当前 Gateway 内,它只豁免当前 Gateway PID,不豁免其他进程。
207207

208208
从备份恢复:
209209

210210
```bash
211-
pm2 stop supertask-gateway
212211
supertask db restore --from ~/.local/share/opencode/tasks.pre-clear-<time>-<id>.db --confirm RESTORE
213-
pm2 start supertask-gateway
214212
supertask db check
215213
curl -fsS http://127.0.0.1:4680/health
216214
```
217215

218-
恢复会先校验来源文件并自动创建当前库的 `pre-restore` 安全备份,然后用同目录暂存文件替换数据库、自动执行缺失 migration,再复检完整性。备份中遗留的 `running` 任务会恢复为 `pending`,对应运行记录关闭为 `failed`,旧 `gateway_lock` 不会继续生效。恢复失败时会尝试原子回滚,并在错误信息中给出安全备份路径。
216+
恢复使用与清空相同的 PM2 自动停启和 `--keep-stopped` 语义。它会先校验来源文件并自动创建当前库的 `pre-restore` 安全备份,然后用同目录暂存文件替换数据库、自动执行缺失 migration,再复检完整性。备份中遗留的 `running` 任务会恢复为 `pending`,对应运行记录关闭为 `failed`,旧 `gateway_lock` 不会继续生效。恢复失败时会尝试原子回滚,并在错误信息中给出安全备份路径。
219217

220218
不要在 Gateway 运行时删除或只复制 `tasks.db`;WAL 模式下最新事务可能还在 `tasks.db-wal`。配置文件仍需单独备份:
221219

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-supertask",
3-
"version": "0.1.24",
3+
"version": "0.1.25",
44
"description": "AI Agent 任务调度系统 — OpenCode 插件 + CLI + Gateway 常驻进程",
55
"type": "module",
66
"main": "dist/plugin/supertask.js",

src/cli/index.ts

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { DatabaseMaintenanceService } from '@core/services/database-maintenance.
55
import { closeDb } from '@core/db';
66
import { parseDuration } from '@core/duration';
77
import type { TaskStatus, ScheduleType } from '@core/db/schema';
8-
import { getPackageVersion } from '../daemon/pm2';
8+
import {
9+
getPackageVersion,
10+
restartGatewayAfterMaintenance,
11+
stopGatewayForMaintenance,
12+
} from '../daemon/pm2';
913

1014
async function withDb<T>(fn: () => Promise<T>): Promise<T> {
1115
try {
@@ -19,6 +23,56 @@ async function withDb<T>(fn: () => Promise<T>): Promise<T> {
1923
}
2024
}
2125

26+
interface GatewayMaintenanceReport {
27+
wasRunning: boolean;
28+
restarted: boolean;
29+
keptStopped: boolean;
30+
}
31+
32+
function messageOf(error: unknown): string {
33+
return error instanceof Error ? error.message : String(error);
34+
}
35+
36+
function runDestructiveDatabaseMaintenance<T extends object>(
37+
keepStopped: boolean,
38+
operation: () => T,
39+
): T & { gateway: GatewayMaintenanceReport } {
40+
const gatewayState = stopGatewayForMaintenance();
41+
let result: T;
42+
try {
43+
result = operation();
44+
} catch (error) {
45+
if (gatewayState.wasRunning && !keepStopped) {
46+
try {
47+
restartGatewayAfterMaintenance(gatewayState);
48+
} catch (restartError) {
49+
throw new Error(
50+
`${messageOf(error)};Gateway 自动恢复也失败:${messageOf(restartError)}`,
51+
);
52+
}
53+
}
54+
throw error;
55+
}
56+
57+
let restarted = false;
58+
if (gatewayState.wasRunning && !keepStopped) {
59+
try {
60+
restarted = restartGatewayAfterMaintenance(gatewayState);
61+
} catch (error) {
62+
throw new Error(`数据库维护已完成,但 Gateway 自动重启失败:${messageOf(error)}`);
63+
}
64+
}
65+
66+
return {
67+
...result,
68+
gateway: {
69+
wasRunning: gatewayState.wasRunning,
70+
restarted,
71+
keptStopped: gatewayState.wasRunning && keepStopped,
72+
},
73+
};
74+
}
75+
2276
const program = new Command();
2377

2478
program
@@ -362,23 +416,33 @@ databaseCommand
362416
.command('clear')
363417
.description('备份后事务性清空任务、执行记录和调度模板')
364418
.option('--confirm <word>', '危险操作确认,必须填写 CLEAR')
365-
.action(async (options: { confirm?: string }) => withDb(async () => {
419+
.option('--keep-stopped', '维护结束后不重启原本由 PM2 管理的 Gateway')
420+
.action(async (options: { confirm?: string; keepStopped?: boolean }) => withDb(async () => {
366421
if (options.confirm !== 'CLEAR') {
367422
throw new Error('清空数据库必须显式传入 --confirm CLEAR');
368423
}
369-
console.log(JSON.stringify(DatabaseMaintenanceService.clear(), null, 2));
424+
const result = runDestructiveDatabaseMaintenance(
425+
options.keepStopped ?? false,
426+
() => DatabaseMaintenanceService.clear(),
427+
);
428+
console.log(JSON.stringify(result, null, 2));
370429
}));
371430

372431
databaseCommand
373432
.command('restore')
374433
.description('自动备份当前库后,从指定备份恢复数据库')
375434
.requiredOption('--from <path>', '要恢复的 SQLite 备份文件')
376435
.option('--confirm <word>', '危险操作确认,必须填写 RESTORE')
377-
.action(async (options: { from: string; confirm?: string }) => withDb(async () => {
436+
.option('--keep-stopped', '维护结束后不重启原本由 PM2 管理的 Gateway')
437+
.action(async (options: { from: string; confirm?: string; keepStopped?: boolean }) => withDb(async () => {
378438
if (options.confirm !== 'RESTORE') {
379439
throw new Error('恢复数据库必须显式传入 --confirm RESTORE');
380440
}
381-
console.log(JSON.stringify(DatabaseMaintenanceService.restore(options.from), null, 2));
441+
const result = runDestructiveDatabaseMaintenance(
442+
options.keepStopped ?? false,
443+
() => DatabaseMaintenanceService.restore(options.from),
444+
);
445+
console.log(JSON.stringify(result, null, 2));
382446
}));
383447

384448
program.addCommand(databaseCommand);

src/core/services/database-maintenance.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ export class DatabaseMaintenanceService {
404404
if (!lock || !isProcessAlive(lock.pid)) return;
405405
if (allowCurrentGateway && lock.pid === process.pid) return;
406406
throw new DatabaseMaintenanceConflictError(
407-
`Gateway PID ${lock.pid} 仍在运行,请先执行 pm2 stop supertask-gateway`,
407+
`Gateway PID ${lock.pid} 仍在运行,但未确认由当前数据库对应的 PM2 进程管理;请先停止该 Gateway(PM2 可执行 pm2 stop supertask-gateway`,
408408
);
409409
}
410410

src/daemon/pm2.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ export type EnsureGatewayResult =
2222
| { ok: true; action: "already-running" | "started" | "restarted" }
2323
| { ok: false; reason: "pm2-not-installed" };
2424

25+
export interface GatewayMaintenanceState {
26+
wasRunning: boolean;
27+
}
28+
2529
export function getPackageVersion(): string {
2630
const envVersion = process.env.npm_package_version;
2731
if (envVersion) return envVersion;
@@ -270,6 +274,35 @@ function waitForGatewayReady(pid: number): boolean {
270274
return isGatewayReady(pid);
271275
}
272276

277+
export function stopGatewayForMaintenance(): GatewayMaintenanceState {
278+
if (!isPm2Installed()) return { wasRunning: false };
279+
280+
const proc = pm2JsonList().find((item) => item.name === PROCESS_NAME);
281+
const managedCurrentDatabase = proc?.pm2_env?.status === "online"
282+
&& typeof proc.pid === "number"
283+
&& isGatewayReady(proc.pid);
284+
if (!managedCurrentDatabase) return { wasRunning: false };
285+
286+
requirePm2(["stop", PROCESS_NAME], "pm2 stop Gateway for database maintenance");
287+
return { wasRunning: true };
288+
}
289+
290+
export function restartGatewayAfterMaintenance(state: GatewayMaintenanceState): boolean {
291+
if (!state.wasRunning) return false;
292+
293+
requirePm2(["start", PROCESS_NAME], "pm2 restart Gateway after database maintenance");
294+
const started = pm2JsonList().find((item) => item.name === PROCESS_NAME);
295+
if (started?.pm2_env?.status !== "online") {
296+
throw new Error(
297+
`[supertask] Gateway 数据库维护后未恢复 online(状态:${started?.pm2_env?.status ?? "missing"})`,
298+
);
299+
}
300+
if (typeof started.pid !== "number" || !waitForGatewayReady(started.pid)) {
301+
throw new Error("[supertask] Gateway 数据库维护后已 online,但未在限定时间内就绪;请查看 pm2 logs supertask-gateway");
302+
}
303+
return true;
304+
}
305+
273306
function findBunPath(): string {
274307
const override = process.env.SUPERTASK_BUN_BIN;
275308
if (override) return override;

0 commit comments

Comments
 (0)