desktop-pet-app 0.3.13 → 0.3.24
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 +23 -208
- package/bin/cli.js +173 -0
- package/out/main/chunks/tray-iconTemplate-BIK86F-0.png +0 -0
- package/out/main/chunks/tray-iconTemplate@2x-B19HpcaE.png +0 -0
- package/out/main/index.js +3771 -0
- package/out/mcp-server/index.js +37725 -0
- package/out/mcp-server/package.json +1 -0
- package/out/mcp-server/remote.js +122 -0
- package/out/mcp-server/tools.js +21 -0
- package/out/preload/index.js +38 -0
- package/out/renderer/assets/hoot-C7XVSf5y.webp +0 -0
- package/out/renderer/assets/index-CHc4qvDr.js +1684 -0
- package/out/renderer/assets/p2p-LRPQ9Jzg.js +374 -0
- package/out/renderer/assets/robot-blue-CFH9bWjZ.png +0 -0
- package/out/renderer/index.html +156 -0
- package/out/renderer/p2p.html +6 -0
- package/package.json +63 -2
package/README.md
CHANGED
|
@@ -1,245 +1,60 @@
|
|
|
1
|
-
# Desktop Pet
|
|
1
|
+
# Desktop Pet
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
一个可悬浮在桌面的 AI 宠物,支持账号登录、好友协作与 MCP。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## 仓库结构
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
app/ Electron 宠物端(TypeScript,独占 WS)+ MCP server(只连本地宠物)
|
|
11
|
-
server/ Go 中转服务器(WebSocket + 用户门户 API)
|
|
12
|
-
web/ 桌宠登录指引页(账号 API 仍由服务端提供给桌宠应用)
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## 协作架构
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
Agent ──MCP stdio──▶ MCP server ──HTTP localhost + session token──▶ Electron 桌宠 ──WebSocket──▶ Go relay
|
|
19
|
-
▲
|
|
20
|
-
好友同理:宠物收任务入队并冒泡 ── Agent 说「帮朋友一下」── MCP list/answer ──┘
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
- MCP **不直连**服务器;同一 `userId` 的 WS 由桌宠独占(避免双连踢线)。
|
|
24
|
-
- Electron 启动时在 `~/.desktop-pet-port` 写入仅当前用户可读写的 JSON 发现信息(端口、PID、随机会话令牌);MCP 会为每个 localhost 请求附上该令牌。不要手动共享或提交这个文件。
|
|
25
|
-
- v1 任务异步持久化到 PostgreSQL,支持 ACK、lease、取消、过期、断线补拉和离线结果;旧问答工具保留兼容映射。
|
|
26
|
-
|
|
27
|
-
## 快速开始
|
|
28
|
-
|
|
29
|
-
### 1. 启动桌面宠物
|
|
30
|
-
|
|
31
|
-
协作身份与中转地址配置在**宠物进程**(不是 MCP)上:
|
|
5
|
+
## 安装
|
|
32
6
|
|
|
33
7
|
```bash
|
|
34
|
-
|
|
35
|
-
npm install
|
|
36
|
-
npm run dev
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
正式客户端默认连接 `https://deskpalai.com`,自动推导 WebSocket `wss://deskpalai.com/ws`、登录 API `https://deskpalai.com/api/*` 和更新检查 `https://deskpalai.com/api/updates/check`,普通用户无需配置任何地址。`DESKTOP_PET_HOST` 与旧的 `DESKTOP_PET_SERVER=ws://…/ws` 只保留给本地开发、测试或私有部署;本地服务使用 `DESKTOP_PET_HOST=http://127.0.0.1:8080 npm run dev`。`DESKTOP_PET_SKIN=robot-blue` 可启用蓝色协作机器人。右键桌宠,选择「登录账号」或「注册账号」会使用系统默认浏览器打开服务端网页;网页认证成功后通过 `desktop-pet://` 自动唤回桌宠,并绑定当前这只宠物。刷新令牌和设备签名/加密私钥会由 Electron `safeStorage` 保存到 macOS Keychain 支持的加密存储。`DESKTOP_PET_TOKEN` 仅保留为不落盘的开发覆盖。仅本机演示可在服务端设置 `DESKTOP_PET_ALLOW_INSECURE_WS=1`,不要在公网开启。
|
|
40
|
-
|
|
41
|
-
宠物出现在桌面右下角。右键宠物打开菜单:
|
|
42
|
-
|
|
43
|
-
- **切换形象**:Codex 小宠 / Claude 小宠 / 我的世界小宠 / Professor Hoot / 橘猫 / 小狗 / 小鸟 / 狮子 / 老虎 / 蓝鲸 / 企鹅
|
|
44
|
-
- **登录账号 / 注册账号**:用系统默认浏览器完成认证,随后自动回到桌宠并绑定当前桌宠
|
|
45
|
-
- **复制 MCP 配置**:生成并复制当前 Agent 可直接粘贴的 MCP 注册 JSON(多 profile 联调时自动带上 `DESKTOP_PET_PORT_FILE`)
|
|
46
|
-
- **送走这只宠物 / 退出**
|
|
47
|
-
|
|
48
|
-
拖拽宠物时它会挣扎,落地会弹跳。
|
|
49
|
-
|
|
50
|
-
### 2. 接入 agent(MCP)
|
|
51
|
-
|
|
52
|
-
先构建 MCP server:
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
cd app
|
|
56
|
-
npm run build # 产物在 out/mcp-server/index.js
|
|
57
|
-
```
|
|
8
|
+
# 公网包(发布后)
|
|
9
|
+
npm install --global desktop-pet-app
|
|
58
10
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
```json
|
|
62
|
-
{
|
|
63
|
-
"mcpServers": {
|
|
64
|
-
"desktop-pet": {
|
|
65
|
-
"command": "node",
|
|
66
|
-
"args": ["/绝对路径/desktop-pet/app/out/mcp-server/index.js"]
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
11
|
+
# 或本地 tgz
|
|
12
|
+
npm install --global ./desktop-pet-app-0.1.1.tgz
|
|
70
13
|
```
|
|
71
14
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
| 工具 | 何时用 | 作用 |
|
|
75
|
-
| --- | --- | --- |
|
|
76
|
-
| `set_pet_state` | 本地表演 | idle / walk / sleep / excited |
|
|
77
|
-
| `show_bubble` | 本地表演 | 头顶冒气泡 |
|
|
78
|
-
| `pet_react` | 本地表演 | 情绪反应 + 吐槽 |
|
|
79
|
-
| `ask_remote_pet` | 「让朋友帮忙 / 问问某某」 | 派发任务(title/question/context/tools),阻塞等结果并要求落地 |
|
|
80
|
-
| `wait_for_remote_task` | 「开始桌宠值班 / 等待朋友任务」 | 接收方 Agent 持续长轮询;完成并回传一项任务后必须继续等待,直到用户明确停止 |
|
|
81
|
-
| `get_pending_questions` | 「帮我朋友一下」 | 列出待办任务与上下文、建议工具 |
|
|
82
|
-
| `answer_question` | 做完好友任务后 | 回传结果(`resultKind`: text/code/patch/markdown/files) |
|
|
83
|
-
| `delegate_task` / `get_task` / `wait_task_result` | v1 异步任务 | 创建、查询并等待持久化任务 |
|
|
84
|
-
| `request_task_context` / `supply_task_context` | 缺少资料 | 在同一任务内补充最小必要上下文 |
|
|
85
|
-
| `submit_task_result` / `cancel_task` / `list_remote_tasks` | v1 生命周期 | 回传结构化结果、取消和筛选任务 |
|
|
86
|
-
| `prepare_task_workspace` / `submit_task_workspace_result` | v1 代码执行 | B 在独立 Git worktree 修改;工具自动审计范围并生成 patch |
|
|
87
|
-
| `upload_task_artifact` / `download_task_artifact` | 大文件 fallback | 端到端加密 relay artifact;下载后校验并安全写入新文件 |
|
|
88
|
-
| `validate_task_result` | 应用结果前 | 校验路径、哈希、大小、patch 基线和符号链接 |
|
|
89
|
-
| `apply_task_patch` | 应用代码结果 | A 校验基线、可写范围、本地冲突及 `git apply --check` 后检查或应用 |
|
|
90
|
-
|
|
91
|
-
宠物未启动时工具会返回明确错误。提问方默认最长等待约 5 分钟。
|
|
92
|
-
|
|
93
|
-
### 3. 启动中转服务器与用户门户
|
|
94
|
-
|
|
95
|
-
先构建门户前端,再启动 Go 服务(同时提供 WebSocket、REST API,并可托管静态页):
|
|
15
|
+
国内网络若拉不到 Electron 二进制,加镜像:
|
|
96
16
|
|
|
97
17
|
```bash
|
|
98
|
-
|
|
99
|
-
npm install
|
|
100
|
-
npm run build
|
|
101
|
-
|
|
102
|
-
cd ../server
|
|
103
|
-
go run ./cmd/server -config config.yaml
|
|
18
|
+
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm install --global ./desktop-pet-app-0.1.1.tgz
|
|
104
19
|
```
|
|
105
20
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
直接打开网页会显示桌宠登录指引;由桌宠打开的网页会显示登录/注册表单,并在完成后自动回传桌宠。开发联调时可分别起两端:`cd web && npm run dev`(默认 `http://127.0.0.1:4001`,Vite 会把 `/api` 代理到 `:8080`)。
|
|
109
|
-
|
|
110
|
-
用户、设备、任务、事件和加密 relay artifact 元数据存在 PostgreSQL;首次启动按 `server/internal/store/migrations/` 自动迁移。
|
|
111
|
-
|
|
112
|
-
跨用户任务派发要求双方先在门户中确认好友关系,再由求助方从桌宠右键菜单发出求助(「请 TA 帮我」)、对方在「要我帮忙」菜单中明确同意;未同意时 MCP 返回 `HELP_NOT_ACCEPTED`。中转服务器会从桌宠 Bearer token 绑定身份,不信任客户端任意上报的 `userId`。
|
|
113
|
-
|
|
114
|
-
**门户 API**
|
|
115
|
-
|
|
116
|
-
| 方法 | 路径 | 说明 |
|
|
117
|
-
| --- | --- | --- |
|
|
118
|
-
| `POST` | `/api/auth/register` | 注册 |
|
|
119
|
-
| `POST` | `/api/auth/login` | 登录 |
|
|
120
|
-
| `GET` | `/api/auth/me` | 当前用户(需 Bearer Token) |
|
|
121
|
-
| `POST` | `/api/auth/password` | 修改当前账号密码 |
|
|
122
|
-
| `GET` | `/api/users/search?q=` | 搜索用户 |
|
|
123
|
-
| `GET` | `/api/friends` | 好友与待处理请求 |
|
|
124
|
-
| `POST` | `/api/friends/request` | 发起好友请求 |
|
|
125
|
-
| `POST` | `/api/friends/{id}/accept` | 同意 |
|
|
126
|
-
| `POST` | `/api/friends/{id}/reject` | 拒绝 |
|
|
127
|
-
| `DELETE` | `/api/friends/{id}` | 解除 / 撤销 |
|
|
128
|
-
| `POST/GET/DELETE` | `/api/devices`、`/api/devices/{id}` | 创建设备、列表和撤销 |
|
|
129
|
-
| `POST` | `/api/auth/device-token` | 用一次性刷新凭据换短期设备 WS token |
|
|
130
|
-
| `GET` | `/api/p2p/ice-config` | STUN 与短时 TURN REST 凭据 |
|
|
131
|
-
| `PUT/GET` | `/api/tasks/{taskId}/artifacts/{artifactId}` | 任务参与者上传/下载加密 relay blob |
|
|
132
|
-
|
|
133
|
-
服务器分层结构:`cmd/`(入口装配)、`internal/config`(配置)、`internal/store`(PostgreSQL 用户与好友)、`internal/auth`(JWT)、`internal/api`(门户 REST)、`internal/hub`(领域路由 + 单测)、`internal/httpserver` + `internal/middleware`(HTTP 装配)、`internal/apperr`(自定义错误码)、`internal/response`(标准输出格式)、`internal/proto`(WS 协议)、`internal/observability`(slog JSON 日志 + Prometheus 指标)。
|
|
134
|
-
|
|
135
|
-
**可观测性端点**
|
|
136
|
-
|
|
137
|
-
| 端点 | 说明 |
|
|
138
|
-
| --- | --- |
|
|
139
|
-
| `GET /healthz` / `GET /readyz` | 存活/就绪探针,输出统一格式 `{"code":0,"message":"ok","data":{...}}` |
|
|
140
|
-
| `GET /metrics` | Prometheus 指标:在线连接数、消息数、提问路由结果、HTTP 耗时 |
|
|
141
|
-
|
|
142
|
-
WS 错误消息带稳定错误码(`{"type":"error","code":"USER_NOT_ONLINE",...}`),客户端可按 `code` 程序化分支。测试:`go test ./...`
|
|
143
|
-
|
|
144
|
-
### 4. 双机任务协作联调
|
|
145
|
-
|
|
146
|
-
两边都要:**先开 Go 服务器 → 再开桌宠(不同 `DESKTOP_PET_USER`)→ Agent 挂上 MCP**。
|
|
147
|
-
|
|
148
|
-
1. 共用一台 `server`(`:8080`)
|
|
149
|
-
2. A:`DESKTOP_PET_USER=alice npm run dev`;B:`DESKTOP_PET_USER=bob npm run dev`
|
|
150
|
-
3. A 对 Agent 说:「让 bob 帮我看这个问题…」(可附上下文)
|
|
151
|
-
→ Agent 调 `ask_remote_pet`;A 的宠物离场出差
|
|
152
|
-
4. B 的宠物冒泡「有任务了!」;B 对 Agent 说:「帮我朋友一下」
|
|
153
|
-
→ `get_pending_questions` → 本机完成 → `answer_question`
|
|
154
|
-
5. A 的宠物归来 + 小鱼干;`ask_remote_pet` 返回结构化结果
|
|
155
|
-
→ A 的 Agent **审查后应用到工作区**(改代码 / 写文件等)
|
|
156
|
-
|
|
157
|
-
本机同时运行多个桌宠进程时,每个进程必须使用独立的 `DESKTOP_PET_PROFILE`,否则 Electron 的单实例锁会让第二个进程退出。最小测试方式(每条命令使用一个终端):
|
|
21
|
+
安装后直接启动:
|
|
158
22
|
|
|
159
23
|
```bash
|
|
160
|
-
|
|
161
|
-
DESKTOP_PET_PROFILE=/tmp/desktop-pet-alice DESKTOP_PET_USER=alice DESKTOP_PET_SKIN=cat npm run dev
|
|
162
|
-
DESKTOP_PET_PROFILE=/tmp/desktop-pet-bob DESKTOP_PET_USER=bob DESKTOP_PET_SKIN=dog npm run dev
|
|
24
|
+
desktop-pet
|
|
163
25
|
```
|
|
164
26
|
|
|
165
|
-
|
|
27
|
+
客户端默认连接 `https://api.xinbit.top/desktop-pet`(登录/注册门户仍由 `https://deskpalai.com` 提供),会自动推导 WebSocket、登录和更新接口。只有本地开发或私有部署时才需要设置 `DESKTOP_PET_HOST`(可含路径前缀)。应用内更新仅接受 HTTPS 地址。
|
|
166
28
|
|
|
167
|
-
|
|
29
|
+
打包发布时可通过环境变量覆盖官方地址(构建期内联,见 `src/shared/service-addresses.ts`):`DESKTOP_PET_SERVER_ROOT`(服务根)、`DESKTOP_PET_PORTAL_ORIGIN`(门户)、`DESKTOP_PET_LEGACY_HOSTS`(需无缝迁移的旧服务器 hostname,逗号分隔)。
|
|
168
30
|
|
|
169
|
-
|
|
31
|
+
首次启动后,在宠物右键菜单中选择“登录账号”或“注册账号”。
|
|
170
32
|
|
|
171
|
-
|
|
33
|
+
## 更新
|
|
172
34
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
代码写入任务不能再用通用 `submit_task_result` 直接完成:MCP 与服务端都会在落终态前检查 `requireTests`、产物类型/大小以及代码 patch/file 是否存在,失败会返回具体原因且任务保持运行。超过本地消息阈值的自动 patch 会加密上传为 relay artifact,`apply_task_patch` 会自动下载、验哈希并继续相同的范围与 `git apply --check` 校验。这样不会因一次缺字段的诊断提交把任务锁成“已完成但没有文件”。
|
|
176
|
-
|
|
177
|
-
每个宠物窗口都有常驻身份徽标:`本机主宠` 或 `好友 @用户名`。收到远端任务时,同一好友只保留一个来访形象;右键菜单会同时区分来访好友身份和本机登录账号,皮肤不再被当作账号身份。
|
|
178
|
-
|
|
179
|
-
MCP 工具不能在 Agent 完全未启动、没有会话时强行唤醒它;这种无人值守体验需要额外的、按 Agent 产品实现的可选启动适配器。基础值班模式不依赖该适配器。
|
|
180
|
-
|
|
181
|
-
如需显式启用首个无人值守试点,可设置 `DESKTOP_PET_RUNNER_ADAPTERS=1` 和 `DESKTOP_PET_RUNNER_WORKSPACE=/绝对工作区`。当前 adapter 使用已安装且自行管理凭据的 Codex CLI;未检测到 CLI 或运行失败时继续保留通用 MCP 值班模式。P2P 数据面默认开启,可设置 `DESKTOP_PET_P2P=0` 关闭;已确认好友之间的简单聊天优先建立 WebRTC 直连,建连失败时自动使用中转消息或加密 relay artifact,无需创建第二个业务任务。帮助邀请和离线提醒走服务器控制通道,以便对方下次上线仍能收到。
|
|
182
|
-
|
|
183
|
-
## 开发
|
|
35
|
+
通过 npm 安装的 macOS 版本可以直接使用应用内更新,首次更新会安装并切换到 `/Applications/Desktop Pet.app`。也可以继续使用 npm 更新:
|
|
184
36
|
|
|
185
37
|
```bash
|
|
186
|
-
|
|
187
|
-
npm run typecheck # 类型检查
|
|
188
|
-
npm run build # 构建 Electron 三端 + MCP server
|
|
189
|
-
|
|
190
|
-
cd server
|
|
191
|
-
go build ./... # 构建 Go 服务器
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
真实双 session 值班链路可在服务器和两只已登录桌宠运行时重复验证:
|
|
195
|
-
|
|
196
|
-
```bash
|
|
197
|
-
cd app
|
|
198
|
-
ASKER_PORT_FILE=/tmp/desktop-pet-ceshi.port \
|
|
199
|
-
DUTY_PORT_FILE=/tmp/desktop-pet-ceshi2.port \
|
|
200
|
-
DUTY_USER=ceshi2 \
|
|
201
|
-
npm run e2e:duty
|
|
38
|
+
npm update --global desktop-pet-app
|
|
202
39
|
```
|
|
203
40
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
`npm run e2e:codebuddy:git` 用真实 CodeBuddy 驻守会话验证隔离 worktree 修改、测试、patch 回传和 A 端安全应用;紧接着运行 `npm run e2e:codebuddy:context` 可验证下一轮无需重新读取文件也能使用同一会话的上一轮上下文。
|
|
41
|
+
`desktop-pet` 会比较 npm 包与 `/Applications/Desktop Pet.app` 的版本并启动较新者,同版本优先原生应用。两者共享 `~/Library/Application Support/Desktop Pet` 中的用户数据;该目录不是程序安装目录。npm 不会绕过系统安装器直接写入 `/Applications`。
|
|
207
42
|
|
|
208
|
-
|
|
43
|
+
如果使用 macOS `.pkg` 或 Windows 安装包安装,桌宠会从服务器的更新接口发现、下载并安装新版本。
|
|
209
44
|
|
|
210
|
-
|
|
45
|
+
## 发布者命令
|
|
211
46
|
|
|
212
47
|
```bash
|
|
213
|
-
cd app
|
|
214
|
-
npm install
|
|
215
48
|
npm run typecheck
|
|
216
49
|
npm test
|
|
50
|
+
npm run pack:check
|
|
217
51
|
npm publish
|
|
218
52
|
```
|
|
219
53
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
用户使用 npm 安装和更新:
|
|
223
|
-
|
|
224
|
-
```bash
|
|
225
|
-
npm install --global desktop-pet-app
|
|
226
|
-
desktop-pet
|
|
227
|
-
npm update --global desktop-pet-app
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
npm 包用于开发者/命令行分发;桌宠内置“请求下载 → 安装并重启”使用平台原生安装包。在 macOS 上,两种分发方式共享 `~/Library/Application Support/Desktop Pet` 中的账号、缓存和单实例状态,但程序文件仍分别位于 npm 全局目录与 `/Applications/Desktop Pet.app`。`desktop-pet` 会校验原生 bundle 身份并比较两边版本,始终启动版本较高的一份,同版本优先启动原生应用;因此 npm 更新和应用内更新可以交替使用,不会切回旧版本。旧版 npm 数据目录会在首次启动时原子迁移。
|
|
231
|
-
|
|
232
|
-
macOS 构建 `.pkg`:
|
|
54
|
+
macOS 原生安装包:
|
|
233
55
|
|
|
234
56
|
```bash
|
|
235
|
-
cd app
|
|
236
57
|
npm run package:mac
|
|
237
58
|
```
|
|
238
59
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
生产更新由独立 `update-service` 负责:PostgreSQL 保存版本控制面和审计记录,安装包位于对象存储/CDN,Electron 校验 Ed25519 manifest 签名、文件大小和 SHA-256。它支持多渠道、多平台、确定性灰度、强更、暂停和回滚。发布新版本只需上传安装包并运行 `server/cmd/releasectl`,不再修改 Helm/Dockerfile,也不再重启 relay。完整的密钥初始化、部署和日常发布命令见 [企业级桌面应用更新系统](docs/in-app-update-design.md)。relay 原有配置文件方式仅作为私有部署兼容层保留。
|
|
242
|
-
|
|
243
|
-
## 新增皮肤
|
|
244
|
-
|
|
245
|
-
在 `app/src/renderer/skins/` 下新增字符皮肤或高分辨率精灵皮肤并在注册表登记。精灵皮肤可参考 `robot-blue.ts`:支持透明 PNG/WebP、平滑缩放、自适应窗口、气泡/地面/特效锚点,以及 working/success/failed 协作状态。
|
|
60
|
+
发布前请为正式包配置 Apple Developer 签名和公证凭据。
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execFileSync, spawn } = require('node:child_process')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const os = require('node:os')
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
|
|
8
|
+
const manifest = require('../package.json')
|
|
9
|
+
|
|
10
|
+
const MAC_APP_PATH = '/Applications/Desktop Pet.app'
|
|
11
|
+
const MAC_BUNDLE_ID = 'com.xinshu.desktoppet'
|
|
12
|
+
const TAKEOVER_TIMEOUT_MS = 5_000
|
|
13
|
+
|
|
14
|
+
function parseVersion(value) {
|
|
15
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(value)
|
|
16
|
+
if (!match) return undefined
|
|
17
|
+
return {
|
|
18
|
+
major: Number(match[1]),
|
|
19
|
+
minor: Number(match[2]),
|
|
20
|
+
patch: Number(match[3]),
|
|
21
|
+
prerelease: match[4]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function compareVersions(a, b) {
|
|
26
|
+
const left = parseVersion(a)
|
|
27
|
+
const right = parseVersion(b)
|
|
28
|
+
if (!left || !right) throw new Error(`无效版本号:${!left ? a : b}`)
|
|
29
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
30
|
+
if (left[key] !== right[key]) return left[key] > right[key] ? 1 : -1
|
|
31
|
+
}
|
|
32
|
+
if (!left.prerelease && right.prerelease) return 1
|
|
33
|
+
if (left.prerelease && !right.prerelease) return -1
|
|
34
|
+
if (!left.prerelease || !right.prerelease) return 0
|
|
35
|
+
return left.prerelease.localeCompare(right.prerelease, undefined, { numeric: true })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function selectLaunchTarget(npmVersion, nativeApp, forceNpm = false) {
|
|
39
|
+
if (!forceNpm && nativeApp && compareVersions(nativeApp.version, npmVersion) >= 0) {
|
|
40
|
+
return { kind: 'native', version: nativeApp.version, appPath: nativeApp.appPath }
|
|
41
|
+
}
|
|
42
|
+
return { kind: 'npm', version: npmVersion }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readPlistValue(plistPath, key) {
|
|
46
|
+
return execFileSync('/usr/libexec/PlistBuddy', ['-c', `Print :${key}`, plistPath], {
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
49
|
+
}).trim()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function installedMacApp() {
|
|
53
|
+
const plistPath = path.join(MAC_APP_PATH, 'Contents', 'Info.plist')
|
|
54
|
+
const executablePath = path.join(MAC_APP_PATH, 'Contents', 'MacOS', 'Desktop Pet')
|
|
55
|
+
try {
|
|
56
|
+
if (!fs.existsSync(executablePath)) return undefined
|
|
57
|
+
if (readPlistValue(plistPath, 'CFBundleIdentifier') !== MAC_BUNDLE_ID) return undefined
|
|
58
|
+
const version = readPlistValue(plistPath, 'CFBundleShortVersionString')
|
|
59
|
+
if (!parseVersion(version)) return undefined
|
|
60
|
+
return { version, appPath: MAC_APP_PATH }
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runningDesktopPet() {
|
|
67
|
+
const endpointPath = process.env.DESKTOP_PET_PORT_FILE?.trim() || path.join(os.homedir(), '.desktop-pet-port')
|
|
68
|
+
try {
|
|
69
|
+
const endpoint = JSON.parse(fs.readFileSync(endpointPath, 'utf8'))
|
|
70
|
+
if (!Number.isSafeInteger(endpoint.pid) || endpoint.pid < 1) return undefined
|
|
71
|
+
process.kill(endpoint.pid, 0)
|
|
72
|
+
return {
|
|
73
|
+
pid: endpoint.pid,
|
|
74
|
+
version: typeof endpoint.appVersion === 'string' && parseVersion(endpoint.appVersion)
|
|
75
|
+
? endpoint.appVersion
|
|
76
|
+
: undefined
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function shouldReplaceRunningVersion(targetVersion, runningVersion) {
|
|
84
|
+
return runningVersion === undefined || compareVersions(targetVersion, runningVersion) > 0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function processIsDesktopPet(pid) {
|
|
88
|
+
if (process.platform === 'win32') return false
|
|
89
|
+
try {
|
|
90
|
+
const command = execFileSync('/bin/ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8' })
|
|
91
|
+
return /desktop[- ]pet/i.test(command)
|
|
92
|
+
} catch {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function processIsRunning(pid) {
|
|
98
|
+
try {
|
|
99
|
+
process.kill(pid, 0)
|
|
100
|
+
return true
|
|
101
|
+
} catch {
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function stopOlderRuntime(targetVersion) {
|
|
107
|
+
const running = runningDesktopPet()
|
|
108
|
+
if (!running || !shouldReplaceRunningVersion(targetVersion, running.version) || !processIsDesktopPet(running.pid)) return
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
process.kill(running.pid, 'SIGTERM')
|
|
112
|
+
} catch {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const deadline = Date.now() + TAKEOVER_TIMEOUT_MS
|
|
117
|
+
while (Date.now() < deadline && processIsRunning(running.pid)) {
|
|
118
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100)
|
|
119
|
+
}
|
|
120
|
+
if (processIsRunning(running.pid)) {
|
|
121
|
+
try {
|
|
122
|
+
process.kill(running.pid, 'SIGKILL')
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function npmElectronPath() {
|
|
128
|
+
try {
|
|
129
|
+
return require('electron')
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('未找到 electron 包,请重新安装 desktop-pet-app。')
|
|
132
|
+
console.error('若在国内网络安装失败,可先设置镜像再重装:')
|
|
133
|
+
console.error(' ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm install --global <包路径或包名>')
|
|
134
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
135
|
+
process.exit(1)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function launch() {
|
|
140
|
+
const appPath = path.join(__dirname, '..')
|
|
141
|
+
const forceNpm = Boolean(process.env.DESKTOP_PET_PROFILE?.trim()) || process.platform !== 'darwin'
|
|
142
|
+
const target = selectLaunchTarget(
|
|
143
|
+
manifest.version,
|
|
144
|
+
process.platform === 'darwin' ? installedMacApp() : undefined,
|
|
145
|
+
forceNpm
|
|
146
|
+
)
|
|
147
|
+
stopOlderRuntime(target.version)
|
|
148
|
+
|
|
149
|
+
let command
|
|
150
|
+
let args
|
|
151
|
+
let env = process.env
|
|
152
|
+
if (target.kind === 'native') {
|
|
153
|
+
command = '/usr/bin/open'
|
|
154
|
+
args = ['-n', target.appPath, '--args', ...process.argv.slice(2)]
|
|
155
|
+
} else {
|
|
156
|
+
command = npmElectronPath()
|
|
157
|
+
if (typeof command !== 'string' || !fs.existsSync(command)) {
|
|
158
|
+
console.error('electron 已安装但二进制缺失,请用镜像重装:')
|
|
159
|
+
console.error(' ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm install --global --force <包路径或包名>')
|
|
160
|
+
process.exit(1)
|
|
161
|
+
}
|
|
162
|
+
args = [appPath, ...process.argv.slice(2)]
|
|
163
|
+
env = { ...process.env, DESKTOP_PET_NPM_DISTRIBUTION: '1' }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const child = spawn(command, args, { stdio: 'ignore', detached: true, env })
|
|
167
|
+
child.on('error', (error) => console.error(`Desktop Pet 启动失败:${error.message}`))
|
|
168
|
+
child.unref()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (require.main === module) launch()
|
|
172
|
+
|
|
173
|
+
module.exports = { compareVersions, selectLaunchTarget, shouldReplaceRunningVersion }
|
|
Binary file
|
|
Binary file
|