@wanghaopeng1148/deskpet 2.0.0 → 2.0.1

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 CHANGED
@@ -391,4 +391,19 @@ resources/ 皮肤、图标等随包资源
391
391
  (执行中 / 排队 N),列表里排队任务显示 `#序号`,点「取消排队」可撤销;仪表盘横幅也会显示执行中与排队数。
392
392
  前端通过 `GET /api/queue` 拉取,并用 WebSocket `queue-changed` 实时更新。
393
393
  - 通知通道:系统原生通知(osascript / PowerShell Toast / notify-send)+ 管理台页面内提示 + 微信。
394
- - 开机自启:Windows 写入 `HKCU\...\Run`,macOS 写入 `~/Library/LaunchAgents`,Linux 暂不支持。
394
+ - **开机自启**:管理台「设置」页有开关(对应配置项 `system.autoStart`)。开启后各平台的落地方式:
395
+
396
+ | 平台 | 写到哪里 |
397
+ | --- | --- |
398
+ | Windows | 启动文件夹 `DeskPet.cmd`(不依赖 reg / schtasks,受管控的机器也能用) |
399
+ | macOS | `~/Library/LaunchAgents/DeskPet.plist` |
400
+ | Linux | `~/.config/autostart/DeskPet.desktop`(XDG 标准) |
401
+
402
+ 注册的命令统一是 `bin/deskpet.mjs --no-open` —— 它内部用当前 node 拉起服务(不依赖 PATH),
403
+ 也不会在开机时弹浏览器。**想临时停用,直接删掉对应文件即可**,或把设置页开关关掉。
404
+ 服务启动时也会按配置同步一次(关闭时会移除文件)。
405
+
406
+ > 历史坑:本项目早期是 Electron 应用,自启是启动文件夹里的 `DeskPet.lnk`
407
+ > (指向旧的 `pet\dist\DeskPet.exe`)。改成 Node 服务、目录改名后,那个快捷方式不会自动失效消失,
408
+ > 结果就是「以为自启开着,实际每次开机拉起的是旧程序,新服务仍要手动启动」。
409
+ > 所以开启自启时会**主动清理**同名的旧 `.lnk`。
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.1",
5
5
  "description": "DeskPet 2.0 — 本机任务自动化服务 + Web 管理台 (Node + Express + Vue 3 + TypeScript)",
6
6
  "type": "module",
7
7
  "author": "whp",
@@ -1,55 +1,86 @@
1
1
  /**
2
- * 开机自启 — 取代 Electron app.setLoginItemSettings
3
- * Windows: HKCU\...\Run 注册表项
4
- * macOS : ~/Library/LaunchAgents plist
5
- * Linux : 暂不支持
2
+ * 开机自启
6
3
  *
7
- * 仅在用户在设置中开启时写入;任何异常都只记录日志,不影响服务启动。
4
+ * 各平台实现:
5
+ * Windows 启动文件夹放一个 .cmd —— 不依赖 reg / schtasks 命令,受管控的机器一样能用
6
+ * macOS ~/Library/LaunchAgents/<APP_ID>.plist
7
+ * Linux ~/.config/autostart/<APP_ID>.desktop(XDG 标准,GNOME/KDE 都认)
8
+ *
9
+ * 启动命令统一走 `bin/deskpet.mjs --no-open`,理由:
10
+ * · bin/ 在「源码运行」和「npm 包运行」两种形态下都存在(scripts/ 不进发布包)
11
+ * · 它内部用 process.execPath 拉起 server/main.ts,不依赖 PATH 里有没有 node / npm
12
+ * · 脚本自己会按 package.json 推导根目录,所以不依赖工作目录
13
+ * · --no-open 避免每次开机都弹一个浏览器
14
+ *
15
+ * 历史坑(说明为什么要主动清理旧文件):
16
+ * 本项目早期是 Electron 应用,自启是安装包写在启动文件夹里的快捷方式
17
+ * (DeskPet.lnk → pet\dist\DeskPet.exe)。后来改成纯 Node 服务、目录也从 pet 换成 pet2.0,
18
+ * 那个旧快捷方式却没人清理 —— 结果用户以为自启开着,实际每次开机拉起的是旧程序,
19
+ * 新服务仍然要手动启动。因此下面在写入自启时会**顺手删掉同名的旧 .lnk**。
20
+ *
21
+ * 仅在用户在设置中开启时写入;任何异常都只记录日志,不影响服务本身。
8
22
  */
9
- import { execFile } from 'node:child_process'
10
23
  import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs'
11
24
  import { homedir } from 'node:os'
12
- import { join } from 'node:path'
25
+ import { dirname, join, resolve } from 'node:path'
13
26
  import { fileURLToPath } from 'node:url'
14
27
 
15
28
  const APP_ID = 'DeskPet'
16
29
 
17
- /** 当前入口脚本绝对路径(供开机启动调用) */
18
- function entryScript(): string {
19
- return fileURLToPath(import.meta.url).replace(/utils[\\/]auto-start\.ts$/, 'main.ts')
30
+ /** 应用根目录(源码运行为项目根,npm 包运行为包根) */
31
+ function appRoot(): string {
32
+ return resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
20
33
  }
21
34
 
22
- /** 建议的启动命令:node <入口> */
23
- function startCommand(): string {
24
- return `"${process.execPath}" "${entryScript()}"`
35
+ /** 开机要执行的 node 可执行文件与入口脚本 */
36
+ function launch(): { node: string; entry: string } {
37
+ return { node: process.execPath, entry: join(appRoot(), 'bin', 'deskpet.mjs') }
25
38
  }
26
39
 
27
- export async function setAutoStart(on: boolean): Promise<boolean> {
28
- try {
29
- if (process.platform === 'win32') {
30
- const args = on
31
- ? ['add', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run', '/v', APP_ID, '/t', 'REG_SZ', '/d', startCommand(), '/f']
32
- : ['delete', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run', '/v', APP_ID, '/f']
33
- await new Promise<void>((resolve) => {
34
- execFile('reg', args, { windowsHide: true }, () => resolve())
35
- })
36
- return true
37
- }
40
+ // ── Windows ─────────────────────────────────────────────────
38
41
 
39
- if (process.platform === 'darwin') {
40
- const dir = join(homedir(), 'Library', 'LaunchAgents')
41
- const plist = join(dir, `${APP_ID}.plist`)
42
- if (!on) {
43
- if (existsSync(plist)) unlinkSync(plist)
44
- return true
45
- }
46
- mkdirSync(dir, { recursive: true })
47
- const cmd = startCommand().replace(/"/g, '')
48
- const parts = cmd.split(' ')
49
- const programArgs = parts.slice(1).map((p) => ` <string>${p}</string>`).join('\n')
50
- writeFileSync(
51
- plist,
52
- `<?xml version="1.0" encoding="UTF-8"?>
42
+ function winStartupDir(): string {
43
+ const appData = process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming')
44
+ return join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
45
+ }
46
+
47
+ function winCmdPath(): string {
48
+ return join(winStartupDir(), `${APP_ID}.cmd`)
49
+ }
50
+
51
+ function winLegacyLnkPath(): string {
52
+ return join(winStartupDir(), `${APP_ID}.lnk`)
53
+ }
54
+
55
+ /**
56
+ * 生成自启批处理。
57
+ *
58
+ * 注意两点:
59
+ * · 换行必须是 CRLF,注释用英文 —— cmd 按系统 ANSI 代码页解析批处理,
60
+ * 写中文注释在中文 Windows 上容易乱码(虽然注释不影响执行,但很难看)
61
+ * · 用 `start /min` 让服务窗口最小化,不打扰用户,同时任务栏里能看到它在跑
62
+ */
63
+ function winCmdContent(): string {
64
+ const { node, entry } = launch()
65
+ return [
66
+ '@echo off',
67
+ 'rem DeskPet autostart - generated by the DeskPet dashboard.',
68
+ 'rem Delete this file to disable autostart.',
69
+ `cd /d "${appRoot()}"`,
70
+ `start "${APP_ID}" /min "${node}" "${entry}" --no-open`,
71
+ ''
72
+ ].join('\r\n')
73
+ }
74
+
75
+ // ── macOS ───────────────────────────────────────────────────
76
+
77
+ function macPlistPath(): string {
78
+ return join(homedir(), 'Library', 'LaunchAgents', `${APP_ID}.plist`)
79
+ }
80
+
81
+ function macPlistContent(): string {
82
+ const { node, entry } = launch()
83
+ return `<?xml version="1.0" encoding="UTF-8"?>
53
84
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
54
85
  <plist version="1.0">
55
86
  <dict>
@@ -57,32 +88,110 @@ export async function setAutoStart(on: boolean): Promise<boolean> {
57
88
  <string>${APP_ID}</string>
58
89
  <key>ProgramArguments</key>
59
90
  <array>
60
- <string>${parts[0]}</string>
61
- ${programArgs}
91
+ <string>${node}</string>
92
+ <string>${entry}</string>
93
+ <string>--no-open</string>
62
94
  </array>
95
+ <key>WorkingDirectory</key>
96
+ <string>${appRoot()}</string>
63
97
  <key>RunAtLoad</key>
64
98
  <true/>
65
99
  </dict>
66
100
  </plist>
67
- `,
68
- 'utf-8'
69
- )
101
+ `
102
+ }
103
+
104
+ // ── Linux (XDG autostart) ───────────────────────────────────
105
+
106
+ function linuxDesktopPath(): string {
107
+ return join(homedir(), '.config', 'autostart', `${APP_ID}.desktop`)
108
+ }
109
+
110
+ function linuxDesktopContent(): string {
111
+ const { node, entry } = launch()
112
+ return [
113
+ '[Desktop Entry]',
114
+ 'Type=Application',
115
+ `Name=${APP_ID}`,
116
+ 'Comment=DeskPet 本机任务自动化服务',
117
+ `Exec="${node}" "${entry}" --no-open`,
118
+ `Path=${appRoot()}`,
119
+ 'X-GNOME-Autostart-enabled=true',
120
+ ''
121
+ ].join('\n')
122
+ }
123
+
124
+ // ── 对外接口 ────────────────────────────────────────────────
125
+
126
+ /** 开启 / 关闭开机自启,返回是否成功(失败原因会打印到日志) */
127
+ export async function setAutoStart(on: boolean): Promise<boolean> {
128
+ try {
129
+ if (process.platform === 'win32') {
130
+ const dir = winStartupDir()
131
+ mkdirSync(dir, { recursive: true })
132
+
133
+ // 清掉 Electron 时代遗留的失效快捷方式(它指向 pet\dist\DeskPet.exe)
134
+ const legacy = winLegacyLnkPath()
135
+ if (existsSync(legacy)) {
136
+ try {
137
+ unlinkSync(legacy)
138
+ console.log(`[autostart] 已清理旧版自启快捷方式: ${legacy}`)
139
+ } catch (err) {
140
+ console.warn(`[autostart] 旧快捷方式清理失败(可手动删除): ${String(err)}`)
141
+ }
142
+ }
143
+
144
+ const file = winCmdPath()
145
+ if (on) {
146
+ writeFileSync(file, winCmdContent(), 'utf-8')
147
+ console.log(`[autostart] 开机自启已开启: ${file}`)
148
+ } else if (existsSync(file)) {
149
+ unlinkSync(file)
150
+ console.log('[autostart] 开机自启已关闭')
151
+ }
70
152
  return true
71
153
  }
154
+
155
+ if (process.platform === 'darwin') {
156
+ const file = macPlistPath()
157
+ if (on) {
158
+ mkdirSync(dirname(file), { recursive: true })
159
+ writeFileSync(file, macPlistContent(), 'utf-8')
160
+ console.log(`[autostart] 开机自启已开启: ${file}`)
161
+ } else if (existsSync(file)) {
162
+ unlinkSync(file)
163
+ console.log('[autostart] 开机自启已关闭')
164
+ }
165
+ return true
166
+ }
167
+
168
+ if (process.platform === 'linux') {
169
+ const file = linuxDesktopPath()
170
+ if (on) {
171
+ mkdirSync(dirname(file), { recursive: true })
172
+ writeFileSync(file, linuxDesktopContent(), 'utf-8')
173
+ console.log(`[autostart] 开机自启已开启: ${file}`)
174
+ } else if (existsSync(file)) {
175
+ unlinkSync(file)
176
+ console.log('[autostart] 开机自启已关闭')
177
+ }
178
+ return true
179
+ }
180
+
181
+ console.warn(`[autostart] 暂不支持的平台: ${process.platform}`)
182
+ return false
72
183
  } catch (err) {
73
184
  console.error('[autostart] 设置失败:', err)
74
185
  return false
75
186
  }
76
- console.warn(`[autostart] 暂不支持的平台: ${process.platform}`)
77
- return false
78
187
  }
79
188
 
80
- /** 查询当前是否已设置自启(尽力而为,失败返回 false) */
189
+ /** 查询当前是否已设置自启 */
81
190
  export function autoStartEnabled(): boolean {
82
191
  try {
83
- if (process.platform === 'darwin') {
84
- return existsSync(join(homedir(), 'Library', 'LaunchAgents', `${APP_ID}.plist`))
85
- }
192
+ if (process.platform === 'win32') return existsSync(winCmdPath())
193
+ if (process.platform === 'darwin') return existsSync(macPlistPath())
194
+ if (process.platform === 'linux') return existsSync(linuxDesktopPath())
86
195
  } catch {
87
196
  /* ignore */
88
197
  }