@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
package/README.md ADDED
@@ -0,0 +1,394 @@
1
+ # DeskPet 2.0
2
+
3
+ 本机任务自动化服务 + Web 管理台 — Node 22 + Express + Vue 3 + TypeScript。
4
+
5
+ 服务常驻本机,负责定时任务、Python 脚本、微信 Bot、扫码枪等本机能力;
6
+ 管理台是纯前端页面,用浏览器访问即可(同源部署时由服务静态托管)。
7
+
8
+ > **最快启动** 源码方式:`npm install && npm start` · 发布到 npm 后:`npx @wanghaopeng1148/deskpet`
9
+ > 三种启动方式的完整说明见 [安装与启动](#安装与启动),发版流程见 [发布到 npm](#发布到-npm)。
10
+
11
+ ## 技术栈
12
+
13
+ - **Node 22**(内置 `node:sqlite`,直接运行 TypeScript)
14
+ - **Express 5 + WebSocket**(REST 接口 + 实时事件推送)
15
+ - **Vue 3 + Pinia + Vue Router + Element Plus**(管理台)
16
+ - **Vite 5 + TypeScript**
17
+
18
+ ## 环境要求
19
+
20
+ | 项目 | 要求 | 说明 |
21
+ | --- | --- | --- |
22
+ | Node.js | **≥ 22.22** | 用到内置 `node:sqlite`,并直接以 `.ts` 运行后端;已在 `package.json` 的 `engines` 声明 |
23
+ | Python | 可选(≥ 3.8) | 只有「Python 脚本」类任务需要;脚本用到的第三方库(`requests`、`paramiko` 等)需自行安装 |
24
+ | 操作系统 | Windows / macOS / Linux | 本机动作与开机自启的差异见文末「说明」 |
25
+
26
+ > 使用者侧**不需要 Git、不需要构建工具** —— 见下面「方式一」。
27
+
28
+ ## 安装与启动
29
+
30
+ ### 方式一:一条命令(使用者)
31
+
32
+ ```bash
33
+ npx @wanghaopeng1148/deskpet
34
+ ```
35
+
36
+ 自动拉取并启动服务,随后打开浏览器到 `http://127.0.0.1:3210/dashboard/`。
37
+
38
+ | 参数 | 作用 |
39
+ | --- | --- |
40
+ | `--no-open` | 只启动、不打开浏览器(SSH 远程 / 服务器上用) |
41
+ | `--port 3211` | 指定端口(等价于设置 `DESKPET_PORT`) |
42
+ | `--check` | 只做环境自检(产物是否就绪、将用哪个端口),不启动服务 |
43
+
44
+ > `<scope>` 需替换为实际发布用的 npm 包名,见文末「发布到 npm」。尚未发布时请先用方式二。
45
+
46
+ ### 方式二:从源码运行
47
+
48
+ ```bash
49
+ git clone <仓库地址>
50
+ cd pet2.0
51
+ npm install
52
+ npm start
53
+ ```
54
+
55
+ `npm start` 本身就是一步到位:缺依赖会先装,前端产物缺失或比源码旧会先构建,然后启动服务。
56
+
57
+ ```bash
58
+ npm start -- --check # 只确认环境是否就绪(同样会补依赖/构建,但不启动)
59
+ npm run serve # 跳过所有检查,直接起后端(自己确定产物没问题时用)
60
+ ```
61
+
62
+ ### 方式三:开发模式(要改代码)
63
+
64
+ ```bash
65
+ npm run dev # 后端 :3210 + Vite :5173(HMR),浏览器打开 http://localhost:5173/dashboard/
66
+ npm run dev:watch # 同上,但后端带 --watch 热重载(改 server/ 代码即重启,见下方警告)
67
+ ```
68
+
69
+ > ⚠️ **改后端代码前先确认没有任务在跑。** `--watch` 重启走不到进程的退出收尾
70
+ > (Windows 上更是直接终止,压根没有可捕获的信号),正在执行的任务会被判成
71
+ > 「服务重启,执行被中断」,且 Python 子进程可能变成孤儿继续跑。
72
+ > 因此 `npm run dev` **默认不开**后端热重载;确实需要时用 `npm run dev:watch` 自行承担。
73
+
74
+ ### 三种方式对比
75
+
76
+ | | 方式一 `npx` | 方式二 源码 | 方式三 开发 |
77
+ | --- | --- | --- | --- |
78
+ | 需要 Git / 构建 | 否 | 是(`npm start` 会自动处理) | 是 |
79
+ | 前端来源 | 包里自带的 `dist/web` | 构建生成 | Vite 实时编译 |
80
+ | 访问地址 | `127.0.0.1:3210` | `127.0.0.1:3210` | 前端 `:5173`,后端 `:3210` |
81
+ | 适合 | 只想用起来 | 部署 / 长期常驻 | 改代码 |
82
+
83
+ ### 环境变量
84
+
85
+ | 变量 | 作用 |
86
+ | --- | --- |
87
+ | `DESKPET_HOME` | 数据目录,默认 `~/.deskpet`;换一个即可让多个实例互不干扰(调试常用) |
88
+ | `DESKPET_PORT` | 覆盖监听端口(优先于 `settings.json`) |
89
+ | `DESKPET_HOST` | 覆盖监听地址(默认 `127.0.0.1`;要让局域网内其他人访问需改成 `0.0.0.0`) |
90
+ | `DESKPET_RESOURCES` | 内置资源目录,默认取包内 `resources/` |
91
+
92
+ ## 目录结构
93
+
94
+ ```
95
+ ├── server/ # 后端服务 (Node, 无 Electron)
96
+ │ ├── main.ts # 入口: 任务系统 / 微信 / 扫码枪 / HTTP+WS 服务
97
+ │ ├── http/ # Express 应用与 WebSocket 广播
98
+ │ ├── db/ # SQLite (node:sqlite) 与任务仓储
99
+ │ ├── plugins/ # 动作插件注册表与内置动作
100
+ │ ├── services/ # 配置 / 调度 / 剪贴板监听 ...
101
+ │ └── utils/ # 路径 / 资源 URL / 剪贴板 / 打开外部 / 原生通知
102
+ ├── src/dashboard/ # 管理台前端 (唯一前端入口)
103
+ ├── shared/ # 前后端共享纯逻辑 (类型)
104
+ └── tests/unit/ # 单元测试 (node:test)
105
+ ```
106
+
107
+ ## 开发辅助命令
108
+
109
+ 启动方式见上面「安装与启动」,这里只列改代码时会用到的:
110
+
111
+ | 命令 | 作用 |
112
+ | --- | --- |
113
+ | `npm run build` | 构建前端到 `dist/web`(发版前必须跑) |
114
+ | `npm run typecheck` | TS 类型检查(`tsconfig.node.json` + `tsconfig.web.json` 各一遍) |
115
+ | `npm test` | 单元测试(内存 SQLite,不触碰真实数据) |
116
+ | `npm run dev:server` | 只起后端(带 `--watch`) |
117
+ | `npm run dev:web` | 只起 Vite(⚠️ 单独跑会因后端没起而报 `Failed to fetch`) |
118
+ | `npm run serve` | 直接起后端,不做任何依赖/产物检查 |
119
+
120
+ 目录约定:后端在 `server/`,前端在 `src/`,两侧共享的纯逻辑放 `shared/`
121
+ (那里**不要** import 任何 Node 或浏览器专有 API)。
122
+
123
+ ## 服务重启与任务中断(重要)
124
+
125
+ 任务执行期间如果后端进程退出,会出现两类结果,管理台会区分显示:
126
+
127
+ | 情况 | 退出方式 | 执行记录里的文案 | 子进程 |
128
+ | --- | --- | --- | --- |
129
+ | 正常退出(Ctrl+C / 关窗口) | 走 `SIGINT`/`SIGTERM` 收尾 | `服务退出,执行被中断(非任务自身错误)` | 被主动终止 |
130
+ | 被强杀 / 崩溃 / 热重载 | 来不及收尾,下次启动兜底 | `服务重启,执行被中断` | 可能仍在后台运行 |
131
+
132
+ 关于「仍在后台运行」:Windows 上父进程退出**不会**带走子进程,所以强杀时 python 可能变成孤儿
133
+ 继续改远端状态。为此执行记录会落库子进程 `pid`,下次启动时逐个探测:
134
+
135
+ - `pid` 已消失 → 按「执行被中断」收尾;
136
+ - `pid` 还活着 → 标注为「脚本仍在后台运行(pid N)」,**不擅自终止**,请先确认它的状态再重跑。
137
+
138
+ 日志抽屉里的「实时输出」面板在检测到上次是中断结束时,会直接给出这条提示,不必再翻执行历史。
139
+
140
+ **单实例保护**:启动时用原子锁文件 `~/.deskpet/.deskpet.lock`(`O_EXCL` 一步完成检查+创建)
141
+ 判定是否已有实例在跑。拿不到锁的实例会跳过「残留任务恢复」,
142
+ 避免把对方正在执行的任务掐成「服务重启,执行被中断」。
143
+
144
+ ## 数据目录
145
+
146
+ 默认 `~/.deskpet/`,可用环境变量 `DESKPET_HOME` 覆盖:
147
+
148
+ ```
149
+ ~/.deskpet/
150
+ ├── config/ settings.json / wechat.json / quick-actions.json
151
+ ├── logs/ 脚本自身写的运行日志
152
+ ├── .deskpet.lock 单实例锁(内容为持有者 pid)
153
+ ├── .deskpet-alive 心跳(pid:时间戳)
154
+ └── deskpet.db SQLite(任务 / 执行记录 / 快照 / 微信消息)
155
+ ```
156
+
157
+ ## 任务动作
158
+
159
+ | 动作 | 说明 |
160
+ |---|---|
161
+ | `ssh_command` | 在「系统设置 → Linux 服务器」选中的服务器上远程执行 Shell 命令(依赖 `ssh2`) |
162
+ | `jenkins_build` | 触发「系统设置 → Jenkins」配置的 Job 构建,轮询等待结果并返回构建号/控制台链接 |
163
+ | `python_script` | 运行**任务内导入**的 Python 脚本(创建任务时上传 .py,源码随任务保存,每次执行写入临时文件运行、结束即清理;并注入系统设置里的 Jenkins/服务器配置,见下) |
164
+ | `show_reminder` | 任务提醒(管理台通知 + 系统原生通知;桌面级全屏闪烁/气泡已移除) |
165
+ | `open_app` | 打开程序 / 网址 / 文件路径 |
166
+ | `http_request` | 发起 HTTP 请求 |
167
+ | `volume_control` / `screenshot` / `send_keystroke` | 音量 / 截图 / 模拟按键 |
168
+ | `task_chain` / `clipboard_watch` | 触发其他任务 / 剪贴板监听 |
169
+ | `lock_screen` / `open_recycle_bin` / `refresh_desktop` / `clear_clipboard` | 系统操作 |
170
+
171
+ ## Python 脚本的配置注入
172
+
173
+ `python_script` 任务运行时,服务会把「系统设置」里的 Jenkins 与 Linux 服务器配置注入给脚本,
174
+ 脚本**不必再硬编码地址与密码**。脚本可读取:
175
+
176
+ | 环境变量 | 说明 |
177
+ |---|---|
178
+ | `DESKPET_CONFIG` | 注入配置文件(JSON)的绝对路径,含 `jenkins` / `servers` / `server` / `task` / `params` |
179
+ | `DESKPET_JENKINS_URL` / `_USER` / `_PASS` | Jenkins 地址与账号 |
180
+ | `DESKPET_SERVER_HOST` / `_PORT` / `_USER` / `_PASS` / `_NAME` | 任务里选中的那台服务器(未选则无) |
181
+ | `DESKPET_LOG_DIR` | 脚本日志目录(`~/.deskpet/logs`,可写文件日志) |
182
+ | `DESKPET_TASK_ID` / `DESKPET_TASK_NAME` / `DESKPET_PARAMS` | 当前任务信息与参数 JSON |
183
+
184
+ ## Python 解释器(重要)
185
+
186
+ 任务「解释器」**留空即自动探测**,解析顺序:Windows `python` → `python3` → `py`;类 Unix `python3` → `python`。
187
+ 解析结果会打印在执行日志里(`解释器: C:\Program Files\Python312\python.exe`),并写入 `GET /api/python/interpreters` 供表单下拉。
188
+
189
+ **为什么不能直接默认 `python3`**:Windows 上绝大多数机器**没有真正的 `python3`**,PATH 里唯一匹配的是
190
+ Microsoft Store 的「应用执行别名」存根
191
+ `%LOCALAPPDATA%\Microsoft\WindowsApps\python3.exe`(实际指向 `AppInstallerPythonRedirector.exe`)。
192
+ 该存根被非交互式拉起时不会执行任何脚本,**直接以退出码 9009 结束,且 stdout/stderr 全空**,现象为:
193
+
194
+ ```
195
+ 脚本异常退出 (exit_code=9009), stderr:
196
+ ```
197
+
198
+ 因此解析器会:① 自动**跳过 Store 别名存根**;② 跳过无法直接启动的 `.cmd`/`.bat`;
199
+ ③ 任务显式指定了解释器却解析不到时**直接报错**(不静默换用别的 Python,避免依赖不一致);
200
+ ④ 执行器对 `9009` / `-4058` 追加中文排查提示。
201
+
202
+ > 依赖第三方库(如 `requests`、`paramiko`)的脚本,请确认所选解释器已装这些库;
203
+ > 本机 `python` → `C:\Program Files\Python312\python.exe`(3.12,已装 requests/paramiko)。
204
+
205
+ ## 实时输出(运行中即可看日志)
206
+
207
+ 任务**运行中**就能在管理台看到脚本的 stdout/stderr,不必等结束:
208
+
209
+ - 任务列表 → 「日志」打开抽屉,顶部即「实时输出」控制台:自动滚动、可清屏、可「回到底部」,
210
+ stderr 以红色区分;结束后的完整记录仍在下方「执行历史」。
211
+ - 链路:`script-runner` 分块读子进程输出 → 调度器 `onOutput` 累积(每流保留最后 64KB)
212
+ → `emit('output')` → WebSocket 事件 **`task-output`** → 前端增量追加。
213
+ - 新接口:`GET /api/tasks/:id/output`(本次执行的输出快照,刷新页面/刚打开抽屉时补齐已有内容)。
214
+ - 适用动作:`python_script`、`ssh_command`(远端命令输出同样实时)。
215
+
216
+ **脚本侧要注意缓冲**:Python 的 stdout 被管道采集时默认是**块缓冲**,
217
+ `print()` 会滞留到缓冲区满或进程结束才输出。因此执行器会注入 **`PYTHONUNBUFFERED=1`**(等价 `python -u`);
218
+ 脚本自身也可加:
219
+
220
+ ```python
221
+ try:
222
+ sys.stdout.reconfigure(line_buffering=True)
223
+ sys.stderr.reconfigure(line_buffering=True)
224
+ except Exception:
225
+ pass
226
+ ```
227
+
228
+ > `logging` 的 `StreamHandler` 每条记录都会 flush,本身能实时输出;需要 flush 的主要是裸 `print()`。
229
+ > 示例脚本 `jenkins_k8s_deploy.py` 已加上这段。
230
+
231
+ **日志保留策略**:执行记录按**每个任务只保留最近 10 条**
232
+ (`EXECUTION_KEEP_PER_TASK`,定义在 `server/db/task-repository.ts`)。
233
+ 每次插入执行记录时自动裁剪最旧的,服务启动时也会对所有任务清理一次。
234
+
235
+ ## 周期任务(选择周几 + 时间)
236
+
237
+ 任务类型选「定时 → 周期(每周)」后,**不用手写 cron**:直接选**执行时间** + **周一~周日**(可多选),
238
+ 并提供「每天 / 工作日 / 周末」快捷按钮,实时显示中文说明(如 `每周一、三、五 17:55`、`每天 09:00`)。
239
+ 任务列表的「触发方式」列也显示这段中文。
240
+
241
+ 底层仍然存成标准 cron(`分 时 * * 周几`),调度器用 `node-schedule` 执行。
242
+
243
+ > **周几编号遵循标准 cron 语义**:`0=周日, 1=周一 … 6=周六`。
244
+ > 旧版 pet(`task_panel.py`)把「周一」写成 `0`,与标准相反会导致整体错位一天,此处**未沿用**。
245
+ > 若某个 cron 无法用「周几+时间」表达(含步进、指定了日/月等),表单会自动切换到
246
+ > 「手动编辑 cron 表达式」,不会把已有表达式改坏。
247
+
248
+ 相关纯逻辑见 `shared/cron-weekly.ts`(`parseWeeklyCron` / `buildWeeklyCron` / `formatWeeklyCron`)。
249
+
250
+ 示例(Python):
251
+
252
+ ```python
253
+ import os, json
254
+ cfg = json.load(open(os.environ['DESKPET_CONFIG'], encoding='utf-8'))
255
+ jenkins = cfg['jenkins'] # {url, username, password}
256
+ server = cfg['server'] or cfg['servers'][0] # {host, port, username, password}
257
+ ```
258
+
259
+ > 已按此约定重构示例脚本 `jenkins_k8s_deploy.py`(多服务器时可用 `--server <名称或id>` 指定)。
260
+ > 运行时也会自动设置 `PYTHONIOENCODING=utf-8`,避免 Windows 下中文日志乱码。
261
+
262
+ ### 脚本参数自动表单(TASK_PARAMS)
263
+
264
+ 脚本可在顶层声明 `TASK_PARAMS`,导入后任务表单会**自动生成对应控件**(下拉 / 多选 / 文本),
265
+ 无需手写命令行参数(对齐旧版 pet 的行为)。解析为纯字面量解析,**不会执行脚本**;未声明则回退到自由文本参数。
266
+
267
+ ```python
268
+ TASK_PARAMS = [
269
+ {"name": "--view", "label": "Jenkins 视图", "type": "text", "required": True, "placeholder": "如 Y26M11"},
270
+ {"name": "--env", "label": "部署环境", "type": "select", "required": True,
271
+ "options": [{"value": "uat", "label": "UAT"}, {"value": "sit", "label": "SIT"}]},
272
+ {"name": "--services", "label": "构建服务", "type": "multiselect",
273
+ "options": [{"value": "biz", "label": "biz - 业务系统"}]},
274
+ ]
275
+ ```
276
+
277
+ 字段:`name`(必填,命令行参数名) / `label` / `type`(text|select|multiselect) / `required` / `placeholder` / `help` / `options`。
278
+ 表单选择结果会按旧版规则拼成脚本参数,例如:`--view Y26M11 --env uat --services biz,vcs`(多选以逗号连接)。
279
+
280
+ **创建任务=设默认值;运行任务=可临时改参数**:任务里保存的即参数默认值;
281
+ 在任务列表点「▶ 运行」时会弹出参数窗(已按默认值填好),可**直接运行**,也可**临时修改本次参数**再运行
282
+ (`POST /api/tasks/:id/run` 支持 `{args}`:临时覆盖、执行完自动恢复,不会改动任务的默认值)。
283
+
284
+ ## 发布到 npm
285
+
286
+ > 面向维护者。发布成功后,使用者就能用上面「方式一」的一条 `npx` 命令启动。
287
+
288
+ ### 前置条件
289
+
290
+ 1. **npm 账号**。未登录时 `npm whoami` 会报 `ENEEDAUTH`,先执行 `npm adduser`(没有账号会引导注册)或 `npm login`。
291
+ 2. **包名**。`deskpet` 这个名字在 npm 上**已被他人占用**(maintainer 是 `deskpet.ai`),发不上去,
292
+ 必须用 scope 形式。本仓库已改为 `@wanghaopeng1148/deskpet`(scope 名即 npm 用户名);
293
+ 换账号时只需改 `package.json` 的 `name` 字段。
294
+ 3. **2FA**。账号开启了两步验证时,发布还会被要求提供一次性验证码(见下方 `--otp` 说明)。
295
+
296
+ ### 发布步骤(一键)
297
+
298
+ ```bash
299
+ npm run release # 一键:校验 → 类型检查 → 测试 → 构建 → 打包核对 → 发布
300
+ npm run release -- --dry-run # 只跑到打包预演,不真正发布(首次发版建议先跑一遍)
301
+ npm run release -- --bump patch # 同上,并先把版本号升一档(patch/minor/major)
302
+ ```
303
+
304
+ 脚本 `scripts/publish.mjs` 会依次完成下面这些事,任何一步不过就直接终止,不会带着问题发出去:
305
+
306
+ | 步骤 | 内容 |
307
+ | --- | --- |
308
+ | ① 校验 `package.json` | 包名、版本、有没有 `private: true`(有就拒绝) |
309
+ | ② 校验登录状态 | `npm whoami`,并核对包名 scope 与当前账号是否一致 |
310
+ | ③ 校验版本 | 该版本是否已经发过(避免 409 覆盖失败) |
311
+ | ④ 质量门禁 | `npm run typecheck` + `npm test`,不通过就不发 |
312
+ | ⑤ 构建 | `npm run build` —— 发布包靠它带上最新的 `dist/web` |
313
+ | ⑥ 打包核对 | `npm pack --dry-run`,并确认 `bin` / `server` / `shared` / `dist/web` 都真的在包里 |
314
+ | ⑦ 发布 | 确认后执行 `npm publish --access public`;开了 2FA 会提示输入验证码 |
315
+
316
+ 其它参数:`--otp 123456`(直接带验证码)、`--yes`(跳过确认)、`--skip-checks`(跳过类型检查与测试)、`--skip-build`(跳过构建)。
317
+
318
+ <details>
319
+ <summary>手动发布(不想用脚本时)</summary>
320
+
321
+ ```bash
322
+ npm login
323
+ npm run build # ⚠️ 必须先构建:发布包要靠它带上最新的 dist/web
324
+ npm pack --dry-run # 先看会发出去哪些文件(正常应为 70+ 文件、3~4 MB)
325
+ npm version patch # 升版本号
326
+ npm publish --otp=123456 # ⚠️ 换成认证器里的 6 位码;access=public 已写在 publishConfig 里
327
+ ```
328
+
329
+ </details>
330
+
331
+ > **关于 403 / 2FA(必读)**:npm 现行政策是**发布任何包都要求 2FA**,或使用带 bypass-2FA 的
332
+ > granular access token —— 换句话说**账号必须启用 2FA**,否则 `npm publish` 一定被拒,
333
+ > 报错是 `403 ... Two-factor authentication or granular access token with bypass 2fa enabled is required`。
334
+ > 两种处理方式:
335
+ > - **开启 2FA(推荐)**:npm → Settings → Two-Factor Authentication,用认证器 App 扫码绑定
336
+ > (Microsoft / Google Authenticator、1Password、Bitwarden 等均可)。之后发布时带上认证器里的
337
+ > 6 位码:`npm publish --otp=<6 位验证码>`(30 秒一换,过期重取)。**务必保存恢复码**。
338
+ > - **或改用带 bypass 2FA 的 token**:npm → Access Tokens → Generate New Token → Granular,
339
+ > 勾选 *Allow this token to bypass two-factor authentication*,写进 `.npmrc`,之后发布不再需要验证码
340
+ > (`.npmrc` 已加入 `.gitignore`,**切勿提交**)。
341
+ >
342
+ > 另一种 403 的成因:当报错 URL 是 `PUT .../<包名>` 时,也可能是**包名不属于你** ——
343
+ > npm 对无权发布的包一律回 403(不区分「已存在」与「无权限」),并且**复用同一段 2FA 文案**。
344
+ > 别被文案带偏,先确认包名是自己的 scope(`npm view <包名> maintainers`)。
345
+
346
+ 发布后验证:
347
+
348
+ ```bash
349
+ npx @wanghaopeng1148/deskpet@latest --check # 不启动,只做环境自检
350
+ npx @wanghaopeng1148/deskpet # 真正启动
351
+ ```
352
+
353
+ ### 发布包里有什么
354
+
355
+ 由 `package.json` 的 `files` 字段决定:
356
+
357
+ ```
358
+ bin/ 命令行入口 —— npx 执行的就是它
359
+ server/ 后端全部源码(以 .ts 直接运行,无需编译)
360
+ shared/ 前后端共享的纯逻辑
361
+ dist/web/ 前端构建产物 ← 关键:带上它,使用者才不必自己构建
362
+ resources/ 皮肤、图标等随包资源
363
+ ```
364
+
365
+ 不会发出去:`src/`(前端源码)、`scripts/`、`tests/`、`node_modules/`、`dist/web_prev_*`。
366
+
367
+ ### 依赖的三类归属(别搞混)
368
+
369
+ | 类别 | 放什么 | 为什么 |
370
+ | --- | --- | --- |
371
+ | `dependencies` | `express`、`node-schedule`、`ssh2`、`ws` | 后端运行时真正 `import` 的包,使用者必须装 |
372
+ | `devDependencies` | `vue`、`element-plus`、`echarts`、`vite`、`typescript` 等 | 纯前端构建用;它们已被 vite 打进 `dist/web`,若留在 `dependencies` 会让使用者白下几十 MB |
373
+ | `optionalDependencies` | `serialport` | **原生模块**,别人机器上可能编译失败;放这里 npm 会跳过它继续装,而不是中断整个安装(代码侧已做懒加载降级) |
374
+
375
+ ### 发版检查清单
376
+
377
+ `npm run release` 已经自动覆盖了大部分(构建、测试、打包核对、`private` 检查、关键文件校验),
378
+ **剩下这几件需要人工把关**:
379
+
380
+ - [ ] 版本号已升(用 `--bump patch` 或手动改 `version`)—— 不升的话 npm 会拒绝重复发布
381
+ - [ ] 依赖声明的改动已用 `npm install --package-lock-only` 同步到 `package-lock.json`
382
+ - [ ] 发布后 `git commit` + `git tag v<版本>`,别只提交代码不打标签
383
+ - [ ] 改了 scope / 包名时,README 里出现的包名一并更新
384
+
385
+ ## 说明
386
+
387
+ - 本机动作(脚本、打开应用、模拟按键、音量、截图、串口扫码枪)作用于**运行服务的这台机器**;
388
+ `ssh_command` / `jenkins_build` 则作用于远程 Linux 服务器与 Jenkins。
389
+ - 服务器与 Jenkins 凭据保存在本机 `~/.deskpet/config/settings.json`(**明文**,个人本机工具,勿在共享环境使用)。
390
+ - 任务串行执行:同一时刻只运行 1 个任务,其余按**先请求先执行**排队。任务管理页顶部有实时「执行队列」
391
+ (执行中 / 排队 N),列表里排队任务显示 `#序号`,点「取消排队」可撤销;仪表盘横幅也会显示执行中与排队数。
392
+ 前端通过 `GET /api/queue` 拉取,并用 WebSocket `queue-changed` 实时更新。
393
+ - 通知通道:系统原生通知(osascript / PowerShell Toast / notify-send)+ 管理台页面内提示 + 微信。
394
+ - 开机自启:Windows 写入 `HKCU\...\Run`,macOS 写入 `~/Library/LaunchAgents`,Linux 暂不支持。
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * DeskPet 命令行入口 —— 使用者的唯一入口:`npx deskpet`
4
+ *
5
+ * 与 scripts/start.mjs 的分工:
6
+ * · scripts/start.mjs 面向**源码开发者**:缺依赖就装、产物过期就构建,然后启动
7
+ * · 本文件 面向**包使用者**:前端产物随包发布(见 package.json 的 files),
8
+ * 所以正常情况下什么都不用准备 —— 起服务、打开浏览器即可
9
+ * 两者都只在产物确实缺失时,才回退去构建一次。
10
+ *
11
+ * 用法:
12
+ * npx deskpet 启动,并自动打开浏览器
13
+ * npx deskpet --no-open 只启动、不开浏览器(SSH 远程 / 服务器场景)
14
+ * npx deskpet --port 3211 指定端口(等价于设置 DESKPET_PORT)
15
+ * npx deskpet --check 只做环境自检,不启动服务
16
+ */
17
+ import { spawn, spawnSync } from 'node:child_process'
18
+ import { existsSync, readFileSync } from 'node:fs'
19
+ import net from 'node:net'
20
+ import { homedir } from 'node:os'
21
+ import { dirname, join } from 'node:path'
22
+ import { fileURLToPath } from 'node:url'
23
+
24
+ const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)))
25
+ const isWin = process.platform === 'win32'
26
+ /** 前端入口产物(vite 的 root 是 src、入口是 src/dashboard/index.html,故多一层 dashboard) */
27
+ const distEntry = join(pkgRoot, 'dist', 'web', 'dashboard', 'index.html')
28
+
29
+ const argv = process.argv.slice(2)
30
+ const hasFlag = (name) => argv.includes(name)
31
+ const flagValue = (name) => {
32
+ const i = argv.indexOf(name)
33
+ return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('-') ? argv[i + 1] : null
34
+ }
35
+
36
+ const cliPort = flagValue('--port') ?? process.env.DESKPET_PORT ?? null
37
+ const checkOnly = hasFlag('--check')
38
+ const shouldOpen = !hasFlag('--no-open')
39
+
40
+ function log(msg) {
41
+ process.stdout.write(`[deskpet] ${msg}\n`)
42
+ }
43
+
44
+ /** 实际监听端口:命令行 > 环境变量 > 配置文件 > 默认 3210 */
45
+ function resolvePort() {
46
+ if (cliPort) {
47
+ const n = Number(cliPort)
48
+ if (Number.isInteger(n) && n > 0) return n
49
+ console.error(`[deskpet] 端口无效:${cliPort}`)
50
+ process.exit(1)
51
+ }
52
+ const home = process.env.DESKPET_HOME?.trim() || join(homedir(), '.deskpet')
53
+ try {
54
+ const s = JSON.parse(readFileSync(join(home, 'config', 'settings.json'), 'utf-8'))
55
+ if (Number.isInteger(s?.server?.port) && s.server.port > 0) return s.server.port
56
+ } catch {
57
+ /* 首次运行还没有配置文件,落到默认端口 */
58
+ }
59
+ return 3210
60
+ }
61
+
62
+ /** 轮询等待端口就绪 */
63
+ function waitPort(port, host = '127.0.0.1', timeoutMs = 30000) {
64
+ const start = Date.now()
65
+ return new Promise((resolve, reject) => {
66
+ const tryOnce = () => {
67
+ const sock = net.connect(port, host)
68
+ sock.once('connect', () => {
69
+ sock.destroy()
70
+ resolve()
71
+ })
72
+ sock.once('error', () => {
73
+ sock.destroy()
74
+ if (Date.now() - start > timeoutMs) {
75
+ reject(new Error(`等待端口 ${port} 超时`))
76
+ return
77
+ }
78
+ setTimeout(tryOnce, 300)
79
+ })
80
+ }
81
+ tryOnce()
82
+ })
83
+ }
84
+
85
+ function openUrl(url) {
86
+ try {
87
+ if (isWin) {
88
+ spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref()
89
+ } else if (process.platform === 'darwin') {
90
+ spawn('open', [url], { stdio: 'ignore', detached: true }).unref()
91
+ } else {
92
+ spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref()
93
+ }
94
+ } catch {
95
+ /* 打不开就算了:URL 已经打印出来,用户可自行复制 */
96
+ }
97
+ }
98
+
99
+ // ── 1. 前端产物(包使用者通常无需关心,这里只是兜底) ─────────
100
+ if (!existsSync(distEntry)) {
101
+ const viteBin = join(pkgRoot, 'node_modules', 'vite', 'bin', 'vite.js')
102
+ if (!existsSync(viteBin)) {
103
+ console.error('[deskpet] 找不到前端产物 dist/web,也找不到 vite 依赖。')
104
+ console.error(' 若你是从源码运行,请先执行:npm install && npm run build')
105
+ process.exit(1)
106
+ }
107
+ log('前端产物缺失,正在构建(源码方式运行时才需要)…')
108
+ const r = spawnSync(process.execPath, [viteBin, 'build'], { cwd: pkgRoot, stdio: 'inherit' })
109
+ if (r.status !== 0) process.exit(r.status ?? 1)
110
+ }
111
+
112
+ const port = resolvePort()
113
+ const url = `http://127.0.0.1:${port}/dashboard/`
114
+
115
+ if (checkOnly) {
116
+ log(`自检通过:产物就绪、端口 ${port}、入口 ${url}`)
117
+ process.exit(0)
118
+ }
119
+
120
+ // ── 2. 启动后端 ───────────────────────────────────────────
121
+ log(`启动服务(端口 ${port})…`)
122
+ const child = spawn(process.execPath, ['--experimental-strip-types', join(pkgRoot, 'server', 'main.ts')], {
123
+ cwd: pkgRoot,
124
+ stdio: 'inherit',
125
+ env: { ...process.env, DESKPET_PORT: String(port) }
126
+ })
127
+
128
+ process.on('SIGINT', () => child.kill('SIGINT'))
129
+ process.on('SIGTERM', () => child.kill('SIGTERM'))
130
+ child.on('exit', (code) => process.exit(code ?? 0))
131
+
132
+ // ── 3. 就绪后打开浏览器(失败不影响服务) ──────────────────
133
+ if (shouldOpen) {
134
+ waitPort(port)
135
+ .then(() => {
136
+ log(`已就绪:${url}`)
137
+ openUrl(url)
138
+ })
139
+ .catch(() => {
140
+ /* 端口没等到就交给子进程自己的日志,不再打扰用户 */
141
+ })
142
+ }
@@ -0,0 +1 @@
1
+ .error-bar[data-v-3056d734]{background:#fb71851f;border:1px solid rgba(251,113,133,.4);color:var(--dp-danger);border-radius:10px;padding:10px 16px;margin-bottom:16px}.hero[data-v-3056d734]{display:flex;align-items:center;justify-content:space-between;padding:22px 26px;background:radial-gradient(500px 200px at 90% 0%,rgba(62,214,255,.1),transparent 60%),linear-gradient(180deg,var(--dp-panel-2),var(--dp-panel))}.hero-left[data-v-3056d734]{display:flex;align-items:center;gap:20px}.hero-avatar[data-v-3056d734]{width:88px;height:88px;border-radius:26px;background:linear-gradient(160deg,#ffd9ad29,#ffb3d01a);border:1px solid var(--dp-line-strong);display:flex;align-items:center;justify-content:center;animation:float-y-3056d734 3.4s ease-in-out infinite}@keyframes float-y-3056d734{0%,to{transform:translateY(0)}50%{transform:translateY(-5px)}}.hero-title[data-v-3056d734]{font-size:21px;font-weight:800}.hero-sub[data-v-3056d734]{color:var(--dp-text-2);margin-top:7px;font-size:13.5px}.hero-sub b[data-v-3056d734]{color:var(--dp-success)}.hero-queue[data-v-3056d734]{display:flex;align-items:center;gap:8px;margin-top:9px}.hero-queue span[data-v-3056d734]{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid transparent;white-space:nowrap}.hq-run[data-v-3056d734]{color:#8fa8ff;background:#6c8cff24;border-color:#6c8cff66}.hq-wait[data-v-3056d734]{color:#fbbf24;background:#fbbf241a;border-color:#fbbf244d}.pad[data-v-3056d734]{padding:14px 18px;border-bottom:1px solid var(--dp-line)}.kpi[data-v-3056d734]{display:flex;align-items:center;gap:14px;padding:18px}.kpi-icon[data-v-3056d734]{width:42px;height:42px;border-radius:12px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tone-blue[data-v-3056d734]{background:#6c8cff29;color:#8fa8ff}.tone-cyan[data-v-3056d734]{background:#3ed6ff24;color:#56dcff}.tone-green[data-v-3056d734]{background:#34d39924;color:#43e0aa}.tone-violet[data-v-3056d734]{background:#9a6cff29;color:#b18aff}.kpi-label[data-v-3056d734]{color:var(--dp-text-3);font-size:12.5px}.kpi-value[data-v-3056d734]{font-size:24px;font-weight:800;margin-top:3px}.kpi-sub[data-v-3056d734]{font-size:12.5px;font-weight:600;color:var(--dp-text-2)}.chart-card[data-v-3056d734]{overflow:hidden}.chart[data-v-3056d734]{height:272px;width:100%;padding:10px 12px 6px}.fail-card[data-v-3056d734]{min-height:330px}.fail-item[data-v-3056d734]{padding:12px 18px;border-bottom:1px dashed var(--dp-line)}.fail-item[data-v-3056d734]:last-child{border-bottom:none}.fail-top[data-v-3056d734]{display:flex;justify-content:space-between;align-items:center}.fail-name[data-v-3056d734]{font-weight:600}.fail-count[data-v-3056d734]{color:var(--dp-danger);font-size:12px;font-weight:700}.fail-bar[data-v-3056d734]{height:5px;border-radius:4px;background:#94aad41a;margin-top:8px;overflow:hidden}.fail-bar-inner[data-v-3056d734]{height:100%;border-radius:4px;background:linear-gradient(90deg,#fb7185,#fb923c)}.fail-err[data-v-3056d734]{color:var(--dp-text-3);font-size:12px;margin-top:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.status-pill[data-v-3056d734]{display:inline-flex;align-items:center;gap:6px;font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid transparent}.status-dot[data-v-3056d734]{width:6px;height:6px;border-radius:50%;background:currentColor}.st-completed[data-v-3056d734]{color:#43e0aa;background:#34d3991a;border-color:#34d3994d}.st-failed[data-v-3056d734]{color:#fb7185;background:#fb71851a;border-color:#fb71854d}.st-timeout[data-v-3056d734]{color:#fbbf24;background:#fbbf241a;border-color:#fbbf244d}.st-running[data-v-3056d734]{color:#8fa8ff;background:#6c8cff1a;border-color:#6c8cff4d}.st-cancelled[data-v-3056d734]{color:var(--dp-text-3);background:#94aad414;border-color:var(--dp-line-strong)}