@wanghaopeng1148/deskpet 2.0.0

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.
Files changed (76) hide show
  1. package/README.md +394 -0
  2. package/bin/deskpet.mjs +142 -0
  3. package/dist/web/assets/DashboardView-BLXa7VrZ.css +1 -0
  4. package/dist/web/assets/DashboardView-CTyWcLtQ.js +60 -0
  5. package/dist/web/assets/SettingsView-7c3RiRrt.js +1 -0
  6. package/dist/web/assets/SettingsView-BkL0OpZC.css +1 -0
  7. package/dist/web/assets/TasksView-BTK1OTwU.css +1 -0
  8. package/dist/web/assets/TasksView-s1MaR5sZ.js +5 -0
  9. package/dist/web/assets/ToolsView-B8V-A1j_.js +178 -0
  10. package/dist/web/assets/ToolsView-DGLJATQ9.css +1 -0
  11. package/dist/web/assets/WeChatView-ChLBhPso.js +1 -0
  12. package/dist/web/assets/WeChatView-CvdhJ05E.css +1 -0
  13. package/dist/web/assets/browser-CjSdxGTc.js +8 -0
  14. package/dist/web/assets/dashboard-DJ_Miuzx.js +93 -0
  15. package/dist/web/assets/dashboard-Fbcagzwx.css +1 -0
  16. package/dist/web/dashboard/index.html +13 -0
  17. package/package.json +71 -0
  18. package/resources/icons/tray.png +0 -0
  19. package/resources/icons/tray@2x.png +0 -0
  20. package/resources/previews/busy.png +0 -0
  21. package/resources/previews/click.png +0 -0
  22. package/resources/previews/hover.png +0 -0
  23. package/resources/previews/idle.png +0 -0
  24. package/resources/previews/preview.png +0 -0
  25. package/resources/previews/sleep.png +0 -0
  26. package/resources/skins/default_cute/pet.json +7 -0
  27. package/resources/skins/default_cute/spritesheet.webp +0 -0
  28. package/resources/skins/default_cute/submission.json +29 -0
  29. package/server/db/database.ts +118 -0
  30. package/server/db/migrate-legacy.ts +121 -0
  31. package/server/db/task-repository.ts +585 -0
  32. package/server/http/http-server.ts +725 -0
  33. package/server/http/ws-hub.ts +66 -0
  34. package/server/main.ts +322 -0
  35. package/server/plugins/actions/builtin.ts +73 -0
  36. package/server/plugins/actions/clipboard-watch.ts +38 -0
  37. package/server/plugins/actions/http-request.ts +53 -0
  38. package/server/plugins/actions/jenkins-build.ts +209 -0
  39. package/server/plugins/actions/open-app.ts +40 -0
  40. package/server/plugins/actions/python-script.ts +187 -0
  41. package/server/plugins/actions/screenshot.ts +41 -0
  42. package/server/plugins/actions/send-keystroke.ts +103 -0
  43. package/server/plugins/actions/show-reminder.ts +16 -0
  44. package/server/plugins/actions/ssh-command.ts +148 -0
  45. package/server/plugins/actions/task-chain.ts +30 -0
  46. package/server/plugins/actions/volume-control.ts +35 -0
  47. package/server/plugins/index.ts +37 -0
  48. package/server/plugins/registry.ts +72 -0
  49. package/server/services/clipboard-watcher.ts +126 -0
  50. package/server/services/config-store.ts +152 -0
  51. package/server/services/idle-monitor.ts +146 -0
  52. package/server/services/notifier.ts +54 -0
  53. package/server/services/quick-actions-store.ts +54 -0
  54. package/server/services/remote-connector.ts +75 -0
  55. package/server/services/scanner-reader.ts +257 -0
  56. package/server/services/script-runner.ts +263 -0
  57. package/server/services/snapshot-service.ts +196 -0
  58. package/server/services/task-scheduler.ts +900 -0
  59. package/server/services/wechat-bot.ts +744 -0
  60. package/server/services/wechat-command-types.ts +15 -0
  61. package/server/services/wechat-commands.ts +367 -0
  62. package/server/suppress-warnings.ts +10 -0
  63. package/server/utils/asset-url.ts +27 -0
  64. package/server/utils/auto-start.ts +90 -0
  65. package/server/utils/clipboard.ts +50 -0
  66. package/server/utils/dashboard-url.ts +9 -0
  67. package/server/utils/instance-guard.ts +170 -0
  68. package/server/utils/native-notify.ts +68 -0
  69. package/server/utils/open.ts +38 -0
  70. package/server/utils/paths.ts +72 -0
  71. package/server/utils/python-interpreter.ts +154 -0
  72. package/shared/animation-engine.ts +422 -0
  73. package/shared/chain-condition.ts +60 -0
  74. package/shared/cron-weekly.ts +131 -0
  75. package/shared/py-task-params.ts +356 -0
  76. package/shared/types.ts +309 -0
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>DeskPet 管理台</title>
7
+ <script type="module" crossorigin src="/assets/dashboard-DJ_Miuzx.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/dashboard-Fbcagzwx.css">
9
+ </head>
10
+ <body>
11
+ <div id="app"></div>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@wanghaopeng1148/deskpet",
3
+ "productName": "DeskPet",
4
+ "version": "2.0.0",
5
+ "description": "DeskPet 2.0 — 本机任务自动化服务 + Web 管理台 (Node + Express + Vue 3 + TypeScript)",
6
+ "type": "module",
7
+ "author": "whp",
8
+ "license": "MIT",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "bin": {
13
+ "deskpet": "./bin/deskpet.mjs"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "server",
18
+ "shared",
19
+ "dist/web",
20
+ "resources",
21
+ "README.md"
22
+ ],
23
+ "engines": {
24
+ "node": ">=22.22.0"
25
+ },
26
+ "scripts": {
27
+ "dev": "node scripts/dev-all.mjs",
28
+ "dev:watch": "node scripts/dev-all.mjs --watch",
29
+ "dev:server": "node --experimental-strip-types --watch server/main.ts",
30
+ "dev:web": "vite",
31
+ "build": "vite build",
32
+ "build:web": "vite build",
33
+ "start": "node scripts/start.mjs",
34
+ "serve": "node --experimental-strip-types server/main.ts",
35
+ "release": "node scripts/publish.mjs",
36
+ "typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
37
+ "test": "node --import ./tests/setup-env.mjs --test --experimental-strip-types \"tests/unit/*.test.ts\"",
38
+ "test:unit": "node --import ./tests/setup-env.mjs --test --experimental-strip-types \"tests/unit/*.test.ts\""
39
+ },
40
+ "dependencies": {
41
+ "express": "^5.2.1",
42
+ "node-schedule": "^2.1.1",
43
+ "ssh2": "^1.17.0",
44
+ "ws": "^8.21.3"
45
+ },
46
+ "optionalDependencies": {
47
+ "serialport": "^13.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/express": "^5.0.6",
51
+ "@types/node": "^22.10.2",
52
+ "@types/node-schedule": "^2.1.8",
53
+ "@types/qrcode": "^1.5.6",
54
+ "@types/spark-md5": "^3.0.5",
55
+ "@types/ssh2": "^1.15.6",
56
+ "@types/ws": "^8.18.1",
57
+ "@vitejs/plugin-vue": "^5.2.1",
58
+ "cron-parser": "^5.10.0",
59
+ "echarts": "^6.1.0",
60
+ "element-plus": "^2.14.5",
61
+ "jsqr": "^1.4.0",
62
+ "pinia": "^2.2.6",
63
+ "qrcode": "^1.5.4",
64
+ "spark-md5": "^3.0.2",
65
+ "typescript": "^5.6.3",
66
+ "vite": "^5.4.11",
67
+ "vue": "^3.5.13",
68
+ "vue-router": "^4.4.5",
69
+ "yaml": "^2.9.1"
70
+ }
71
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "saber--petdex-zhenyou-ling",
3
+ "displayName": "Saber",
4
+ "description": "试问,你是我的御主吗?",
5
+ "spritesheetPath": "spritesheet.webp",
6
+ "kind": "character"
7
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "slug": "saber--petdex-zhenyou-ling",
3
+ "pet_slug": "saber",
4
+ "author_slug": "petdex-zhenyou-ling",
5
+ "name": "Saber",
6
+ "localized_names": {
7
+ "en": "Saber",
8
+ "zh": "阿尔托莉雅"
9
+ },
10
+ "author": "真宵 绫.",
11
+ "author_handle": "真宵 绫.",
12
+ "primary_category": "Anime Characters",
13
+ "tags": ["fate", "pixel-art", "chibi"],
14
+ "collections": ["fate"],
15
+ "source_type": "petdex",
16
+ "source_url": "https://petdex.dev/pets/saber",
17
+ "source_pet_json": "https://assets.petdex.dev/pets/saber-f1d13215f002/petjson.json",
18
+ "license": "Source author terms; personal non-commercial use only",
19
+ "preview_image": "../../assets/previews/saber--petdex-zhenyou-ling/gifs/idle.gif",
20
+ "codex_install": {
21
+ "pet_json": "pet.json",
22
+ "spritesheet": "spritesheet.webp"
23
+ },
24
+ "preview_assets": {
25
+ "contact_sheet": "../../assets/previews/saber--petdex-zhenyou-ling/contact-sheet.png",
26
+ "gifs_dir": "../../assets/previews/saber--petdex-zhenyou-ling/gifs"
27
+ },
28
+ "spritesheet_sha256": "6e9da7a6cd1d00ebd9a41e66bf9e8a7f4eb97bb2ea4c8b6097d359b5f58b01e1"
29
+ }
@@ -0,0 +1,118 @@
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.ts'
8
+
9
+ /** 数据库文件路径 */
10
+ export function defaultDbPath(): string {
11
+ return join(getConfigDir(), 'deskpet.db')
12
+ }
13
+
14
+ /** 打开(或创建)数据库并执行迁移 */
15
+ export function openDatabase(path?: string): DatabaseSync {
16
+ const db = new DatabaseSync(path ?? defaultDbPath())
17
+ db.exec('PRAGMA journal_mode = WAL')
18
+ db.exec('PRAGMA foreign_keys = ON')
19
+ migrate(db)
20
+ return db
21
+ }
22
+
23
+ const MIGRATIONS: string[] = [
24
+ // ── v1: 初始表结构 ──────────────────────────────────────
25
+ `
26
+ CREATE TABLE IF NOT EXISTS tasks (
27
+ id TEXT PRIMARY KEY,
28
+ name TEXT NOT NULL,
29
+ type TEXT NOT NULL, -- manual/scheduled/scene
30
+ action TEXT NOT NULL, -- python_script/show_reminder/...
31
+ action_params TEXT DEFAULT '{}', -- JSON
32
+ trigger_type TEXT DEFAULT '', -- once/cron/delay/interval/idle/startup/network
33
+ trigger_params TEXT DEFAULT '{}', -- JSON
34
+ script_path TEXT DEFAULT '',
35
+ interpreter TEXT,
36
+ script_args TEXT DEFAULT '[]', -- JSON array
37
+ script_timeout INTEGER DEFAULT 120,
38
+ work_dir TEXT,
39
+ priority INTEGER DEFAULT 1,
40
+ retry_count INTEGER DEFAULT 0,
41
+ enabled INTEGER DEFAULT 1,
42
+ notify_channels TEXT DEFAULT '["desktop"]', -- JSON
43
+ chain_next TEXT,
44
+ chain_condition TEXT,
45
+ one_time INTEGER DEFAULT 0,
46
+ status TEXT DEFAULT 'idle',
47
+ execution_count INTEGER DEFAULT 0,
48
+ created_at TEXT,
49
+ updated_at TEXT
50
+ );
51
+
52
+ CREATE TABLE IF NOT EXISTS executions (
53
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
54
+ task_id TEXT NOT NULL,
55
+ task_name TEXT NOT NULL,
56
+ status TEXT NOT NULL, -- running/completed/failed/timeout/cancelled
57
+ started_at TEXT NOT NULL,
58
+ finished_at TEXT,
59
+ exit_code INTEGER,
60
+ duration_ms INTEGER,
61
+ stdout TEXT DEFAULT '',
62
+ stderr TEXT DEFAULT '',
63
+ error TEXT DEFAULT ''
64
+ );
65
+ CREATE INDEX IF NOT EXISTS idx_executions_task ON executions(task_id, started_at);
66
+ CREATE INDEX IF NOT EXISTS idx_executions_started ON executions(started_at);
67
+
68
+ CREATE TABLE IF NOT EXISTS snapshots (
69
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
70
+ created_at TEXT NOT NULL,
71
+ reason TEXT NOT NULL, -- manual/auto
72
+ data TEXT NOT NULL -- 完整配置 JSON
73
+ );
74
+
75
+ CREATE TABLE IF NOT EXISTS wechat_messages (
76
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
77
+ direction TEXT NOT NULL, -- in/out
78
+ content TEXT,
79
+ created_at TEXT
80
+ );
81
+ `,
82
+
83
+ // ── v2: 执行记录补记子进程 pid ────────────────────────────
84
+ //
85
+ // 为什么需要:服务被强杀 / 重启后重新启动时,原先只能「有罪推定」——
86
+ // 库里凡是 running 的执行记录一律收尾为「服务重启,执行被中断」。
87
+ // 但子进程(python 等)在 Windows 上并不会随父进程退出而死,
88
+ // 于是会出现「任务被判失败、脚本其实还在后台跑」的错判。
89
+ // 记下 pid 后,启动时可以先探测进程是否还活着,再决定怎么定性。
90
+ `
91
+ ALTER TABLE executions ADD COLUMN pid INTEGER;
92
+ `
93
+ ]
94
+
95
+ /** 按 user_version 增量迁移 */
96
+ export function migrate(db: DatabaseSync): void {
97
+ const row = db.prepare('PRAGMA user_version').get() as { user_version: number }
98
+ let version = row?.user_version ?? 0
99
+ while (version < MIGRATIONS.length) {
100
+ db.exec('BEGIN')
101
+ try {
102
+ db.exec(MIGRATIONS[version])
103
+ version += 1
104
+ db.exec(`PRAGMA user_version = ${version}`)
105
+ db.exec('COMMIT')
106
+ } catch (err) {
107
+ db.exec('ROLLBACK')
108
+ throw err
109
+ }
110
+ }
111
+ }
112
+
113
+ /** 便于测试:内存数据库 */
114
+ export function openMemoryDatabase(): DatabaseSync {
115
+ const db = new DatabaseSync(':memory:')
116
+ migrate(db)
117
+ return db
118
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * 旧版数据迁移 — Python 版 tasks.json → SQLite(方案 §6.2 迁移策略)
3
+ */
4
+ import { existsSync, readFileSync } from 'node:fs'
5
+ import type { TaskConfig, TaskStatus, TaskTrigger } from '../../shared/types.ts'
6
+ import type { TaskRepository } from './task-repository.ts'
7
+ import { newTaskId } from './task-repository.ts'
8
+
9
+ interface LegacyTask {
10
+ id?: string
11
+ name?: string
12
+ type?: string
13
+ trigger?: Record<string, unknown>
14
+ priority?: number
15
+ action?: string
16
+ action_params?: Record<string, unknown>
17
+ script_path?: string
18
+ interpreter?: string | null
19
+ script_args?: string[]
20
+ script_timeout?: number
21
+ script_work_dir?: string | null
22
+ retry_count?: number
23
+ enabled?: boolean
24
+ notify_on_completion?: boolean
25
+ one_time?: boolean
26
+ status?: string
27
+ execution_count?: number
28
+ }
29
+
30
+ /** 解析旧版 tasks.json 内容为 TaskConfig 数组 */
31
+ export function parseLegacyTasks(rawJson: string): TaskConfig[] {
32
+ let data: { tasks?: LegacyTask[] }
33
+ try {
34
+ data = JSON.parse(rawJson) as { tasks?: LegacyTask[] }
35
+ } catch {
36
+ return []
37
+ }
38
+ const out: TaskConfig[] = []
39
+ for (const t of data.tasks ?? []) {
40
+ try {
41
+ out.push(legacyToConfig(t))
42
+ } catch {
43
+ // 单条失败不影响整体导入
44
+ }
45
+ }
46
+ return out
47
+ }
48
+
49
+ function legacyToConfig(t: LegacyTask): TaskConfig {
50
+ const tr = t.trigger ?? {}
51
+ const trigger: TaskTrigger = {
52
+ type: String(tr.type ?? 'manual'),
53
+ datetime: (tr.datetime as string | null) ?? null,
54
+ cron: (tr.cron as string | null) ?? null,
55
+ delaySeconds: (tr.delay_seconds as number | null) ?? null,
56
+ intervalMinutes: (tr.interval_minutes as number | null) ?? null,
57
+ idleMinutes: (tr.idle_minutes as number | null) ?? null,
58
+ maxExecutions: typeof tr.max_executions === 'number' ? tr.max_executions : -1,
59
+ holidayCheck: !!tr.holiday_check
60
+ }
61
+ return {
62
+ name: String(t.name ?? '未命名任务'),
63
+ type: (t.type as TaskConfig['type']) ?? 'manual',
64
+ trigger,
65
+ action: String(t.action ?? 'python_script'),
66
+ actionParams: t.action_params ?? {},
67
+ scriptPath: t.script_path ?? '',
68
+ interpreter: t.interpreter ?? null,
69
+ scriptArgs: t.script_args ?? [],
70
+ scriptTimeout: t.script_timeout ?? 120,
71
+ scriptWorkDir: t.script_work_dir ?? null,
72
+ retryCount: t.retry_count ?? 0,
73
+ enabled: t.enabled !== false,
74
+ notifyChannels: ['desktop'],
75
+ chainNext: null,
76
+ chainCondition: null,
77
+ oneTime: !!t.one_time
78
+ }
79
+ }
80
+
81
+ export interface MigrateResult {
82
+ imported: number
83
+ skipped: boolean
84
+ reason?: string
85
+ }
86
+
87
+ /**
88
+ * 从旧版 tasks.json 导入任务到 SQLite。
89
+ * 仅当任务表为空且文件存在时导入(避免重复导入)。
90
+ */
91
+ export function migrateLegacyTasksFile(
92
+ filePath: string,
93
+ repo: TaskRepository
94
+ ): MigrateResult {
95
+ if (!existsSync(filePath)) {
96
+ return { imported: 0, skipped: true, reason: '文件不存在' }
97
+ }
98
+ if (repo.count() > 0) {
99
+ return { imported: 0, skipped: true, reason: '已有任务数据,跳过导入' }
100
+ }
101
+ const configs = parseLegacyTasks(readFileSync(filePath, 'utf-8'))
102
+ const now = new Date().toISOString()
103
+ let imported = 0
104
+ for (const config of configs) {
105
+ const id = newTaskId()
106
+ repo.insert({
107
+ id,
108
+ config,
109
+ status: config.enabled ? ('idle' as TaskStatus) : ('disabled' as TaskStatus),
110
+ createdAt: now,
111
+ executionCount: 0,
112
+ lastRunAt: null,
113
+ lastResult: null,
114
+ lastError: null,
115
+ nextRunAt: null,
116
+ currentRetry: 0
117
+ })
118
+ imported += 1
119
+ }
120
+ return { imported, skipped: false }
121
+ }