desktop-pet-app 0.2.5 → 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
@@ -1653,6 +1653,7 @@ async function fallbackChat(operationId) {
1653
1653
  const REQUEST_TIMEOUT_MS = 6e3;
1654
1654
  const DOWNLOAD_TIMEOUT_MS = 10 * 6e4;
1655
1655
  const LEGACY_OFFICIAL_HTTP_HOST = "47.94.20.104";
1656
+ const MAC_INSTALLED_APP_EXECUTABLE = "/Applications/Desktop Pet.app/Contents/MacOS/Desktop Pet";
1656
1657
  let status = { kind: "idle" };
1657
1658
  function updateCheckURL() {
1658
1659
  const override = process.env.DESKTOP_PET_UPDATE_API?.trim();
@@ -1669,6 +1670,7 @@ function getUpdateStatus() {
1669
1670
  return status;
1670
1671
  }
1671
1672
  async function checkForUpdates(currentVersion = electron.app.getVersion()) {
1673
+ if (status.kind === "downloading") return status;
1672
1674
  status = { kind: "checking" };
1673
1675
  try {
1674
1676
  const endpoint = new URL(updateCheckURL());
@@ -1713,7 +1715,7 @@ function updateCheckHeaders(endpoint) {
1713
1715
  async function downloadAvailableUpdate() {
1714
1716
  const available = status.kind === "available" ? status.update : void 0;
1715
1717
  if (!available) return status;
1716
- status = { kind: "downloading", update: available };
1718
+ status = { kind: "downloading", update: available, downloadedBytes: 0 };
1717
1719
  const updateDirectory = node_path.join(electron.app.getPath("userData"), "updates");
1718
1720
  const artifactPath = node_path.join(updateDirectory, `desktop-pet-${available.version}${installerExtension(available.releaseUrl)}`);
1719
1721
  const temporaryPath = `${artifactPath}.${node_crypto.randomUUID()}.download`;
@@ -1730,7 +1732,13 @@ async function downloadAvailableUpdate() {
1730
1732
  if (response.url && new URL(response.url).protocol !== "https:") {
1731
1733
  throw new Error("更新下载地址必须使用 HTTPS");
1732
1734
  }
1733
- 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
+ });
1734
1742
  if (digest !== available.sha256) throw new Error("更新包校验失败,文件未安装");
1735
1743
  await promises.rm(artifactPath, { force: true });
1736
1744
  await moveFile(temporaryPath, artifactPath);
@@ -1747,7 +1755,7 @@ async function downloadAvailableUpdate() {
1747
1755
  }
1748
1756
  async function installDownloadedUpdate() {
1749
1757
  if (status.kind !== "downloaded") return void 0;
1750
- if (!electron.app.isPackaged) throw new Error("仅打包后的宠物应用支持安装更新");
1758
+ if (!supportsInAppInstall()) throw new Error("当前运行方式不支持应用内安装,请使用正式发布版");
1751
1759
  if (process.platform !== "darwin" && process.platform !== "win32") {
1752
1760
  throw new Error("当前平台暂不支持应用内安装,请下载对应安装包更新");
1753
1761
  }
@@ -1759,6 +1767,7 @@ async function installDownloadedUpdate() {
1759
1767
  downloaded.artifactPath,
1760
1768
  downloaded.update.sha256,
1761
1769
  process.platform,
1770
+ restartExecutableAfterUpdate(),
1762
1771
  process.execPath
1763
1772
  ], {
1764
1773
  detached: true,
@@ -1771,11 +1780,21 @@ async function installDownloadedUpdate() {
1771
1780
  electron.app.quit();
1772
1781
  return downloaded.update;
1773
1782
  }
1774
- 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) {
1775
1793
  const hash = node_crypto.createHash("sha256");
1776
1794
  const meter = new node_stream.Transform({
1777
1795
  transform(chunk, _encoding, done) {
1778
1796
  hash.update(chunk);
1797
+ onChunk(chunk.length);
1779
1798
  done(null, chunk);
1780
1799
  }
1781
1800
  });
@@ -1786,6 +1805,23 @@ async function saveAndHash(body, destination) {
1786
1805
  );
1787
1806
  return hash.digest("hex");
1788
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
+ }
1789
1825
  async function moveFile(source, destination) {
1790
1826
  const { rename } = await import("node:fs/promises");
1791
1827
  await rename(source, destination);
@@ -1802,7 +1838,7 @@ const { dirname, join } = require('node:path')
1802
1838
  const { execFile, spawn } = require('node:child_process')
1803
1839
  const { promisify } = require('node:util')
1804
1840
 
1805
- const [, , parentPidText, sourcePath, expectedSHA256, platform, appExecutable] = process.argv
1841
+ const [, , parentPidText, sourcePath, expectedSHA256, platform, installedAppExecutable, fallbackExecutable] = process.argv
1806
1842
  const parentPid = Number(parentPidText)
1807
1843
  const run = promisify(execFile)
1808
1844
 
@@ -1817,7 +1853,11 @@ const sha256 = (path) => new Promise((resolve, reject) => {
1817
1853
  input.on('error', reject)
1818
1854
  input.on('end', () => resolve(hash.digest('hex')))
1819
1855
  })
1820
- 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
+ }
1821
1861
  const shellQuote = (value) => "'" + value.replace(/'/g, "'\\''") + "'"
1822
1862
 
1823
1863
  async function install() {
@@ -1837,17 +1877,19 @@ async function install() {
1837
1877
  async function main() {
1838
1878
  while (parentIsRunning()) await sleep(200)
1839
1879
 
1880
+ let installed = false
1840
1881
  try {
1841
1882
  if (await sha256(sourcePath) !== expectedSHA256) throw new Error('downloaded update hash changed')
1842
1883
  await install()
1884
+ installed = true
1843
1885
  } catch (error) {
1844
1886
  await fs.writeFile(join(dirname(sourcePath), 'last-install-error.log'), String(error)).catch(() => undefined)
1845
1887
  } finally {
1846
- restart()
1888
+ restart(installed ? installedAppExecutable : fallbackExecutable)
1847
1889
  }
1848
1890
  }
1849
1891
 
1850
- main().catch(() => restart())
1892
+ main().catch(() => restart(fallbackExecutable))
1851
1893
  `;
1852
1894
  function installerExtension(releaseUrl) {
1853
1895
  const pathname = new URL(releaseUrl).pathname.toLowerCase();
@@ -2115,7 +2157,9 @@ function createPetWindow(skin, options) {
2115
2157
  function updateMenuLabel() {
2116
2158
  const status2 = getUpdateStatus();
2117
2159
  if (status2.kind === "available") return `软件更新(v${status2.update.version} 可下载)`;
2118
- if (status2.kind === "downloading") return `软件更新(正在下载 v${status2.update.version})`;
2160
+ if (status2.kind === "downloading") {
2161
+ return `软件更新(v${status2.update.version} 下载 ${downloadProgressText(status2)})`;
2162
+ }
2119
2163
  if (status2.kind === "downloaded") return `软件更新(v${status2.update.version} 已下载)`;
2120
2164
  return "软件更新";
2121
2165
  }
@@ -2131,6 +2175,12 @@ function updateMenuItems(petId) {
2131
2175
  dispatch({ type: "bubble", text: `发现 v${result.update.version},可从“软件更新”安装`, ttl: 6e3 }, petId);
2132
2176
  } else if (result.kind === "up-to-date") {
2133
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);
2134
2184
  } else if (result.kind === "error") {
2135
2185
  dispatch({ type: "bubble", text: result.message, ttl: 7e3 }, petId);
2136
2186
  }
@@ -2154,7 +2204,10 @@ function updateMenuItems(petId) {
2154
2204
  });
2155
2205
  }
2156
2206
  if (status2.kind === "downloading") {
2157
- 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
+ });
2158
2211
  }
2159
2212
  if (status2.kind === "downloaded") {
2160
2213
  items.splice(1, 0, {
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.7",
4
4
  "description": "AI desktop pet with MCP support and a self-hosted relay server",
5
5
  "license": "MIT",
6
6
  "keywords": [