@wanghaopeng1148/deskpet 2.0.1 → 2.0.3

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 (55) hide show
  1. package/README.md +54 -15
  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 +295 -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 +193 -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/main.ts +5 -1
  54. package/server/utils/auto-start.ts +29 -8
  55. 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.1",
4
+ "version": "2.0.3",
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')
package/server/main.ts CHANGED
@@ -239,12 +239,16 @@ async function main(): Promise<void> {
239
239
  scheduler.start({ recover: instanceLock.acquired })
240
240
 
241
241
  // 开机自启:启动时应用 + 跟随设置变化
242
+ // removeWhenOff: false —— 启动时的同步只「确保开启时文件存在」,不删除。
243
+ // 否则一个用默认配置(autoStart=false)起来的实例(例如临时跑一次 npx,
244
+ // 或换了 DESKPET_HOME)会把主实例写在同一个启动文件夹里的自启文件删掉。
242
245
  let lastAutoStart = config.get().system.autoStart
243
- void setAutoStart(lastAutoStart)
246
+ void setAutoStart(lastAutoStart, { removeWhenOff: false })
244
247
  config.on('changed', () => {
245
248
  const next = config.get().system.autoStart
246
249
  if (next !== lastAutoStart) {
247
250
  lastAutoStart = next
251
+ // 用户在设置页显式切换 → 关闭时应当真的移除文件
248
252
  void setAutoStart(next)
249
253
  }
250
254
  })
@@ -22,14 +22,21 @@
22
22
  */
23
23
  import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs'
24
24
  import { homedir } from 'node:os'
25
- import { dirname, join, resolve } from 'node:path'
26
- import { fileURLToPath } from 'node:url'
25
+ import { dirname, join } from 'node:path'
26
+ import { getPackageRoot } from './paths.ts'
27
27
 
28
28
  const APP_ID = 'DeskPet'
29
29
 
30
- /** 应用根目录(源码运行为项目根,npm 包运行为包根) */
30
+ /**
31
+ * 应用根目录。
32
+ *
33
+ * 用 getPackageRoot() 向上查找 package.json,而不是写死 `../..`:
34
+ * 源码运行时本模块在 `server/utils/`,编译产物在 `dist/node/server/utils/`,
35
+ * 距包根的层数不同 —— 写死层级会让**从 npm 安装版开启自启**生成一个指向
36
+ * 不存在路径的启动脚本(开机静默失败)。
37
+ */
31
38
  function appRoot(): string {
32
- return resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
39
+ return getPackageRoot()
33
40
  }
34
41
 
35
42
  /** 开机要执行的 node 可执行文件与入口脚本 */
@@ -123,8 +130,22 @@ function linuxDesktopContent(): string {
123
130
 
124
131
  // ── 对外接口 ────────────────────────────────────────────────
125
132
 
133
+ export interface AutoStartOptions {
134
+ /**
135
+ * 配置为「关闭」时是否删除自启文件(默认 true)。
136
+ *
137
+ * 启动时的同步必须传 false:多个实例可能共用同一个启动文件夹,
138
+ * 一个以默认配置(autoStart=false)起来的实例不该把别人的自启文件删掉。
139
+ * 这个坑很隐蔽 —— 临时跑一次 `npx deskpet`(数据目录是全新的默认配置)
140
+ * 就会把主实例的开机自启悄悄弄没,之后每次开机都得手动启动。
141
+ * 关闭自启只应由「设置页里显式关掉开关」这个动作来执行。
142
+ */
143
+ removeWhenOff?: boolean
144
+ }
145
+
126
146
  /** 开启 / 关闭开机自启,返回是否成功(失败原因会打印到日志) */
127
- export async function setAutoStart(on: boolean): Promise<boolean> {
147
+ export async function setAutoStart(on: boolean, opts: AutoStartOptions = {}): Promise<boolean> {
148
+ const removeWhenOff = opts.removeWhenOff ?? true
128
149
  try {
129
150
  if (process.platform === 'win32') {
130
151
  const dir = winStartupDir()
@@ -145,7 +166,7 @@ export async function setAutoStart(on: boolean): Promise<boolean> {
145
166
  if (on) {
146
167
  writeFileSync(file, winCmdContent(), 'utf-8')
147
168
  console.log(`[autostart] 开机自启已开启: ${file}`)
148
- } else if (existsSync(file)) {
169
+ } else if (removeWhenOff && existsSync(file)) {
149
170
  unlinkSync(file)
150
171
  console.log('[autostart] 开机自启已关闭')
151
172
  }
@@ -158,7 +179,7 @@ export async function setAutoStart(on: boolean): Promise<boolean> {
158
179
  mkdirSync(dirname(file), { recursive: true })
159
180
  writeFileSync(file, macPlistContent(), 'utf-8')
160
181
  console.log(`[autostart] 开机自启已开启: ${file}`)
161
- } else if (existsSync(file)) {
182
+ } else if (removeWhenOff && existsSync(file)) {
162
183
  unlinkSync(file)
163
184
  console.log('[autostart] 开机自启已关闭')
164
185
  }
@@ -171,7 +192,7 @@ export async function setAutoStart(on: boolean): Promise<boolean> {
171
192
  mkdirSync(dirname(file), { recursive: true })
172
193
  writeFileSync(file, linuxDesktopContent(), 'utf-8')
173
194
  console.log(`[autostart] 开机自启已开启: ${file}`)
174
- } else if (existsSync(file)) {
195
+ } else if (removeWhenOff && existsSync(file)) {
175
196
  unlinkSync(file)
176
197
  console.log('[autostart] 开机自启已关闭')
177
198
  }
@@ -23,6 +23,33 @@ export function moduleDir(): string {
23
23
 
24
24
  const MODULE_DIR = moduleDir()
25
25
 
26
+ /**
27
+ * 包根目录(含 package.json 的那一层)。
28
+ *
29
+ * 为什么不写死相对层级:源码运行时本模块在 `server/utils/`,
30
+ * 而发布产物在 `dist/node/server/utils/` —— 两者距包根的层数不同,
31
+ * 任何 `../../xxx` 形式的假设都会在编译后失效(曾导致 resources 与 dist/web 定位不到)。
32
+ * 这里改为从模块目录向上查找 package.json,源码形态与产物形态都能正确定位。
33
+ */
34
+ function findPackageRoot(): string {
35
+ let dir = MODULE_DIR
36
+ for (let i = 0; i < 8; i += 1) {
37
+ if (existsSync(join(dir, 'package.json'))) return dir
38
+ const parent = dirname(dir)
39
+ if (parent === dir) break
40
+ dir = parent
41
+ }
42
+ // 兜底:沿用源码形态的假设(server/utils → 上两级即包根)
43
+ return resolve(MODULE_DIR, '../..')
44
+ }
45
+
46
+ const PACKAGE_ROOT = findPackageRoot()
47
+
48
+ /** 包根目录 */
49
+ export function getPackageRoot(): string {
50
+ return PACKAGE_ROOT
51
+ }
52
+
26
53
  /** 数据根目录(配置/数据库/皮肤/快照) */
27
54
  export function getDataRoot(): string {
28
55
  const env = process.env.DESKPET_HOME
@@ -34,7 +61,7 @@ export function getDataRoot(): string {
34
61
  export function getResourcesRoot(): string {
35
62
  const env = process.env.DESKPET_RESOURCES
36
63
  if (env && env.trim()) return resolve(env.trim())
37
- return resolve(MODULE_DIR, '../../resources')
64
+ return join(PACKAGE_ROOT, 'resources')
38
65
  }
39
66
 
40
67
  /** 用户配置目录 */