@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.
Files changed (54) hide show
  1. package/README.md +70 -16
  2. package/bin/deskpet.mjs +70 -2
  3. package/dist/node/server/db/database.js +113 -0
  4. package/dist/node/server/db/migrate-legacy.js +88 -0
  5. package/dist/node/server/db/task-repository.js +374 -0
  6. package/dist/node/server/http/http-server.js +493 -0
  7. package/dist/node/server/http/ws-hub.js +59 -0
  8. package/dist/node/server/main.js +291 -0
  9. package/dist/node/server/plugins/actions/builtin.js +67 -0
  10. package/dist/node/server/plugins/actions/clipboard-watch.js +27 -0
  11. package/dist/node/server/plugins/actions/http-request.js +41 -0
  12. package/dist/node/server/plugins/actions/jenkins-build.js +183 -0
  13. package/dist/node/server/plugins/actions/open-app.js +41 -0
  14. package/dist/node/server/plugins/actions/python-script.js +180 -0
  15. package/dist/node/server/plugins/actions/screenshot.js +38 -0
  16. package/dist/node/server/plugins/actions/send-keystroke.js +100 -0
  17. package/dist/node/server/plugins/actions/show-reminder.js +7 -0
  18. package/dist/node/server/plugins/actions/ssh-command.js +123 -0
  19. package/dist/node/server/plugins/actions/task-chain.js +24 -0
  20. package/dist/node/server/plugins/actions/volume-control.js +31 -0
  21. package/dist/node/server/plugins/index.js +35 -0
  22. package/dist/node/server/plugins/registry.js +23 -0
  23. package/dist/node/server/services/clipboard-watcher.js +112 -0
  24. package/dist/node/server/services/config-store.js +141 -0
  25. package/dist/node/server/services/idle-monitor.js +131 -0
  26. package/dist/node/server/services/notifier.js +36 -0
  27. package/dist/node/server/services/quick-actions-store.js +52 -0
  28. package/dist/node/server/services/remote-connector.js +67 -0
  29. package/dist/node/server/services/scanner-reader.js +217 -0
  30. package/dist/node/server/services/script-runner.js +228 -0
  31. package/dist/node/server/services/snapshot-service.js +135 -0
  32. package/dist/node/server/services/task-scheduler.js +813 -0
  33. package/dist/node/server/services/wechat-bot.js +635 -0
  34. package/dist/node/server/services/wechat-command-types.js +1 -0
  35. package/dist/node/server/services/wechat-commands.js +330 -0
  36. package/dist/node/server/suppress-warnings.js +12 -0
  37. package/dist/node/server/utils/asset-url.js +26 -0
  38. package/dist/node/server/utils/auto-start.js +186 -0
  39. package/dist/node/server/utils/clipboard.js +50 -0
  40. package/dist/node/server/utils/dashboard-url.js +8 -0
  41. package/dist/node/server/utils/instance-guard.js +165 -0
  42. package/dist/node/server/utils/native-notify.js +53 -0
  43. package/dist/node/server/utils/open.js +37 -0
  44. package/dist/node/server/utils/paths.js +95 -0
  45. package/dist/node/server/utils/python-interpreter.js +129 -0
  46. package/dist/node/shared/animation-engine.js +349 -0
  47. package/dist/node/shared/chain-condition.js +39 -0
  48. package/dist/node/shared/cron-weekly.js +124 -0
  49. package/dist/node/shared/py-task-params.js +335 -0
  50. package/dist/node/shared/types.js +69 -0
  51. package/package.json +6 -2
  52. package/server/http/http-server.ts +4 -1
  53. package/server/utils/auto-start.ts +158 -49
  54. package/server/utils/paths.ts +28 -1
@@ -0,0 +1,335 @@
1
+ /**
2
+ * 解析 Python 脚本中的 TASK_PARAMS 常量 → 任务表单动态参数控件
3
+ *
4
+ * 对齐旧版 pet/src/ui/task_panel.py 的 `_parse_task_params`:
5
+ * - 只做**字面量解析**(不执行脚本),仅支持 dict / list / str / number / bool / None
6
+ * - 找不到 TASK_PARAMS 或解析失败 → 返回 null(表单回退到自由文本参数)
7
+ *
8
+ * 脚本内声明示例:
9
+ * TASK_PARAMS = [
10
+ * {"name": "--env", "label": "环境", "type": "select",
11
+ * "options": [{"value": "uat", "label": "UAT"}]},
12
+ * {"name": "--services", "label": "服务", "type": "multiselect",
13
+ * "options": [{"value": "biz", "label": "biz"}]},
14
+ * {"name": "--view", "label": "视图", "type": "text", "placeholder": "如 Y26M11"},
15
+ * ]
16
+ */
17
+ /** 截取 `TASK_PARAMS = <字面量>` 的字面量源码(到配对括号结束) */
18
+ function extractTaskParamsSource(source) {
19
+ const m = /(^|\n)[ \t]*TASK_PARAMS\s*=\s*/.exec(source);
20
+ if (!m)
21
+ return null;
22
+ const start = m.index + m[0].length;
23
+ const end = scanBalanced(source, start);
24
+ return end === null ? null : source.slice(start, end);
25
+ }
26
+ /** 从 start 处的 `[` / `{` 起,返回配对括号的结束位置(不含) */
27
+ function scanBalanced(s, start) {
28
+ let i = start;
29
+ while (i < s.length && /\s/.test(s[i]))
30
+ i++;
31
+ const open = s[i];
32
+ if (open !== '[' && open !== '{')
33
+ return null;
34
+ const stack = [];
35
+ let inStr = null;
36
+ for (; i < s.length; i++) {
37
+ const ch = s[i];
38
+ if (inStr) {
39
+ if (ch === '\\') {
40
+ i++;
41
+ continue;
42
+ }
43
+ if (ch === inStr)
44
+ inStr = null;
45
+ continue;
46
+ }
47
+ if (ch === '#') {
48
+ while (i < s.length && s[i] !== '\n')
49
+ i++;
50
+ continue;
51
+ }
52
+ if (ch === '"' || ch === "'") {
53
+ inStr = ch;
54
+ continue;
55
+ }
56
+ if (ch === '[' || ch === '{')
57
+ stack.push(ch);
58
+ else if (ch === ']' || ch === '}') {
59
+ stack.pop();
60
+ if (stack.length === 0)
61
+ return i + 1;
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ /** Python 字面量递归下降解析器(受限子集) */
67
+ class PyLiteralParser {
68
+ i = 0;
69
+ src;
70
+ constructor(src) {
71
+ this.src = src;
72
+ }
73
+ parse() {
74
+ const v = this.value();
75
+ this.skipWs();
76
+ if (this.i < this.src.length) {
77
+ throw new Error(`存在多余内容: ${this.src.slice(this.i, this.i + 20)}`);
78
+ }
79
+ return v;
80
+ }
81
+ skipWs() {
82
+ for (;;) {
83
+ while (this.i < this.src.length && /\s/.test(this.src[this.i]))
84
+ this.i++;
85
+ if (this.src[this.i] === '#') {
86
+ while (this.i < this.src.length && this.src[this.i] !== '\n')
87
+ this.i++;
88
+ continue;
89
+ }
90
+ break;
91
+ }
92
+ }
93
+ value() {
94
+ this.skipWs();
95
+ const ch = this.src[this.i];
96
+ if (ch === '[')
97
+ return this.list();
98
+ if (ch === '{')
99
+ return this.dict();
100
+ if (ch === '"' || ch === "'")
101
+ return this.string();
102
+ if (ch && /[A-Za-z_]/.test(ch)) {
103
+ const word = this.ident();
104
+ // 字符串前缀 r'' / b'' / u'' / f''(f-string 不支持)
105
+ const nxt = this.src[this.i];
106
+ if (nxt === '"' || nxt === "'") {
107
+ if (word === 'f' || word === 'F')
108
+ throw new Error('不支持 f-string');
109
+ return this.string();
110
+ }
111
+ if (word === 'True')
112
+ return true;
113
+ if (word === 'False')
114
+ return false;
115
+ if (word === 'None')
116
+ return null;
117
+ throw new Error(`不支持的标识符: ${word}`);
118
+ }
119
+ return this.number();
120
+ }
121
+ list() {
122
+ const out = [];
123
+ this.i++;
124
+ this.skipWs();
125
+ if (this.src[this.i] === ']') {
126
+ this.i++;
127
+ return out;
128
+ }
129
+ for (;;) {
130
+ out.push(this.value());
131
+ this.skipWs();
132
+ const ch = this.src[this.i];
133
+ if (ch === ',') {
134
+ this.i++;
135
+ this.skipWs();
136
+ if (this.src[this.i] === ']') {
137
+ this.i++;
138
+ break;
139
+ }
140
+ continue;
141
+ }
142
+ if (ch === ']') {
143
+ this.i++;
144
+ break;
145
+ }
146
+ throw new Error('列表格式错误');
147
+ }
148
+ return out;
149
+ }
150
+ dict() {
151
+ const out = {};
152
+ this.i++;
153
+ this.skipWs();
154
+ if (this.src[this.i] === '}') {
155
+ this.i++;
156
+ return out;
157
+ }
158
+ for (;;) {
159
+ this.skipWs();
160
+ const key = this.value();
161
+ this.skipWs();
162
+ if (this.src[this.i] !== ':')
163
+ throw new Error('字典缺少冒号');
164
+ this.i++;
165
+ out[String(key)] = this.value();
166
+ this.skipWs();
167
+ const ch = this.src[this.i];
168
+ if (ch === ',') {
169
+ this.i++;
170
+ this.skipWs();
171
+ if (this.src[this.i] === '}') {
172
+ this.i++;
173
+ break;
174
+ }
175
+ continue;
176
+ }
177
+ if (ch === '}') {
178
+ this.i++;
179
+ break;
180
+ }
181
+ throw new Error('字典格式错误');
182
+ }
183
+ return out;
184
+ }
185
+ string() {
186
+ const quote = this.src[this.i];
187
+ this.i++;
188
+ let out = '';
189
+ while (this.i < this.src.length) {
190
+ const ch = this.src[this.i];
191
+ if (ch === '\\') {
192
+ const next = this.src[this.i + 1];
193
+ this.i += 2;
194
+ switch (next) {
195
+ case 'n':
196
+ out += '\n';
197
+ break;
198
+ case 't':
199
+ out += '\t';
200
+ break;
201
+ case 'r':
202
+ out += '\r';
203
+ break;
204
+ case '\\':
205
+ out += '\\';
206
+ break;
207
+ case "'":
208
+ out += "'";
209
+ break;
210
+ case '"':
211
+ out += '"';
212
+ break;
213
+ case 'u': {
214
+ const hex = this.src.slice(this.i, this.i + 4);
215
+ if (/^[0-9a-fA-F]{4}$/.test(hex)) {
216
+ out += String.fromCharCode(parseInt(hex, 16));
217
+ this.i += 4;
218
+ }
219
+ break;
220
+ }
221
+ default:
222
+ out += next ?? '';
223
+ }
224
+ continue;
225
+ }
226
+ if (ch === quote) {
227
+ this.i++;
228
+ return out;
229
+ }
230
+ out += ch;
231
+ this.i++;
232
+ }
233
+ throw new Error('字符串未闭合');
234
+ }
235
+ ident() {
236
+ const m = /^[A-Za-z_][A-Za-z0-9_]*/.exec(this.src.slice(this.i));
237
+ if (!m)
238
+ throw new Error('标识符格式错误');
239
+ this.i += m[0].length;
240
+ return m[0];
241
+ }
242
+ number() {
243
+ const m = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?/.exec(this.src.slice(this.i));
244
+ if (!m || !m[0])
245
+ throw new Error(`无法解析的值: ${this.src.slice(this.i, this.i + 12)}`);
246
+ this.i += m[0].length;
247
+ return Number(m[0]);
248
+ }
249
+ }
250
+ /** 解析脚本源码中的 TASK_PARAMS;无该常量或解析失败返回 null */
251
+ export function parseTaskParams(source) {
252
+ if (!source)
253
+ return null;
254
+ try {
255
+ const literal = extractTaskParamsSource(source);
256
+ if (!literal)
257
+ return null;
258
+ const parsed = new PyLiteralParser(literal).parse();
259
+ if (!Array.isArray(parsed))
260
+ return null;
261
+ const out = [];
262
+ for (const raw of parsed) {
263
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
264
+ continue;
265
+ const r = raw;
266
+ const name = String(r['name'] ?? '').trim();
267
+ if (!name)
268
+ continue;
269
+ const rawType = String(r['type'] ?? 'text');
270
+ const type = rawType === 'select' || rawType === 'multiselect' ? rawType : 'text';
271
+ const options = Array.isArray(r['options'])
272
+ ? r['options']
273
+ .map((o) => {
274
+ const oo = (o ?? {});
275
+ return {
276
+ value: String(oo['value'] ?? ''),
277
+ label: oo['label'] != null ? String(oo['label']) : undefined
278
+ };
279
+ })
280
+ .filter((o) => o.value !== '')
281
+ : undefined;
282
+ out.push({
283
+ name,
284
+ label: r['label'] != null ? String(r['label']) : undefined,
285
+ type,
286
+ required: r['required'] === true,
287
+ placeholder: r['placeholder'] != null ? String(r['placeholder']) : undefined,
288
+ help: r['help'] != null ? String(r['help']) : undefined,
289
+ options
290
+ });
291
+ }
292
+ return out.length ? out : null;
293
+ }
294
+ catch {
295
+ return null;
296
+ }
297
+ }
298
+ /** 从已有脚本参数回填动态表单(对齐旧版 _restore_params_from_args) */
299
+ export function restoreParamState(params, args) {
300
+ const values = {};
301
+ const consumed = new Set();
302
+ for (const p of params) {
303
+ const idx = args.indexOf(p.name);
304
+ if (idx === -1 || idx + 1 >= args.length)
305
+ continue;
306
+ const next = args[idx + 1];
307
+ if (next.startsWith('--'))
308
+ continue;
309
+ consumed.add(idx);
310
+ consumed.add(idx + 1);
311
+ values[p.name] =
312
+ p.type === 'multiselect' ? next.split(',').map((s) => s.trim()).filter(Boolean) : next;
313
+ }
314
+ const extra = args.filter((_, i) => !consumed.has(i));
315
+ return { values, extra };
316
+ }
317
+ /** 由动态表单值生成脚本参数(对齐旧版 _collect_params_as_args) */
318
+ export function buildArgsFromParams(params, values, extra = []) {
319
+ const args = [];
320
+ for (const p of params) {
321
+ const v = values[p.name];
322
+ if (p.type === 'multiselect') {
323
+ const arr = Array.isArray(v) ? v : [];
324
+ if (arr.length)
325
+ args.push(p.name, arr.join(','));
326
+ }
327
+ else {
328
+ const s = typeof v === 'string' ? v.trim() : '';
329
+ if (s)
330
+ args.push(p.name, s);
331
+ }
332
+ }
333
+ args.push(...extra);
334
+ return args;
335
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * 共享类型定义 — 主进程 / 渲染进程 / 预加载共用
3
+ */
4
+ /**
5
+ * 宠物状态(值越大优先级越高,与旧版 Python 一致)
6
+ * 使用 const 对象 + 联合类型(兼容 Node 原生 TS strip-only 模式)
7
+ */
8
+ export const PetState = {
9
+ SLEEP: 0,
10
+ IDLE: 1,
11
+ HOVER: 2,
12
+ BUSY: 3,
13
+ CLICK: 4
14
+ };
15
+ export const PET_STATE_NAMES = [
16
+ 'sleep',
17
+ 'idle',
18
+ 'hover',
19
+ 'click',
20
+ 'busy'
21
+ ];
22
+ export function petStateName(state) {
23
+ switch (state) {
24
+ case PetState.SLEEP:
25
+ return 'sleep';
26
+ case PetState.IDLE:
27
+ return 'idle';
28
+ case PetState.HOVER:
29
+ return 'hover';
30
+ case PetState.BUSY:
31
+ return 'busy';
32
+ case PetState.CLICK:
33
+ return 'click';
34
+ default:
35
+ return 'idle';
36
+ }
37
+ }
38
+ /** 点击互动动画类型(轮流触发) */
39
+ export const ClickAnimationType = {
40
+ JUMP: 'jump',
41
+ SQUASH: 'squash',
42
+ SHAKE: 'shake'
43
+ };
44
+ // ── 任务系统 (M2) ─────────────────────────────────────────────
45
+ export const TaskType = {
46
+ MANUAL: 'manual', // 手动
47
+ SCHEDULED: 'scheduled', // 定时
48
+ SCENE: 'scene' // 场景触发
49
+ };
50
+ export const TaskStatus = {
51
+ IDLE: 'idle',
52
+ PENDING: 'pending',
53
+ RUNNING: 'running',
54
+ COMPLETED: 'completed',
55
+ FAILED: 'failed',
56
+ CANCELLED: 'cancelled',
57
+ DISABLED: 'disabled'
58
+ };
59
+ export const ScheduleType = {
60
+ ONCE: 'once',
61
+ CRON: 'cron',
62
+ DELAY: 'delay',
63
+ INTERVAL: 'interval'
64
+ };
65
+ export const SceneTrigger = {
66
+ IDLE: 'idle',
67
+ STARTUP: 'startup',
68
+ NETWORK: 'network'
69
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wanghaopeng1148/deskpet",
3
3
  "productName": "DeskPet",
4
- "version": "2.0.0",
4
+ "version": "2.0.2",
5
5
  "description": "DeskPet 2.0 — 本机任务自动化服务 + Web 管理台 (Node + Express + Vue 3 + TypeScript)",
6
6
  "type": "module",
7
7
  "author": "whp",
@@ -16,6 +16,7 @@
16
16
  "bin",
17
17
  "server",
18
18
  "shared",
19
+ "dist/node",
19
20
  "dist/web",
20
21
  "resources",
21
22
  "README.md"
@@ -28,8 +29,11 @@
28
29
  "dev:watch": "node scripts/dev-all.mjs --watch",
29
30
  "dev:server": "node --experimental-strip-types --watch server/main.ts",
30
31
  "dev:web": "vite",
31
- "build": "vite build",
32
+ "build": "npm run build:web && npm run build:server",
32
33
  "build:web": "vite build",
34
+ "build:server": "tsc -p tsconfig.build.json",
35
+ "prepublishOnly": "npm run build:server",
36
+ "verify:package": "node scripts/verify-package.mjs",
33
37
  "start": "node scripts/start.mjs",
34
38
  "serve": "node --experimental-strip-types server/main.ts",
35
39
  "release": "node scripts/publish.mjs",
@@ -26,7 +26,7 @@ import { testJenkinsConnection, testSshConnection } from '../services/remote-con
26
26
  import type { ClipboardWatchRegistry } from '../services/clipboard-watcher.ts'
27
27
  import { resolveAssetRequest } from '../utils/asset-url.ts'
28
28
  import { detectPythonInterpreters } from '../utils/python-interpreter.ts'
29
- import { moduleDir } from '../utils/paths.ts'
29
+ import { getPackageRoot, moduleDir } from '../utils/paths.ts'
30
30
  import type { WsHub } from './ws-hub.ts'
31
31
 
32
32
  export interface HttpServerDeps {
@@ -714,6 +714,9 @@ function clampInt(v: unknown, min: number, max: number): number {
714
714
  /** 静态资源根目录:优先 vite 构建产物 dist/web,兼容旧路径 out/renderer */
715
715
  function resolveStaticRoot(): string | null {
716
716
  const candidates = [
717
+ // 用包根定位,源码运行与编译产物(dist/node/...)都适用;
718
+ // moduleDir() 的相对层级只在源码形态下成立,不能单独依赖
719
+ join(getPackageRoot(), 'dist/web'),
717
720
  join(moduleDir(), '../../dist/web'),
718
721
  join(moduleDir(), '../renderer'),
719
722
  join(process.cwd(), 'dist/web')