desktop-pet-app 0.2.5 → 0.2.9

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
@@ -30,7 +30,7 @@ desktop-pet
30
30
 
31
31
  ## 更新
32
32
 
33
- 通过 npm 安装的版本使用 npm 更新:
33
+ 通过 npm 安装的 macOS 版本可以直接使用应用内更新,首次更新会安装并切换到 `/Applications/Desktop Pet.app`。也可以继续使用 npm 更新:
34
34
 
35
35
  ```bash
36
36
  npm update --global desktop-pet-app
package/bin/cli.js CHANGED
@@ -25,7 +25,8 @@ const appPath = path.join(__dirname, '..')
25
25
 
26
26
  const child = spawn(electron, [appPath, ...process.argv.slice(2)], {
27
27
  stdio: 'ignore',
28
- detached: true
28
+ detached: true,
29
+ env: { ...process.env, DESKTOP_PET_NPM_DISTRIBUTION: '1' }
29
30
  })
30
31
 
31
32
  child.unref()
package/out/main/index.js CHANGED
@@ -23,11 +23,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
  const electron = require("electron");
25
25
  const node_path = require("node:path");
26
+ const node_fs = require("node:fs");
26
27
  const path = require("path");
27
28
  const os = require("os");
28
29
  const node_crypto = require("node:crypto");
29
30
  const node_http = require("node:http");
30
- const node_fs = require("node:fs");
31
31
  const node_os = require("node:os");
32
32
  const crypto = require("crypto");
33
33
  const WebSocket = require("ws");
@@ -66,7 +66,20 @@ function levelFromEnv() {
66
66
  const v = (process.env.DESKTOP_PET_LOG_LEVEL ?? "info").toLowerCase();
67
67
  return v === "debug" || v === "warn" || v === "error" ? v : "info";
68
68
  }
69
- const logger = new Logger(levelFromEnv());
69
+ function mainLogDirectory() {
70
+ return node_path.join(electron.app.getPath("userData"), "logs");
71
+ }
72
+ function writeMainLog(line) {
73
+ console.log(line);
74
+ try {
75
+ const directory = mainLogDirectory();
76
+ node_fs.mkdirSync(directory, { recursive: true });
77
+ node_fs.appendFileSync(node_path.join(directory, "main.log"), `${line}
78
+ `, "utf8");
79
+ } catch {
80
+ }
81
+ }
82
+ const logger = new Logger(levelFromEnv(), writeMainLog);
70
83
  const PORT_FILE = process.env.DESKTOP_PET_PORT_FILE?.trim() || path.join(os.homedir(), ".desktop-pet-port");
71
84
  const LOCAL_PET_PROTOCOL_VERSION = "1.0";
72
85
  const LOCAL_PET_MAX_BODY_BYTES = 64 * 1024;
@@ -268,9 +281,11 @@ class PetManager {
268
281
  pets = /* @__PURE__ */ new Map();
269
282
  lastGreetAt = /* @__PURE__ */ new Map();
270
283
  timer;
284
+ petsVisible = true;
271
285
  register(id, win, skin, identity) {
272
286
  this.pets.set(id, { id, win, skin, identity });
273
287
  win.on("closed", () => this.pets.delete(id));
288
+ if (!this.petsVisible) win.hide();
274
289
  if (!this.timer) this.timer = setInterval(() => this.checkProximity(), PROXIMITY_SCAN_MS);
275
290
  logger.info("pet registered", { id, skin, total: this.pets.size });
276
291
  }
@@ -282,6 +297,17 @@ class PetManager {
282
297
  (pet) => !pet.win.isDestroyed() && pet.identity.kind === "primary"
283
298
  );
284
299
  }
300
+ arePetsVisible() {
301
+ return this.petsVisible;
302
+ }
303
+ setPetsVisible(visible) {
304
+ this.petsVisible = visible;
305
+ for (const pet of this.pets.values()) {
306
+ if (pet.win.isDestroyed()) continue;
307
+ if (visible) pet.win.showInactive();
308
+ else pet.win.hide();
309
+ }
310
+ }
285
311
  findRemoteFriend(username) {
286
312
  const normalized = username.trim().toLowerCase();
287
313
  return [...this.pets.values()].find(
@@ -1653,6 +1679,7 @@ async function fallbackChat(operationId) {
1653
1679
  const REQUEST_TIMEOUT_MS = 6e3;
1654
1680
  const DOWNLOAD_TIMEOUT_MS = 10 * 6e4;
1655
1681
  const LEGACY_OFFICIAL_HTTP_HOST = "47.94.20.104";
1682
+ const MAC_BUNDLE_ID = "com.xinshu.desktoppet";
1656
1683
  let status = { kind: "idle" };
1657
1684
  function updateCheckURL() {
1658
1685
  const override = process.env.DESKTOP_PET_UPDATE_API?.trim();
@@ -1669,6 +1696,7 @@ function getUpdateStatus() {
1669
1696
  return status;
1670
1697
  }
1671
1698
  async function checkForUpdates(currentVersion = electron.app.getVersion()) {
1699
+ if (status.kind === "downloading") return status;
1672
1700
  status = { kind: "checking" };
1673
1701
  try {
1674
1702
  const endpoint = new URL(updateCheckURL());
@@ -1713,7 +1741,7 @@ function updateCheckHeaders(endpoint) {
1713
1741
  async function downloadAvailableUpdate() {
1714
1742
  const available = status.kind === "available" ? status.update : void 0;
1715
1743
  if (!available) return status;
1716
- status = { kind: "downloading", update: available };
1744
+ status = { kind: "downloading", update: available, downloadedBytes: 0 };
1717
1745
  const updateDirectory = node_path.join(electron.app.getPath("userData"), "updates");
1718
1746
  const artifactPath = node_path.join(updateDirectory, `desktop-pet-${available.version}${installerExtension(available.releaseUrl)}`);
1719
1747
  const temporaryPath = `${artifactPath}.${node_crypto.randomUUID()}.download`;
@@ -1730,7 +1758,13 @@ async function downloadAvailableUpdate() {
1730
1758
  if (response.url && new URL(response.url).protocol !== "https:") {
1731
1759
  throw new Error("更新下载地址必须使用 HTTPS");
1732
1760
  }
1733
- const digest = await saveAndHash(response.body, temporaryPath);
1761
+ const totalBytes = parseContentLength(response.headers.get("content-length"));
1762
+ let downloadedBytes = 0;
1763
+ status = { kind: "downloading", update: available, downloadedBytes, totalBytes };
1764
+ const digest = await saveAndHash(response.body, temporaryPath, (chunkBytes) => {
1765
+ downloadedBytes += chunkBytes;
1766
+ status = { kind: "downloading", update: available, downloadedBytes, totalBytes };
1767
+ });
1734
1768
  if (digest !== available.sha256) throw new Error("更新包校验失败,文件未安装");
1735
1769
  await promises.rm(artifactPath, { force: true });
1736
1770
  await moveFile(temporaryPath, artifactPath);
@@ -1747,7 +1781,7 @@ async function downloadAvailableUpdate() {
1747
1781
  }
1748
1782
  async function installDownloadedUpdate() {
1749
1783
  if (status.kind !== "downloaded") return void 0;
1750
- if (!electron.app.isPackaged) throw new Error("仅打包后的宠物应用支持安装更新");
1784
+ if (!supportsInAppInstall()) throw new Error("当前运行方式不支持应用内安装,请使用正式发布版");
1751
1785
  if (process.platform !== "darwin" && process.platform !== "win32") {
1752
1786
  throw new Error("当前平台暂不支持应用内安装,请下载对应安装包更新");
1753
1787
  }
@@ -1759,7 +1793,8 @@ async function installDownloadedUpdate() {
1759
1793
  downloaded.artifactPath,
1760
1794
  downloaded.update.sha256,
1761
1795
  process.platform,
1762
- process.execPath
1796
+ process.execPath,
1797
+ JSON.stringify(restartArgumentsAfterUpdate())
1763
1798
  ], {
1764
1799
  detached: true,
1765
1800
  stdio: "ignore",
@@ -1771,11 +1806,18 @@ async function installDownloadedUpdate() {
1771
1806
  electron.app.quit();
1772
1807
  return downloaded.update;
1773
1808
  }
1774
- async function saveAndHash(body, destination) {
1809
+ function supportsInAppInstall() {
1810
+ return electron.app.isPackaged || process.env.DESKTOP_PET_NPM_DISTRIBUTION === "1";
1811
+ }
1812
+ function restartArgumentsAfterUpdate() {
1813
+ return !electron.app.isPackaged && process.argv[1] ? [process.argv[1]] : [];
1814
+ }
1815
+ async function saveAndHash(body, destination, onChunk) {
1775
1816
  const hash = node_crypto.createHash("sha256");
1776
1817
  const meter = new node_stream.Transform({
1777
1818
  transform(chunk, _encoding, done) {
1778
1819
  hash.update(chunk);
1820
+ onChunk(chunk.length);
1779
1821
  done(null, chunk);
1780
1822
  }
1781
1823
  });
@@ -1786,6 +1828,23 @@ async function saveAndHash(body, destination) {
1786
1828
  );
1787
1829
  return hash.digest("hex");
1788
1830
  }
1831
+ function parseContentLength(value) {
1832
+ if (!value) return void 0;
1833
+ const bytes = Number(value);
1834
+ return Number.isSafeInteger(bytes) && bytes > 0 ? bytes : void 0;
1835
+ }
1836
+ function downloadProgressText(progress) {
1837
+ if (progress.totalBytes) {
1838
+ const percent = Math.min(100, Math.floor(progress.downloadedBytes / progress.totalBytes * 100));
1839
+ return `${percent}%`;
1840
+ }
1841
+ return formatBytes(progress.downloadedBytes);
1842
+ }
1843
+ function formatBytes(bytes) {
1844
+ if (bytes < 1024) return `${bytes} B`;
1845
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1846
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1847
+ }
1789
1848
  async function moveFile(source, destination) {
1790
1849
  const { rename } = await import("node:fs/promises");
1791
1850
  await rename(source, destination);
@@ -1802,8 +1861,9 @@ const { dirname, join } = require('node:path')
1802
1861
  const { execFile, spawn } = require('node:child_process')
1803
1862
  const { promisify } = require('node:util')
1804
1863
 
1805
- const [, , parentPidText, sourcePath, expectedSHA256, platform, appExecutable] = process.argv
1864
+ const [, , parentPidText, sourcePath, expectedSHA256, platform, fallbackExecutable, fallbackArgsJSON] = process.argv
1806
1865
  const parentPid = Number(parentPidText)
1866
+ const fallbackArgs = JSON.parse(fallbackArgsJSON || '[]')
1807
1867
  const run = promisify(execFile)
1808
1868
 
1809
1869
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
@@ -1817,7 +1877,22 @@ const sha256 = (path) => new Promise((resolve, reject) => {
1817
1877
  input.on('error', reject)
1818
1878
  input.on('end', () => resolve(hash.digest('hex')))
1819
1879
  })
1820
- const restart = () => spawn(appExecutable, [], { detached: true, stdio: 'ignore' }).unref()
1880
+ const restartFallback = () => {
1881
+ const env = { ...process.env }
1882
+ delete env.ELECTRON_RUN_AS_NODE
1883
+ const child = spawn(fallbackExecutable, fallbackArgs, { detached: true, stdio: 'ignore', env })
1884
+ child.on('error', () => undefined)
1885
+ child.unref()
1886
+ }
1887
+ const restartInstalledApp = async () => {
1888
+ // 安装器可能会保留已有应用的位置(开发构建尤其如此),不能假设它在 /Applications。
1889
+ // 让 LaunchServices 按 bundle ID 查找刚安装的实际应用位置。
1890
+ if (platform === 'darwin') {
1891
+ await run('/usr/bin/open', ['-b', '${MAC_BUNDLE_ID}'])
1892
+ return
1893
+ }
1894
+ restartFallback()
1895
+ }
1821
1896
  const shellQuote = (value) => "'" + value.replace(/'/g, "'\\''") + "'"
1822
1897
 
1823
1898
  async function install() {
@@ -1837,17 +1912,28 @@ async function install() {
1837
1912
  async function main() {
1838
1913
  while (parentIsRunning()) await sleep(200)
1839
1914
 
1915
+ let installed = false
1840
1916
  try {
1841
1917
  if (await sha256(sourcePath) !== expectedSHA256) throw new Error('downloaded update hash changed')
1842
1918
  await install()
1919
+ installed = true
1843
1920
  } catch (error) {
1844
1921
  await fs.writeFile(join(dirname(sourcePath), 'last-install-error.log'), String(error)).catch(() => undefined)
1845
1922
  } finally {
1846
- restart()
1923
+ if (installed) {
1924
+ try {
1925
+ await restartInstalledApp()
1926
+ } catch (error) {
1927
+ await fs.writeFile(join(dirname(sourcePath), 'last-restart-error.log'), String(error)).catch(() => undefined)
1928
+ restartFallback()
1929
+ }
1930
+ } else {
1931
+ restartFallback()
1932
+ }
1847
1933
  }
1848
1934
  }
1849
1935
 
1850
- main().catch(() => restart())
1936
+ main().catch(() => restartFallback())
1851
1937
  `;
1852
1938
  function installerExtension(releaseUrl) {
1853
1939
  const pathname = new URL(releaseUrl).pathname.toLowerCase();
@@ -1957,6 +2043,7 @@ function createPetWindow(skin, options) {
1957
2043
  const x = options.near?.x ?? workArea.x + workArea.width - W - 120;
1958
2044
  const y = options.near?.y ?? workArea.y + workArea.height - H - 60;
1959
2045
  const win = new electron.BrowserWindow({
2046
+ show: petManager.arePetsVisible(),
1960
2047
  width: W,
1961
2048
  height: H,
1962
2049
  x,
@@ -2090,11 +2177,20 @@ function createPetWindow(skin, options) {
2090
2177
  dispatch({ type: "bubble", text: result.message, ttl: 6e3 }, id);
2091
2178
  }
2092
2179
  },
2180
+ {
2181
+ label: "打开日志文件夹",
2182
+ click: () => {
2183
+ void electron.shell.openPath(mainLogDirectory()).then((error) => {
2184
+ if (error) dispatch({ type: "bubble", text: "无法打开日志文件夹", ttl: 6e3 }, id);
2185
+ });
2186
+ }
2187
+ },
2093
2188
  {
2094
2189
  label: updateMenuLabel(),
2095
2190
  submenu: updateMenuItems(id)
2096
2191
  },
2097
2192
  { type: "separator" },
2193
+ { label: "隐藏所有桌面宠物", click: () => petManager.setPetsVisible(false) },
2098
2194
  ...canSendAwayPet(identity) ? [{ label: "送走这只宠物", click: () => win.close() }] : [],
2099
2195
  { label: "退出", click: () => electron.app.quit() }
2100
2196
  ]);
@@ -2115,7 +2211,9 @@ function createPetWindow(skin, options) {
2115
2211
  function updateMenuLabel() {
2116
2212
  const status2 = getUpdateStatus();
2117
2213
  if (status2.kind === "available") return `软件更新(v${status2.update.version} 可下载)`;
2118
- if (status2.kind === "downloading") return `软件更新(正在下载 v${status2.update.version})`;
2214
+ if (status2.kind === "downloading") {
2215
+ return `软件更新(v${status2.update.version} 下载 ${downloadProgressText(status2)})`;
2216
+ }
2119
2217
  if (status2.kind === "downloaded") return `软件更新(v${status2.update.version} 已下载)`;
2120
2218
  return "软件更新";
2121
2219
  }
@@ -2131,6 +2229,12 @@ function updateMenuItems(petId) {
2131
2229
  dispatch({ type: "bubble", text: `发现 v${result.update.version},可从“软件更新”安装`, ttl: 6e3 }, petId);
2132
2230
  } else if (result.kind === "up-to-date") {
2133
2231
  dispatch({ type: "bubble", text: "已经是最新版本啦", ttl: 4500 }, petId);
2232
+ } else if (result.kind === "downloading") {
2233
+ dispatch({
2234
+ type: "bubble",
2235
+ text: `v${result.update.version} 正在下载:${downloadProgressText(result)}`,
2236
+ ttl: 4500
2237
+ }, petId);
2134
2238
  } else if (result.kind === "error") {
2135
2239
  dispatch({ type: "bubble", text: result.message, ttl: 7e3 }, petId);
2136
2240
  }
@@ -2154,7 +2258,10 @@ function updateMenuItems(petId) {
2154
2258
  });
2155
2259
  }
2156
2260
  if (status2.kind === "downloading") {
2157
- items.splice(1, 0, { label: `正在下载 v${status2.update.version}…`, enabled: false });
2261
+ items.splice(1, 0, {
2262
+ label: `正在下载 v${status2.update.version} · ${downloadProgressText(status2)}`,
2263
+ enabled: false
2264
+ });
2158
2265
  }
2159
2266
  if (status2.kind === "downloaded") {
2160
2267
  items.splice(1, 0, {
@@ -2993,6 +3100,39 @@ async function dutyLoop(adapter) {
2993
3100
  }
2994
3101
  }
2995
3102
  }
3103
+ const trayIcon1xPath = path.join(__dirname, "./chunks/tray-iconTemplate-BIK86F-0.png");
3104
+ const trayIcon2xPath = path.join(__dirname, "./chunks/tray-iconTemplate@2x-B19HpcaE.png");
3105
+ let tray;
3106
+ function createMenuBar() {
3107
+ if (process.platform !== "darwin" || tray) return;
3108
+ tray = new electron.Tray(createPetTemplateImage());
3109
+ tray.setToolTip("Desktop Pet");
3110
+ tray.on("click", showMenu);
3111
+ tray.on("right-click", showMenu);
3112
+ }
3113
+ function destroyMenuBar() {
3114
+ tray?.destroy();
3115
+ tray = void 0;
3116
+ }
3117
+ function showMenu() {
3118
+ tray?.popUpContextMenu(electron.Menu.buildFromTemplate([
3119
+ { label: `Desktop Pet v${electron.app.getVersion()}`, enabled: false },
3120
+ { type: "separator" },
3121
+ {
3122
+ label: petManager.arePetsVisible() ? "隐藏所有桌面宠物" : "显示所有桌面宠物",
3123
+ click: () => petManager.setPetsVisible(!petManager.arePetsVisible())
3124
+ },
3125
+ { type: "separator" },
3126
+ { label: "退出 Desktop Pet", click: () => electron.app.quit() }
3127
+ ]));
3128
+ }
3129
+ function createPetTemplateImage() {
3130
+ let image = electron.nativeImage.createFromBuffer(node_fs.readFileSync(trayIcon2xPath), { scaleFactor: 2 });
3131
+ if (image.isEmpty()) image = electron.nativeImage.createFromPath(trayIcon1xPath);
3132
+ if (image.isEmpty()) throw new Error("无法加载 macOS 菜单栏图标");
3133
+ image.setTemplateImage(true);
3134
+ return image;
3135
+ }
2996
3136
  const profileDirectory = process.env.DESKTOP_PET_PROFILE?.trim();
2997
3137
  if (profileDirectory) {
2998
3138
  electron.app.setPath("userData", node_path.resolve(profileDirectory));
@@ -3005,6 +3145,7 @@ if (!singleInstance) {
3005
3145
  electron.app.on("second-instance", (_event, argv) => {
3006
3146
  const callback = argv.find((value) => value.startsWith("desktop-pet://"));
3007
3147
  if (callback) handleAccountCallback(callback);
3148
+ petManager.setPetsVisible(true);
3008
3149
  ensurePrimaryPet();
3009
3150
  });
3010
3151
  }
@@ -3015,6 +3156,7 @@ function ensurePrimaryPet() {
3015
3156
  });
3016
3157
  }
3017
3158
  electron.app.whenReady().then(async () => {
3159
+ createMenuBar();
3018
3160
  await startAccountCallbackRouter();
3019
3161
  startEventServer();
3020
3162
  const activated = await activateStoredDevice().catch((err) => {
@@ -3039,9 +3181,10 @@ electron.app.whenReady().then(async () => {
3039
3181
  logger.info("app started", { firstPet: id });
3040
3182
  });
3041
3183
  electron.app.on("window-all-closed", () => {
3042
- electron.app.quit();
3184
+ if (process.platform !== "darwin") electron.app.quit();
3043
3185
  });
3044
3186
  electron.app.on("quit", () => {
3187
+ destroyMenuBar();
3045
3188
  stopAccountCallbackRouter();
3046
3189
  stopRelay();
3047
3190
  stopP2PWindow();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "desktop-pet-app",
3
- "version": "0.2.5",
3
+ "version": "0.2.9",
4
4
  "description": "AI desktop pet with MCP support and a self-hosted relay server",
5
5
  "license": "MIT",
6
6
  "keywords": [