@wanghaopeng1148/deskpet 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -16
- package/bin/deskpet.mjs +70 -2
- package/dist/node/server/db/database.js +113 -0
- package/dist/node/server/db/migrate-legacy.js +88 -0
- package/dist/node/server/db/task-repository.js +374 -0
- package/dist/node/server/http/http-server.js +493 -0
- package/dist/node/server/http/ws-hub.js +59 -0
- package/dist/node/server/main.js +291 -0
- package/dist/node/server/plugins/actions/builtin.js +67 -0
- package/dist/node/server/plugins/actions/clipboard-watch.js +27 -0
- package/dist/node/server/plugins/actions/http-request.js +41 -0
- package/dist/node/server/plugins/actions/jenkins-build.js +183 -0
- package/dist/node/server/plugins/actions/open-app.js +41 -0
- package/dist/node/server/plugins/actions/python-script.js +180 -0
- package/dist/node/server/plugins/actions/screenshot.js +38 -0
- package/dist/node/server/plugins/actions/send-keystroke.js +100 -0
- package/dist/node/server/plugins/actions/show-reminder.js +7 -0
- package/dist/node/server/plugins/actions/ssh-command.js +123 -0
- package/dist/node/server/plugins/actions/task-chain.js +24 -0
- package/dist/node/server/plugins/actions/volume-control.js +31 -0
- package/dist/node/server/plugins/index.js +35 -0
- package/dist/node/server/plugins/registry.js +23 -0
- package/dist/node/server/services/clipboard-watcher.js +112 -0
- package/dist/node/server/services/config-store.js +141 -0
- package/dist/node/server/services/idle-monitor.js +131 -0
- package/dist/node/server/services/notifier.js +36 -0
- package/dist/node/server/services/quick-actions-store.js +52 -0
- package/dist/node/server/services/remote-connector.js +67 -0
- package/dist/node/server/services/scanner-reader.js +217 -0
- package/dist/node/server/services/script-runner.js +228 -0
- package/dist/node/server/services/snapshot-service.js +135 -0
- package/dist/node/server/services/task-scheduler.js +813 -0
- package/dist/node/server/services/wechat-bot.js +635 -0
- package/dist/node/server/services/wechat-command-types.js +1 -0
- package/dist/node/server/services/wechat-commands.js +330 -0
- package/dist/node/server/suppress-warnings.js +12 -0
- package/dist/node/server/utils/asset-url.js +26 -0
- package/dist/node/server/utils/auto-start.js +186 -0
- package/dist/node/server/utils/clipboard.js +50 -0
- package/dist/node/server/utils/dashboard-url.js +8 -0
- package/dist/node/server/utils/instance-guard.js +165 -0
- package/dist/node/server/utils/native-notify.js +53 -0
- package/dist/node/server/utils/open.js +37 -0
- package/dist/node/server/utils/paths.js +95 -0
- package/dist/node/server/utils/python-interpreter.js +129 -0
- package/dist/node/shared/animation-engine.js +349 -0
- package/dist/node/shared/chain-condition.js +39 -0
- package/dist/node/shared/cron-weekly.js +124 -0
- package/dist/node/shared/py-task-params.js +335 -0
- package/dist/node/shared/types.js +69 -0
- package/package.json +6 -2
- package/server/http/http-server.ts +4 -1
- package/server/utils/auto-start.ts +158 -49
- package/server/utils/paths.ts +28 -1
package/README.md
CHANGED
|
@@ -101,7 +101,10 @@ npm run dev:watch # 同上,但后端带 --watch 热重载(改 server/
|
|
|
101
101
|
│ └── utils/ # 路径 / 资源 URL / 剪贴板 / 打开外部 / 原生通知
|
|
102
102
|
├── src/dashboard/ # 管理台前端 (唯一前端入口)
|
|
103
103
|
├── shared/ # 前后端共享纯逻辑 (类型)
|
|
104
|
-
|
|
104
|
+
├── tests/unit/ # 单元测试 (node:test)
|
|
105
|
+
└── dist/ # 构建产物(不入版本库)
|
|
106
|
+
├── web/ # 前端产物,由 vite 生成
|
|
107
|
+
└── node/ # 后端编译产物,由 tsc 生成(发布包必须带上)
|
|
105
108
|
```
|
|
106
109
|
|
|
107
110
|
## 开发辅助命令
|
|
@@ -110,16 +113,32 @@ npm run dev:watch # 同上,但后端带 --watch 热重载(改 server/
|
|
|
110
113
|
|
|
111
114
|
| 命令 | 作用 |
|
|
112
115
|
| --- | --- |
|
|
113
|
-
| `npm run build` |
|
|
116
|
+
| `npm run build` | 构建全部产物(前端 `dist/web` + 后端编译 `dist/node`),发版前必须跑 |
|
|
117
|
+
| `npm run build:web` | 只构建前端 |
|
|
118
|
+
| `npm run build:server` | 只编译后端(`tsconfig.build.json` → `dist/node`) |
|
|
114
119
|
| `npm run typecheck` | TS 类型检查(`tsconfig.node.json` + `tsconfig.web.json` 各一遍) |
|
|
115
120
|
| `npm test` | 单元测试(内存 SQLite,不触碰真实数据) |
|
|
121
|
+
| `npm run verify:package` | 把打好的包装进临时 `node_modules` 真跑一遍,验证发布形态可用 |
|
|
116
122
|
| `npm run dev:server` | 只起后端(带 `--watch`) |
|
|
117
123
|
| `npm run dev:web` | 只起 Vite(⚠️ 单独跑会因后端没起而报 `Failed to fetch`) |
|
|
118
|
-
| `npm run serve` |
|
|
124
|
+
| `npm run serve` | 直接跑源码后端(`.ts`),不做任何依赖/产物检查 |
|
|
119
125
|
|
|
120
126
|
目录约定:后端在 `server/`,前端在 `src/`,两侧共享的纯逻辑放 `shared/`
|
|
121
127
|
(那里**不要** import 任何 Node 或浏览器专有 API)。
|
|
122
128
|
|
|
129
|
+
### 为什么后端必须编译成 JS(重要)
|
|
130
|
+
|
|
131
|
+
发布包里的后端**不能**是 `.ts` 源码:Node 明确禁止对 `node_modules` 下的文件做类型擦除
|
|
132
|
+
(`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`)。而 `npx` 安装/缓存的位置正在
|
|
133
|
+
`node_modules` 里,所以「本地跑源码一切正常、用户 `npx` 一启动就崩」——**这个坑只在发布形态下出现**。
|
|
134
|
+
|
|
135
|
+
因此流程是:`tsc -p tsconfig.build.json` 把 `server/` 与 `shared/` 编译到 `dist/node`
|
|
136
|
+
(保持相对层级,并用 `rewriteRelativeImportExtensions` 把导入后缀从 `.ts` 改写成 `.js`,
|
|
137
|
+
因此不需要任何打包器),`bin/deskpet.mjs` 优先启动编译产物,只有源码形态才回退到 `.ts`。
|
|
138
|
+
|
|
139
|
+
验证方式就是 `npm run verify:package`——它把 tarball 装进临时 `node_modules` 再启动,
|
|
140
|
+
**只有这样才能复现上面那个报错**。
|
|
141
|
+
|
|
123
142
|
## 服务重启与任务中断(重要)
|
|
124
143
|
|
|
125
144
|
任务执行期间如果后端进程退出,会出现两类结果,管理台会区分显示:
|
|
@@ -296,8 +315,8 @@ TASK_PARAMS = [
|
|
|
296
315
|
### 发布步骤(一键)
|
|
297
316
|
|
|
298
317
|
```bash
|
|
299
|
-
npm run release # 一键:校验 → 类型检查 → 测试 → 构建 → 打包核对 → 发布
|
|
300
|
-
npm run release -- --dry-run #
|
|
318
|
+
npm run release # 一键:校验 → 类型检查 → 测试 → 构建 → 打包核对 → 真实安装验证 → 发布
|
|
319
|
+
npm run release -- --dry-run # 一路跑到真实安装验证,但不发布(首次发版建议先跑一遍)
|
|
301
320
|
npm run release -- --bump patch # 同上,并先把版本号升一档(patch/minor/major)
|
|
302
321
|
```
|
|
303
322
|
|
|
@@ -309,19 +328,32 @@ npm run release -- --bump patch # 同上,并先把版本号升一档(patch/
|
|
|
309
328
|
| ② 校验登录状态 | `npm whoami`,并核对包名 scope 与当前账号是否一致 |
|
|
310
329
|
| ③ 校验版本 | 该版本是否已经发过(避免 409 覆盖失败) |
|
|
311
330
|
| ④ 质量门禁 | `npm run typecheck` + `npm test`,不通过就不发 |
|
|
312
|
-
| ⑤ 构建 | `npm run build` ——
|
|
313
|
-
| ⑥ 打包核对 | `npm pack --dry-run`,并确认 `bin` / `server
|
|
314
|
-
| ⑦
|
|
331
|
+
| ⑤ 构建 | `npm run build` —— 前端 `dist/web` + 后端编译 `dist/node`,**两者缺一使用者都跑不起来** |
|
|
332
|
+
| ⑥ 打包核对 | `npm pack --dry-run`,并确认 `bin` / `dist/node/server/main.js` / `dist/web` 都真的在包里 |
|
|
333
|
+
| ⑦ 真实安装验证 | 把 tarball 装进临时 `node_modules` 启动一次,校验 `/dashboard/` 与接口(见下) |
|
|
334
|
+
| ⑧ 发布 | 确认后执行 `npm publish --access public`;开了 2FA 会提示输入验证码 |
|
|
335
|
+
|
|
336
|
+
其它参数:`--otp 123456`(直接带验证码)、`--yes`(跳过确认)、`--skip-checks`(跳过类型检查与测试)、`--skip-build`(跳过构建)、`--skip-verify`(跳过真实安装验证,**不推荐**)。
|
|
315
337
|
|
|
316
|
-
|
|
338
|
+
> **第 ⑦ 步为什么不能省**:本地跑的是源码,使用者跑的是装进 `node_modules` 的包,两者环境不同。
|
|
339
|
+
> 详见上面「为什么后端必须编译成 JS」——那个坑在本地跑源码时完全看不出来,
|
|
340
|
+
> 只有把包装进 `node_modules` 再启动才能暴露。第 ⑦ 步就是干这个的。
|
|
341
|
+
|
|
342
|
+
单独跑验证(不发布):
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
npm run verify:package # 打包 → 装进临时 node_modules → 启动 → 查接口 → 回收
|
|
346
|
+
npm run verify:package -- --keep # 保留临时目录,便于排查
|
|
347
|
+
```
|
|
317
348
|
|
|
318
349
|
<details>
|
|
319
350
|
<summary>手动发布(不想用脚本时)</summary>
|
|
320
351
|
|
|
321
352
|
```bash
|
|
322
353
|
npm login
|
|
323
|
-
npm run build # ⚠️
|
|
324
|
-
npm
|
|
354
|
+
npm run build # ⚠️ 必须先构建(前端 dist/web + 后端 dist/node),少一个都不行
|
|
355
|
+
npm run verify:package # ⚠️ 强烈建议:确认装进 node_modules 后真的能起来
|
|
356
|
+
npm pack --dry-run # 先看会发出去哪些文件(正常应为 120+ 文件、约 4 MB)
|
|
325
357
|
npm version patch # 升版本号
|
|
326
358
|
npm publish --otp=123456 # ⚠️ 换成认证器里的 6 位码;access=public 已写在 publishConfig 里
|
|
327
359
|
```
|
|
@@ -356,13 +388,18 @@ npx @wanghaopeng1148/deskpet # 真正启动
|
|
|
356
388
|
|
|
357
389
|
```
|
|
358
390
|
bin/ 命令行入口 —— npx 执行的就是它
|
|
359
|
-
server/
|
|
360
|
-
shared/ 前后端共享的纯逻辑
|
|
391
|
+
dist/node/ 后端编译产物 ← 关键:运行的是这里的 JS,不是 server/ 下的 .ts
|
|
361
392
|
dist/web/ 前端构建产物 ← 关键:带上它,使用者才不必自己构建
|
|
393
|
+
server/ 后端源码(供阅读/调试用;运行时不加载它)
|
|
394
|
+
shared/ 前后端共享的纯逻辑(同上)
|
|
362
395
|
resources/ 皮肤、图标等随包资源
|
|
363
396
|
```
|
|
364
397
|
|
|
365
|
-
|
|
398
|
+
> ⚠️ **发布包里必须有 `dist/node`**。`server/*.ts` 只是随包附带,**不能**作为运行入口:
|
|
399
|
+
> Node 禁止对 `node_modules` 下的文件做类型擦除,用户 `npx` 时会直接崩。
|
|
400
|
+
> `npm run release` 的第 ⑤⑥⑦ 步会分别从构建、打包清单、真实安装三个角度挡住这种情况。
|
|
401
|
+
|
|
402
|
+
不会发出去:`src/`(前端源码)、`scripts/`、`tests/`、`node_modules/`。
|
|
366
403
|
|
|
367
404
|
### 依赖的三类归属(别搞混)
|
|
368
405
|
|
|
@@ -374,13 +411,15 @@ resources/ 皮肤、图标等随包资源
|
|
|
374
411
|
|
|
375
412
|
### 发版检查清单
|
|
376
413
|
|
|
377
|
-
`npm run release`
|
|
414
|
+
`npm run release` 已经自动覆盖了大部分(构建、typecheck、测试、打包核对、真实安装验证、`private` 检查、关键文件校验),
|
|
378
415
|
**剩下这几件需要人工把关**:
|
|
379
416
|
|
|
380
417
|
- [ ] 版本号已升(用 `--bump patch` 或手动改 `version`)—— 不升的话 npm 会拒绝重复发布
|
|
381
418
|
- [ ] 依赖声明的改动已用 `npm install --package-lock-only` 同步到 `package-lock.json`
|
|
382
419
|
- [ ] 发布后 `git commit` + `git tag v<版本>`,别只提交代码不打标签
|
|
383
420
|
- [ ] 改了 scope / 包名时,README 里出现的包名一并更新
|
|
421
|
+
- [ ] **手动改过 `server/` 下代码后发版**:确认走的是 `npm run release`(内含 `build:server`),
|
|
422
|
+
别绕过脚本直接 `npm publish` —— 那样发出去的可能是旧的 `dist/node` 产物
|
|
384
423
|
|
|
385
424
|
## 说明
|
|
386
425
|
|
|
@@ -391,4 +430,19 @@ resources/ 皮肤、图标等随包资源
|
|
|
391
430
|
(执行中 / 排队 N),列表里排队任务显示 `#序号`,点「取消排队」可撤销;仪表盘横幅也会显示执行中与排队数。
|
|
392
431
|
前端通过 `GET /api/queue` 拉取,并用 WebSocket `queue-changed` 实时更新。
|
|
393
432
|
- 通知通道:系统原生通知(osascript / PowerShell Toast / notify-send)+ 管理台页面内提示 + 微信。
|
|
394
|
-
-
|
|
433
|
+
- **开机自启**:管理台「设置」页有开关(对应配置项 `system.autoStart`)。开启后各平台的落地方式:
|
|
434
|
+
|
|
435
|
+
| 平台 | 写到哪里 |
|
|
436
|
+
| --- | --- |
|
|
437
|
+
| Windows | 启动文件夹 `DeskPet.cmd`(不依赖 reg / schtasks,受管控的机器也能用) |
|
|
438
|
+
| macOS | `~/Library/LaunchAgents/DeskPet.plist` |
|
|
439
|
+
| Linux | `~/.config/autostart/DeskPet.desktop`(XDG 标准) |
|
|
440
|
+
|
|
441
|
+
注册的命令统一是 `bin/deskpet.mjs --no-open` —— 它内部用当前 node 拉起服务(不依赖 PATH),
|
|
442
|
+
也不会在开机时弹浏览器。**想临时停用,直接删掉对应文件即可**,或把设置页开关关掉。
|
|
443
|
+
服务启动时也会按配置同步一次(关闭时会移除文件)。
|
|
444
|
+
|
|
445
|
+
> 历史坑:本项目早期是 Electron 应用,自启是启动文件夹里的 `DeskPet.lnk`
|
|
446
|
+
> (指向旧的 `pet\dist\DeskPet.exe`)。改成 Node 服务、目录改名后,那个快捷方式不会自动失效消失,
|
|
447
|
+
> 结果就是「以为自启开着,实际每次开机拉起的是旧程序,新服务仍要手动启动」。
|
|
448
|
+
> 所以开启自启时会**主动清理**同名的旧 `.lnk`。
|
package/bin/deskpet.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* npx deskpet --check 只做环境自检,不启动服务
|
|
16
16
|
*/
|
|
17
17
|
import { spawn, spawnSync } from 'node:child_process'
|
|
18
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
18
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
19
19
|
import net from 'node:net'
|
|
20
20
|
import { homedir } from 'node:os'
|
|
21
21
|
import { dirname, join } from 'node:path'
|
|
@@ -25,6 +25,69 @@ const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
|
25
25
|
const isWin = process.platform === 'win32'
|
|
26
26
|
/** 前端入口产物(vite 的 root 是 src、入口是 src/dashboard/index.html,故多一层 dashboard) */
|
|
27
27
|
const distEntry = join(pkgRoot, 'dist', 'web', 'dashboard', 'index.html')
|
|
28
|
+
/**
|
|
29
|
+
* 后端入口。
|
|
30
|
+
*
|
|
31
|
+
* 必须优先用编译产物:Node **禁止对 node_modules 下的文件做类型擦除**
|
|
32
|
+
* (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING),而 npm 安装 / npx 缓存里的包
|
|
33
|
+
* 正位于 node_modules 下 —— 若直接跑 .ts,使用者一启动就会崩。
|
|
34
|
+
* 源码形态(git clone 下来跑)才回退到 .ts + --experimental-strip-types。
|
|
35
|
+
*/
|
|
36
|
+
const compiledEntry = join(pkgRoot, 'dist', 'node', 'server', 'main.js')
|
|
37
|
+
const sourceEntry = join(pkgRoot, 'server', 'main.ts')
|
|
38
|
+
|
|
39
|
+
/** 递归取目录下最新修改时间 */
|
|
40
|
+
function newestMtime(dir) {
|
|
41
|
+
let newest = 0
|
|
42
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
43
|
+
const p = join(dir, entry.name)
|
|
44
|
+
newest = Math.max(newest, entry.isDirectory() ? newestMtime(p) : statSync(p).mtimeMs)
|
|
45
|
+
}
|
|
46
|
+
return newest
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 源码是否比编译产物更新。
|
|
51
|
+
*
|
|
52
|
+
* 为什么需要:bin 优先跑编译产物,若改了 `server/` 却没重新编译,
|
|
53
|
+
* 就会**静默跑旧代码**(开机自启走的正是这条路径,最不容易发现)。
|
|
54
|
+
* 发布包里 npm 会把所有文件时间统一,两者相等 → 视为产物可用,不受影响。
|
|
55
|
+
*/
|
|
56
|
+
function sourceIsNewer() {
|
|
57
|
+
try {
|
|
58
|
+
const compiledTime = statSync(compiledEntry).mtimeMs
|
|
59
|
+
for (const dir of ['server', 'shared']) {
|
|
60
|
+
if (newestMtime(join(pkgRoot, dir)) > compiledTime) return true
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
/* 读不到就按「不更新」处理,退回产物 */
|
|
64
|
+
}
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 返回 { entry, args }:编译产物无需额外参数 */
|
|
69
|
+
function resolveServerEntry() {
|
|
70
|
+
const hasCompiled = existsSync(compiledEntry)
|
|
71
|
+
const hasSource = existsSync(sourceEntry)
|
|
72
|
+
|
|
73
|
+
if (hasCompiled && !(hasSource && sourceIsNewer())) {
|
|
74
|
+
return { entry: compiledEntry, args: [], compiled: true, stale: false }
|
|
75
|
+
}
|
|
76
|
+
if (hasSource) {
|
|
77
|
+
return {
|
|
78
|
+
entry: sourceEntry,
|
|
79
|
+
args: ['--experimental-strip-types'],
|
|
80
|
+
compiled: false,
|
|
81
|
+
// 有产物但源码更新:说明忘了重新编译,提示一下(本次按源码跑,保证跑的是最新代码)
|
|
82
|
+
stale: hasCompiled
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
console.error('[deskpet] 找不到后端入口。')
|
|
86
|
+
console.error(` 已尝试:${compiledEntry}`)
|
|
87
|
+
console.error(` ${sourceEntry}`)
|
|
88
|
+
console.error(' 源码方式请先执行:npm install && npm run build')
|
|
89
|
+
process.exit(1)
|
|
90
|
+
}
|
|
28
91
|
|
|
29
92
|
const argv = process.argv.slice(2)
|
|
30
93
|
const hasFlag = (name) => argv.includes(name)
|
|
@@ -111,15 +174,20 @@ if (!existsSync(distEntry)) {
|
|
|
111
174
|
|
|
112
175
|
const port = resolvePort()
|
|
113
176
|
const url = `http://127.0.0.1:${port}/dashboard/`
|
|
177
|
+
const server = resolveServerEntry()
|
|
178
|
+
if (server.stale) {
|
|
179
|
+
log('提示:server/ 比编译产物新,本次按源码运行(npm run build:server 可更新产物)')
|
|
180
|
+
}
|
|
114
181
|
|
|
115
182
|
if (checkOnly) {
|
|
183
|
+
log(`后端入口:${server.compiled ? '编译产物' : '源码(.ts)'} — ${server.entry}`)
|
|
116
184
|
log(`自检通过:产物就绪、端口 ${port}、入口 ${url}`)
|
|
117
185
|
process.exit(0)
|
|
118
186
|
}
|
|
119
187
|
|
|
120
188
|
// ── 2. 启动后端 ───────────────────────────────────────────
|
|
121
189
|
log(`启动服务(端口 ${port})…`)
|
|
122
|
-
const child = spawn(process.execPath, [
|
|
190
|
+
const child = spawn(process.execPath, [...server.args, server.entry], {
|
|
123
191
|
cwd: pkgRoot,
|
|
124
192
|
stdio: 'inherit',
|
|
125
193
|
env: { ...process.env, DESKPET_PORT: String(port) }
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite 数据库层 — node:sqlite 零依赖
|
|
3
|
+
* 表结构对齐方案文档 §6.1
|
|
4
|
+
*/
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { getConfigDir } from "../utils/paths.js";
|
|
8
|
+
/** 数据库文件路径 */
|
|
9
|
+
export function defaultDbPath() {
|
|
10
|
+
return join(getConfigDir(), 'deskpet.db');
|
|
11
|
+
}
|
|
12
|
+
/** 打开(或创建)数据库并执行迁移 */
|
|
13
|
+
export function openDatabase(path) {
|
|
14
|
+
const db = new DatabaseSync(path ?? defaultDbPath());
|
|
15
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
16
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
17
|
+
migrate(db);
|
|
18
|
+
return db;
|
|
19
|
+
}
|
|
20
|
+
const MIGRATIONS = [
|
|
21
|
+
// ── v1: 初始表结构 ──────────────────────────────────────
|
|
22
|
+
`
|
|
23
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
name TEXT NOT NULL,
|
|
26
|
+
type TEXT NOT NULL, -- manual/scheduled/scene
|
|
27
|
+
action TEXT NOT NULL, -- python_script/show_reminder/...
|
|
28
|
+
action_params TEXT DEFAULT '{}', -- JSON
|
|
29
|
+
trigger_type TEXT DEFAULT '', -- once/cron/delay/interval/idle/startup/network
|
|
30
|
+
trigger_params TEXT DEFAULT '{}', -- JSON
|
|
31
|
+
script_path TEXT DEFAULT '',
|
|
32
|
+
interpreter TEXT,
|
|
33
|
+
script_args TEXT DEFAULT '[]', -- JSON array
|
|
34
|
+
script_timeout INTEGER DEFAULT 120,
|
|
35
|
+
work_dir TEXT,
|
|
36
|
+
priority INTEGER DEFAULT 1,
|
|
37
|
+
retry_count INTEGER DEFAULT 0,
|
|
38
|
+
enabled INTEGER DEFAULT 1,
|
|
39
|
+
notify_channels TEXT DEFAULT '["desktop"]', -- JSON
|
|
40
|
+
chain_next TEXT,
|
|
41
|
+
chain_condition TEXT,
|
|
42
|
+
one_time INTEGER DEFAULT 0,
|
|
43
|
+
status TEXT DEFAULT 'idle',
|
|
44
|
+
execution_count INTEGER DEFAULT 0,
|
|
45
|
+
created_at TEXT,
|
|
46
|
+
updated_at TEXT
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE IF NOT EXISTS executions (
|
|
50
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
51
|
+
task_id TEXT NOT NULL,
|
|
52
|
+
task_name TEXT NOT NULL,
|
|
53
|
+
status TEXT NOT NULL, -- running/completed/failed/timeout/cancelled
|
|
54
|
+
started_at TEXT NOT NULL,
|
|
55
|
+
finished_at TEXT,
|
|
56
|
+
exit_code INTEGER,
|
|
57
|
+
duration_ms INTEGER,
|
|
58
|
+
stdout TEXT DEFAULT '',
|
|
59
|
+
stderr TEXT DEFAULT '',
|
|
60
|
+
error TEXT DEFAULT ''
|
|
61
|
+
);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_executions_task ON executions(task_id, started_at);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_executions_started ON executions(started_at);
|
|
64
|
+
|
|
65
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
66
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
+
created_at TEXT NOT NULL,
|
|
68
|
+
reason TEXT NOT NULL, -- manual/auto
|
|
69
|
+
data TEXT NOT NULL -- 完整配置 JSON
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE TABLE IF NOT EXISTS wechat_messages (
|
|
73
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
74
|
+
direction TEXT NOT NULL, -- in/out
|
|
75
|
+
content TEXT,
|
|
76
|
+
created_at TEXT
|
|
77
|
+
);
|
|
78
|
+
`,
|
|
79
|
+
// ── v2: 执行记录补记子进程 pid ────────────────────────────
|
|
80
|
+
//
|
|
81
|
+
// 为什么需要:服务被强杀 / 重启后重新启动时,原先只能「有罪推定」——
|
|
82
|
+
// 库里凡是 running 的执行记录一律收尾为「服务重启,执行被中断」。
|
|
83
|
+
// 但子进程(python 等)在 Windows 上并不会随父进程退出而死,
|
|
84
|
+
// 于是会出现「任务被判失败、脚本其实还在后台跑」的错判。
|
|
85
|
+
// 记下 pid 后,启动时可以先探测进程是否还活着,再决定怎么定性。
|
|
86
|
+
`
|
|
87
|
+
ALTER TABLE executions ADD COLUMN pid INTEGER;
|
|
88
|
+
`
|
|
89
|
+
];
|
|
90
|
+
/** 按 user_version 增量迁移 */
|
|
91
|
+
export function migrate(db) {
|
|
92
|
+
const row = db.prepare('PRAGMA user_version').get();
|
|
93
|
+
let version = row?.user_version ?? 0;
|
|
94
|
+
while (version < MIGRATIONS.length) {
|
|
95
|
+
db.exec('BEGIN');
|
|
96
|
+
try {
|
|
97
|
+
db.exec(MIGRATIONS[version]);
|
|
98
|
+
version += 1;
|
|
99
|
+
db.exec(`PRAGMA user_version = ${version}`);
|
|
100
|
+
db.exec('COMMIT');
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
db.exec('ROLLBACK');
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** 便于测试:内存数据库 */
|
|
109
|
+
export function openMemoryDatabase() {
|
|
110
|
+
const db = new DatabaseSync(':memory:');
|
|
111
|
+
migrate(db);
|
|
112
|
+
return db;
|
|
113
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 旧版数据迁移 — Python 版 tasks.json → SQLite(方案 §6.2 迁移策略)
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { newTaskId } from "./task-repository.js";
|
|
6
|
+
/** 解析旧版 tasks.json 内容为 TaskConfig 数组 */
|
|
7
|
+
export function parseLegacyTasks(rawJson) {
|
|
8
|
+
let data;
|
|
9
|
+
try {
|
|
10
|
+
data = JSON.parse(rawJson);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const t of data.tasks ?? []) {
|
|
17
|
+
try {
|
|
18
|
+
out.push(legacyToConfig(t));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// 单条失败不影响整体导入
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function legacyToConfig(t) {
|
|
27
|
+
const tr = t.trigger ?? {};
|
|
28
|
+
const trigger = {
|
|
29
|
+
type: String(tr.type ?? 'manual'),
|
|
30
|
+
datetime: tr.datetime ?? null,
|
|
31
|
+
cron: tr.cron ?? null,
|
|
32
|
+
delaySeconds: tr.delay_seconds ?? null,
|
|
33
|
+
intervalMinutes: tr.interval_minutes ?? null,
|
|
34
|
+
idleMinutes: tr.idle_minutes ?? null,
|
|
35
|
+
maxExecutions: typeof tr.max_executions === 'number' ? tr.max_executions : -1,
|
|
36
|
+
holidayCheck: !!tr.holiday_check
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
name: String(t.name ?? '未命名任务'),
|
|
40
|
+
type: t.type ?? 'manual',
|
|
41
|
+
trigger,
|
|
42
|
+
action: String(t.action ?? 'python_script'),
|
|
43
|
+
actionParams: t.action_params ?? {},
|
|
44
|
+
scriptPath: t.script_path ?? '',
|
|
45
|
+
interpreter: t.interpreter ?? null,
|
|
46
|
+
scriptArgs: t.script_args ?? [],
|
|
47
|
+
scriptTimeout: t.script_timeout ?? 120,
|
|
48
|
+
scriptWorkDir: t.script_work_dir ?? null,
|
|
49
|
+
retryCount: t.retry_count ?? 0,
|
|
50
|
+
enabled: t.enabled !== false,
|
|
51
|
+
notifyChannels: ['desktop'],
|
|
52
|
+
chainNext: null,
|
|
53
|
+
chainCondition: null,
|
|
54
|
+
oneTime: !!t.one_time
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 从旧版 tasks.json 导入任务到 SQLite。
|
|
59
|
+
* 仅当任务表为空且文件存在时导入(避免重复导入)。
|
|
60
|
+
*/
|
|
61
|
+
export function migrateLegacyTasksFile(filePath, repo) {
|
|
62
|
+
if (!existsSync(filePath)) {
|
|
63
|
+
return { imported: 0, skipped: true, reason: '文件不存在' };
|
|
64
|
+
}
|
|
65
|
+
if (repo.count() > 0) {
|
|
66
|
+
return { imported: 0, skipped: true, reason: '已有任务数据,跳过导入' };
|
|
67
|
+
}
|
|
68
|
+
const configs = parseLegacyTasks(readFileSync(filePath, 'utf-8'));
|
|
69
|
+
const now = new Date().toISOString();
|
|
70
|
+
let imported = 0;
|
|
71
|
+
for (const config of configs) {
|
|
72
|
+
const id = newTaskId();
|
|
73
|
+
repo.insert({
|
|
74
|
+
id,
|
|
75
|
+
config,
|
|
76
|
+
status: config.enabled ? 'idle' : 'disabled',
|
|
77
|
+
createdAt: now,
|
|
78
|
+
executionCount: 0,
|
|
79
|
+
lastRunAt: null,
|
|
80
|
+
lastResult: null,
|
|
81
|
+
lastError: null,
|
|
82
|
+
nextRunAt: null,
|
|
83
|
+
currentRetry: 0
|
|
84
|
+
});
|
|
85
|
+
imported += 1;
|
|
86
|
+
}
|
|
87
|
+
return { imported, skipped: false };
|
|
88
|
+
}
|