desktop-pet-app 0.2.4 → 0.2.7

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
@@ -246,6 +246,9 @@ function sleep(ms) {
246
246
  function isPetOnQuest(petId) {
247
247
  return busy.has(petId);
248
248
  }
249
+ function canSendAwayPet(identity) {
250
+ return identity.kind === "remote-friend";
251
+ }
249
252
  function presentPetIdentity(identity, localUsername) {
250
253
  if (identity.kind === "remote-friend") {
251
254
  return {
@@ -274,6 +277,11 @@ class PetManager {
274
277
  count() {
275
278
  return this.pets.size;
276
279
  }
280
+ hasPrimaryPet() {
281
+ return [...this.pets.values()].some(
282
+ (pet) => !pet.win.isDestroyed() && pet.identity.kind === "primary"
283
+ );
284
+ }
277
285
  findRemoteFriend(username) {
278
286
  const normalized = username.trim().toLowerCase();
279
287
  return [...this.pets.values()].find(
@@ -1645,6 +1653,7 @@ async function fallbackChat(operationId) {
1645
1653
  const REQUEST_TIMEOUT_MS = 6e3;
1646
1654
  const DOWNLOAD_TIMEOUT_MS = 10 * 6e4;
1647
1655
  const LEGACY_OFFICIAL_HTTP_HOST = "47.94.20.104";
1656
+ const MAC_INSTALLED_APP_EXECUTABLE = "/Applications/Desktop Pet.app/Contents/MacOS/Desktop Pet";
1648
1657
  let status = { kind: "idle" };
1649
1658
  function updateCheckURL() {
1650
1659
  const override = process.env.DESKTOP_PET_UPDATE_API?.trim();
@@ -1661,6 +1670,7 @@ function getUpdateStatus() {
1661
1670
  return status;
1662
1671
  }
1663
1672
  async function checkForUpdates(currentVersion = electron.app.getVersion()) {
1673
+ if (status.kind === "downloading") return status;
1664
1674
  status = { kind: "checking" };
1665
1675
  try {
1666
1676
  const endpoint = new URL(updateCheckURL());
@@ -1705,7 +1715,7 @@ function updateCheckHeaders(endpoint) {
1705
1715
  async function downloadAvailableUpdate() {
1706
1716
  const available = status.kind === "available" ? status.update : void 0;
1707
1717
  if (!available) return status;
1708
- status = { kind: "downloading", update: available };
1718
+ status = { kind: "downloading", update: available, downloadedBytes: 0 };
1709
1719
  const updateDirectory = node_path.join(electron.app.getPath("userData"), "updates");
1710
1720
  const artifactPath = node_path.join(updateDirectory, `desktop-pet-${available.version}${installerExtension(available.releaseUrl)}`);
1711
1721
  const temporaryPath = `${artifactPath}.${node_crypto.randomUUID()}.download`;
@@ -1722,7 +1732,13 @@ async function downloadAvailableUpdate() {
1722
1732
  if (response.url && new URL(response.url).protocol !== "https:") {
1723
1733
  throw new Error("更新下载地址必须使用 HTTPS");
1724
1734
  }
1725
- const digest = await saveAndHash(response.body, temporaryPath);
1735
+ const totalBytes = parseContentLength(response.headers.get("content-length"));
1736
+ let downloadedBytes = 0;
1737
+ status = { kind: "downloading", update: available, downloadedBytes, totalBytes };
1738
+ const digest = await saveAndHash(response.body, temporaryPath, (chunkBytes) => {
1739
+ downloadedBytes += chunkBytes;
1740
+ status = { kind: "downloading", update: available, downloadedBytes, totalBytes };
1741
+ });
1726
1742
  if (digest !== available.sha256) throw new Error("更新包校验失败,文件未安装");
1727
1743
  await promises.rm(artifactPath, { force: true });
1728
1744
  await moveFile(temporaryPath, artifactPath);
@@ -1739,7 +1755,7 @@ async function downloadAvailableUpdate() {
1739
1755
  }
1740
1756
  async function installDownloadedUpdate() {
1741
1757
  if (status.kind !== "downloaded") return void 0;
1742
- if (!electron.app.isPackaged) throw new Error("仅打包后的宠物应用支持安装更新");
1758
+ if (!supportsInAppInstall()) throw new Error("当前运行方式不支持应用内安装,请使用正式发布版");
1743
1759
  if (process.platform !== "darwin" && process.platform !== "win32") {
1744
1760
  throw new Error("当前平台暂不支持应用内安装,请下载对应安装包更新");
1745
1761
  }
@@ -1751,6 +1767,7 @@ async function installDownloadedUpdate() {
1751
1767
  downloaded.artifactPath,
1752
1768
  downloaded.update.sha256,
1753
1769
  process.platform,
1770
+ restartExecutableAfterUpdate(),
1754
1771
  process.execPath
1755
1772
  ], {
1756
1773
  detached: true,
@@ -1763,11 +1780,21 @@ async function installDownloadedUpdate() {
1763
1780
  electron.app.quit();
1764
1781
  return downloaded.update;
1765
1782
  }
1766
- async function saveAndHash(body, destination) {
1783
+ function supportsInAppInstall() {
1784
+ return electron.app.isPackaged || process.env.DESKTOP_PET_NPM_DISTRIBUTION === "1";
1785
+ }
1786
+ function restartExecutableAfterUpdate() {
1787
+ if (!electron.app.isPackaged && process.env.DESKTOP_PET_NPM_DISTRIBUTION === "1" && process.platform === "darwin") {
1788
+ return MAC_INSTALLED_APP_EXECUTABLE;
1789
+ }
1790
+ return process.execPath;
1791
+ }
1792
+ async function saveAndHash(body, destination, onChunk) {
1767
1793
  const hash = node_crypto.createHash("sha256");
1768
1794
  const meter = new node_stream.Transform({
1769
1795
  transform(chunk, _encoding, done) {
1770
1796
  hash.update(chunk);
1797
+ onChunk(chunk.length);
1771
1798
  done(null, chunk);
1772
1799
  }
1773
1800
  });
@@ -1778,6 +1805,23 @@ async function saveAndHash(body, destination) {
1778
1805
  );
1779
1806
  return hash.digest("hex");
1780
1807
  }
1808
+ function parseContentLength(value) {
1809
+ if (!value) return void 0;
1810
+ const bytes = Number(value);
1811
+ return Number.isSafeInteger(bytes) && bytes > 0 ? bytes : void 0;
1812
+ }
1813
+ function downloadProgressText(progress) {
1814
+ if (progress.totalBytes) {
1815
+ const percent = Math.min(100, Math.floor(progress.downloadedBytes / progress.totalBytes * 100));
1816
+ return `${percent}%`;
1817
+ }
1818
+ return formatBytes(progress.downloadedBytes);
1819
+ }
1820
+ function formatBytes(bytes) {
1821
+ if (bytes < 1024) return `${bytes} B`;
1822
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1823
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1824
+ }
1781
1825
  async function moveFile(source, destination) {
1782
1826
  const { rename } = await import("node:fs/promises");
1783
1827
  await rename(source, destination);
@@ -1794,7 +1838,7 @@ const { dirname, join } = require('node:path')
1794
1838
  const { execFile, spawn } = require('node:child_process')
1795
1839
  const { promisify } = require('node:util')
1796
1840
 
1797
- const [, , parentPidText, sourcePath, expectedSHA256, platform, appExecutable] = process.argv
1841
+ const [, , parentPidText, sourcePath, expectedSHA256, platform, installedAppExecutable, fallbackExecutable] = process.argv
1798
1842
  const parentPid = Number(parentPidText)
1799
1843
  const run = promisify(execFile)
1800
1844
 
@@ -1809,7 +1853,11 @@ const sha256 = (path) => new Promise((resolve, reject) => {
1809
1853
  input.on('error', reject)
1810
1854
  input.on('end', () => resolve(hash.digest('hex')))
1811
1855
  })
1812
- const restart = () => spawn(appExecutable, [], { detached: true, stdio: 'ignore' }).unref()
1856
+ const restart = (executable) => {
1857
+ const env = { ...process.env }
1858
+ delete env.ELECTRON_RUN_AS_NODE
1859
+ spawn(executable, [], { detached: true, stdio: 'ignore', env }).unref()
1860
+ }
1813
1861
  const shellQuote = (value) => "'" + value.replace(/'/g, "'\\''") + "'"
1814
1862
 
1815
1863
  async function install() {
@@ -1829,17 +1877,19 @@ async function install() {
1829
1877
  async function main() {
1830
1878
  while (parentIsRunning()) await sleep(200)
1831
1879
 
1880
+ let installed = false
1832
1881
  try {
1833
1882
  if (await sha256(sourcePath) !== expectedSHA256) throw new Error('downloaded update hash changed')
1834
1883
  await install()
1884
+ installed = true
1835
1885
  } catch (error) {
1836
1886
  await fs.writeFile(join(dirname(sourcePath), 'last-install-error.log'), String(error)).catch(() => undefined)
1837
1887
  } finally {
1838
- restart()
1888
+ restart(installed ? installedAppExecutable : fallbackExecutable)
1839
1889
  }
1840
1890
  }
1841
1891
 
1842
- main().catch(() => restart())
1892
+ main().catch(() => restart(fallbackExecutable))
1843
1893
  `;
1844
1894
  function installerExtension(releaseUrl) {
1845
1895
  const pathname = new URL(releaseUrl).pathname.toLowerCase();
@@ -2087,7 +2137,7 @@ function createPetWindow(skin, options) {
2087
2137
  submenu: updateMenuItems(id)
2088
2138
  },
2089
2139
  { type: "separator" },
2090
- { label: "送走这只宠物", click: () => win.close() },
2140
+ ...canSendAwayPet(identity) ? [{ label: "送走这只宠物", click: () => win.close() }] : [],
2091
2141
  { label: "退出", click: () => electron.app.quit() }
2092
2142
  ]);
2093
2143
  menu.popup({ window: win });
@@ -2107,7 +2157,9 @@ function createPetWindow(skin, options) {
2107
2157
  function updateMenuLabel() {
2108
2158
  const status2 = getUpdateStatus();
2109
2159
  if (status2.kind === "available") return `软件更新(v${status2.update.version} 可下载)`;
2110
- if (status2.kind === "downloading") return `软件更新(正在下载 v${status2.update.version})`;
2160
+ if (status2.kind === "downloading") {
2161
+ return `软件更新(v${status2.update.version} 下载 ${downloadProgressText(status2)})`;
2162
+ }
2111
2163
  if (status2.kind === "downloaded") return `软件更新(v${status2.update.version} 已下载)`;
2112
2164
  return "软件更新";
2113
2165
  }
@@ -2123,6 +2175,12 @@ function updateMenuItems(petId) {
2123
2175
  dispatch({ type: "bubble", text: `发现 v${result.update.version},可从“软件更新”安装`, ttl: 6e3 }, petId);
2124
2176
  } else if (result.kind === "up-to-date") {
2125
2177
  dispatch({ type: "bubble", text: "已经是最新版本啦", ttl: 4500 }, petId);
2178
+ } else if (result.kind === "downloading") {
2179
+ dispatch({
2180
+ type: "bubble",
2181
+ text: `v${result.update.version} 正在下载:${downloadProgressText(result)}`,
2182
+ ttl: 4500
2183
+ }, petId);
2126
2184
  } else if (result.kind === "error") {
2127
2185
  dispatch({ type: "bubble", text: result.message, ttl: 7e3 }, petId);
2128
2186
  }
@@ -2146,7 +2204,10 @@ function updateMenuItems(petId) {
2146
2204
  });
2147
2205
  }
2148
2206
  if (status2.kind === "downloading") {
2149
- items.splice(1, 0, { label: `正在下载 v${status2.update.version}…`, enabled: false });
2207
+ items.splice(1, 0, {
2208
+ label: `正在下载 v${status2.update.version} · ${downloadProgressText(status2)}`,
2209
+ enabled: false
2210
+ });
2150
2211
  }
2151
2212
  if (status2.kind === "downloaded") {
2152
2213
  items.splice(1, 0, {
@@ -2997,6 +3058,13 @@ if (!singleInstance) {
2997
3058
  electron.app.on("second-instance", (_event, argv) => {
2998
3059
  const callback = argv.find((value) => value.startsWith("desktop-pet://"));
2999
3060
  if (callback) handleAccountCallback(callback);
3061
+ ensurePrimaryPet();
3062
+ });
3063
+ }
3064
+ function ensurePrimaryPet() {
3065
+ if (petManager.hasPrimaryPet()) return null;
3066
+ return createPetWindow(process.env.DESKTOP_PET_SKIN || "codex", {
3067
+ identity: { kind: "primary" }
3000
3068
  });
3001
3069
  }
3002
3070
  electron.app.whenReady().then(async () => {
@@ -3009,9 +3077,7 @@ electron.app.whenReady().then(async () => {
3009
3077
  if (!activated) startRelay();
3010
3078
  if (process.env.DESKTOP_PET_P2P !== "0") startP2PWindow(getDeviceIceServers());
3011
3079
  void startRunnerManager();
3012
- const id = createPetWindow(process.env.DESKTOP_PET_SKIN || "codex", {
3013
- identity: { kind: "primary" }
3014
- });
3080
+ const id = ensurePrimaryPet();
3015
3081
  setTimeout(() => {
3016
3082
  void checkForUpdates().then((result) => {
3017
3083
  if (id && result.kind === "available") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "desktop-pet-app",
3
- "version": "0.2.4",
3
+ "version": "0.2.7",
4
4
  "description": "AI desktop pet with MCP support and a self-hosted relay server",
5
5
  "license": "MIT",
6
6
  "keywords": [