desktop-pet-app 0.3.4 → 0.3.13
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 +210 -21
- package/package.json +2 -55
- package/bin/cli.js +0 -173
- 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 +0 -3259
- package/out/mcp-server/index.js +0 -1457
- package/out/mcp-server/package.json +0 -1
- package/out/mcp-server/remote.js +0 -122
- package/out/mcp-server/tools.js +0 -21
- package/out/preload/index.js +0 -38
- package/out/renderer/assets/hoot-C7XVSf5y.webp +0 -0
- package/out/renderer/assets/index-CHc4qvDr.js +0 -1684
- package/out/renderer/assets/p2p-LRPQ9Jzg.js +0 -374
- package/out/renderer/assets/robot-blue-CFH9bWjZ.png +0 -0
- package/out/renderer/index.html +0 -156
- package/out/renderer/p2p.html +0 -6
package/README.md
CHANGED
|
@@ -1,56 +1,245 @@
|
|
|
1
|
-
# Desktop Pet
|
|
1
|
+
# Desktop Pet 桌面宠物
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
完整的跨 Agent 协作目标架构、协议、P2P、任务状态机、安全与视觉实施清单见 [`docs/cross-agent-implementation-plan.md`](docs/cross-agent-implementation-plan.md)。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
macOS 桌面像素小宠物:悬浮在窗口之上,平时自己溜达、眨眼、打瞌睡;AI agent 通过 MCP 驱动宠物;跨用户协作时 **Agent → MCP → 桌宠 → Go 中转服务器**,由好友的 Agent 接手任务并把结果送回落地。
|
|
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)上:
|
|
6
32
|
|
|
7
33
|
```bash
|
|
8
|
-
|
|
9
|
-
npm install
|
|
34
|
+
cd app
|
|
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
|
+
- **送走这只宠物 / 退出**
|
|
10
47
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
13
57
|
```
|
|
14
58
|
|
|
15
|
-
|
|
59
|
+
在 agent 的 MCP 配置中注册(以 CodeBuddy / Claude Code 为例)。MCP 只连本地宠物,**无需**再配 `DESKTOP_PET_SERVER`:
|
|
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
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
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,并可托管静态页):
|
|
16
96
|
|
|
17
97
|
```bash
|
|
18
|
-
|
|
98
|
+
cd web
|
|
99
|
+
npm install
|
|
100
|
+
npm run build
|
|
101
|
+
|
|
102
|
+
cd ../server
|
|
103
|
+
go run ./cmd/server -config config.yaml
|
|
19
104
|
```
|
|
20
105
|
|
|
21
|
-
|
|
106
|
+
本地启动配置已提供在 `server/config.yaml`,其中包含监听地址、门户目录、数据库 DSN、开发 JWT 和实时通信参数。生产环境从 `server/config.example.yaml` 复制后替换实际值,并用 `-config /etc/desktop-pet/config.yaml` 启动。优先级为默认值 < 配置文件 < 环境变量 < 命令行 flag;环境变量可用于覆盖数据库密码、JWT 或 TLS 证书路径。
|
|
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 的单实例锁会让第二个进程退出。最小测试方式(每条命令使用一个终端):
|
|
22
158
|
|
|
23
159
|
```bash
|
|
24
|
-
|
|
160
|
+
cd app
|
|
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
|
|
25
163
|
```
|
|
26
164
|
|
|
27
|
-
|
|
165
|
+
如果使用已登录账号,分别在两个窗口里登录对应账号。多个 profile 同时登录时,桌宠会把系统错投的 `desktop-pet://` 登录回调自动转发到真正发起认证的实例;两边都需重启到此版本后再测试。如果只是测试 WS 在线状态,可在本地服务器开启 `DESKTOP_PET_ALLOW_INSECURE_WS=1`,再用不同的 `DESKTOP_PET_USER` 启动。测试结束后可删除这两个临时 profile 目录,里面只包含本地 Electron 配置和测试设备凭据。
|
|
166
|
+
|
|
167
|
+
### 任意 MCP Agent 的“值班”模式
|
|
28
168
|
|
|
29
|
-
|
|
169
|
+
桌宠不会绑定 Codex、Cursor、CodeBuddy 或其他某一家 Agent。接收方在任意支持 MCP tools 的 Agent 中说一次「开始桌宠值班」,Agent 调用 `wait_for_remote_task` 等待任务;拿到任务后,使用**它自身已有**的读文件、编辑、终端等能力完成,再回传结构化结果。值班是持续模式:回传每项任务后以及每次长轮询超时后,Agent 都应立即再次调用 `wait_for_remote_task`,直到用户明确要求停止。本机桌宠短暂重启或端口凭据轮换时,当前等待会在预算内自动重新发现连接;整轮仍未恢复也会返回可继续轮询的正常结果,而不是用工具错误终止 Agent 会话。
|
|
30
170
|
|
|
31
|
-
|
|
171
|
+
设备开箱即用地声明协议词汇表中的能力标签(`filesystem.read,filesystem.write,shell,network,browser,document.create`),无需用户配置环境变量;MCP 客户端初始化后会自动上报宿主与协议信息。能力标签只用于任务匹配,不等于任意写权限:每项任务仍受 `writableScopes`、网络策略和审批模式限制。任务无法派发时,发起方会在 `task.created` 中收到 `warning`,接收方桌宠也会显示缺失能力及“安装或启用对应 Agent 工具后重新连接”的提示;如果 Agent 领取后才发现工具不可用,它会调用 `report_task_capability_gap` 将任务标记为失败,并立刻通知双方。
|
|
32
172
|
|
|
33
|
-
|
|
173
|
+
Git 代码任务采用隔离交付链路:B 先调用 `prepare_task_workspace`,只能在返回的 detached worktree 中修改和测试;`submit_task_workspace_result` 从该 worktree 自动生成与 `baseCommit` 绑定的 patch,并拒绝任何超出 `writableScopes` 的改动。A 的原工作区在此期间不变;A 收到结果后调用 `apply_task_patch`,只有 artifact 哈希、基线、范围、目标文件无本地冲突及 `git apply --check` 全部通过才会应用。`approvalMode=manual` 时还必须显式确认。已接受的定向帮助授权采用 24 小时滑动有效期,每次成功授权任务都会续期,避免活跃的长期值守会话在执行中突然失效。
|
|
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
|
+
## 开发
|
|
34
184
|
|
|
35
185
|
```bash
|
|
36
|
-
|
|
186
|
+
cd app
|
|
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
|
|
37
202
|
```
|
|
38
203
|
|
|
39
|
-
|
|
204
|
+
脚本会启动相互独立的 A/B MCP stdio session,连续派发并完成三项带唯一校验码的任务,验证 B 在 A 派发前已进入长轮询、A 能显示结构化结果,以及同一个 B MCP 进程在多次回传后仍保持可用。测试脚本结束是显式停止边界。
|
|
205
|
+
|
|
206
|
+
`npm run e2e:codebuddy:git` 用真实 CodeBuddy 驻守会话验证隔离 worktree 修改、测试、patch 回传和 A 端安全应用;紧接着运行 `npm run e2e:codebuddy:context` 可验证下一轮无需重新读取文件也能使用同一会话的上一轮上下文。
|
|
207
|
+
|
|
208
|
+
## 发布 npm 包与应用更新
|
|
40
209
|
|
|
41
|
-
|
|
210
|
+
当前 Electron 客户端可发布为 npm 包。先在 npmjs.com 创建账号并执行一次 `npm login`,然后:
|
|
42
211
|
|
|
43
212
|
```bash
|
|
213
|
+
cd app
|
|
214
|
+
npm install
|
|
44
215
|
npm run typecheck
|
|
45
216
|
npm test
|
|
46
|
-
npm run pack:check
|
|
47
217
|
npm publish
|
|
48
218
|
```
|
|
49
219
|
|
|
50
|
-
|
|
220
|
+
默认包名为 `desktop-pet-app`。发布前请先在 npm Registry 确认该名称归你所有;若需改为你的 scope(例如 `@你的账号/desktop-pet`),先修改 `app/package.json` 的 `name`,用户安装与更新命令也使用这个新名称。
|
|
221
|
+
|
|
222
|
+
用户使用 npm 安装和更新:
|
|
51
223
|
|
|
52
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`:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
cd app
|
|
53
236
|
npm run package:mac
|
|
54
237
|
```
|
|
55
238
|
|
|
56
|
-
|
|
239
|
+
构建产物位于 `app/release.noindex/`。PKG 固定安装到 `/Applications` 且禁止重定位,更新 helper 会在重启前再次核对固定路径中的 bundle ID 与目标版本。正式发给用户前必须配置 Apple Developer 签名与公证,并通过 `DESKTOP_PET_UPDATE_PUBLIC_KEYS_JSON` 内嵌 manifest 验签公钥。
|
|
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 协作状态。
|
package/package.json
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "desktop-pet-app",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"description": "AI desktop pet with MCP support and a self-hosted relay server",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"desktop-pet",
|
|
8
|
-
"electron",
|
|
9
|
-
"mcp",
|
|
10
|
-
"ai-agent"
|
|
11
|
-
],
|
|
12
6
|
"engines": {
|
|
13
7
|
"node": ">=20"
|
|
14
8
|
},
|
|
@@ -23,53 +17,6 @@
|
|
|
23
17
|
"bin",
|
|
24
18
|
"out"
|
|
25
19
|
],
|
|
26
|
-
"scripts": {
|
|
27
|
-
"dev": "electron-vite dev",
|
|
28
|
-
"build": "electron-vite build && npm run build:mcp",
|
|
29
|
-
"build:mcp": "esbuild src/mcp-server/index.ts --bundle --platform=node --format=esm --packages=external --outfile=out/mcp-server/index.js && node -e \"require('fs').writeFileSync('out/mcp-server/package.json', JSON.stringify({ type: 'module' }))\"",
|
|
30
|
-
"e2e:duty": "node scripts/e2e-duty-sessions.mjs",
|
|
31
|
-
"e2e:codebuddy:git": "node scripts/e2e-codebuddy-git-task.mjs",
|
|
32
|
-
"e2e:codebuddy:context": "node scripts/e2e-codebuddy-context-round.mjs",
|
|
33
|
-
"typecheck": "tsc --noEmit",
|
|
34
|
-
"test": "vitest run",
|
|
35
|
-
"package:mac": "npm run build && electron-builder --mac pkg --publish never",
|
|
36
|
-
"pack:check": "npm pack --dry-run",
|
|
37
|
-
"prepack": "npm run build",
|
|
38
|
-
"prepublishOnly": "npm run typecheck && npm test && npm run pack:check"
|
|
39
|
-
},
|
|
40
|
-
"build": {
|
|
41
|
-
"appId": "com.xinshu.desktoppet",
|
|
42
|
-
"productName": "Desktop Pet",
|
|
43
|
-
"directories": {
|
|
44
|
-
"output": "release"
|
|
45
|
-
},
|
|
46
|
-
"files": [
|
|
47
|
-
"out/**",
|
|
48
|
-
"bin/**",
|
|
49
|
-
"package.json"
|
|
50
|
-
],
|
|
51
|
-
"pkg": {
|
|
52
|
-
"installLocation": "/Applications",
|
|
53
|
-
"isRelocatable": false,
|
|
54
|
-
"isVersionChecked": true
|
|
55
|
-
},
|
|
56
|
-
"mac": {
|
|
57
|
-
"category": "public.app-category.utilities",
|
|
58
|
-
"target": [
|
|
59
|
-
"pkg"
|
|
60
|
-
]
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
"devDependencies": {
|
|
64
|
-
"@types/node": "^22.0.0",
|
|
65
|
-
"@types/ws": "^8.18.1",
|
|
66
|
-
"electron": "^33.2.0",
|
|
67
|
-
"electron-builder": "^26.15.3",
|
|
68
|
-
"electron-vite": "^2.3.0",
|
|
69
|
-
"typescript": "^5.6.3",
|
|
70
|
-
"vite": "^5.4.8",
|
|
71
|
-
"vitest": "^3.2.7"
|
|
72
|
-
},
|
|
73
20
|
"peerDependencies": {
|
|
74
21
|
"electron": "^33.2.0"
|
|
75
22
|
},
|
|
@@ -78,4 +25,4 @@
|
|
|
78
25
|
"ws": "^8.21.3",
|
|
79
26
|
"zod": "^4.5.4"
|
|
80
27
|
}
|
|
81
|
-
}
|
|
28
|
+
}
|
package/bin/cli.js
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
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
|