Преглед изворни кода

客户端沿用历史 App 标识,完整版改用独立身份并完善安装说明

- 客户端 appId 恢复 cn.samien.yaoyao.desktop、工件名恢复 Yaoyao-*,旧版「夭夭」升级原地替换、保留钥匙串与登录数据
- 完整版(双模式)改 appId cn.samien.yaoyao.desktop.full、安装名「夭夭完整版」,userData/日志/单实例锁与客户端隔离,可并存
- 原生助手签名白名单同步两个标识;main.mjs 按构建变体的 productName 设置应用身份
- 新增 docs/npm-install.md;README 增补 npm 安装一节并区分两种桌面形态;macos-app.md 标注为完整版文档
- 已验证:客户端重打包 appId 正确、钥匙串往返通过、19 项回归测试全过
Samien пре 1 месец
родитељ
комит
75a79092b7

+ 18 - 2
README.md

@@ -40,9 +40,25 @@ Web 数据默认保存在 `~/.yaoyao`,可用 `YAOYAO_HOME` 指定(兼容 `HE
 
 ## macOS 桌面版
 
-在 [v0.4.75 发布页](https://github.com/Lsamien/yaoyao/releases/tag/v0.4.75) 下载 `Yaoyao-0.4.75-arm64.dmg`,将“夭夭”拖到应用程序后打开。首次启动会核对并同步配套本机 Web 服务;普通退出保留后台运行,“停止后台服务并退出”可完整停止本应用管理的后台。详细说明见 [macOS App](docs/macos-app.md)。
+桌面版分两种,安装包均为 Apple Silicon(arm64):
 
-桌面安装包的签名、公证状态及自动升级可用范围见对应版本的 GitHub 发布说明。
+- **夭夭(客户端)**:连接远程服务器使用,本机不运行 Web 服务。下载 `Yaoyao-<版本>-arm64.dmg`,拖到应用程序后打开,首次启动填写服务器地址并登录。适合服务器已部署(Docker 或 npm)的场景,也是此后主推的桌面形态。
+- **夭夭完整版**:内嵌本机 Web 服务与执行节点的双模式版本,可「本机运行」也可「连接远程」。适合希望一台 Mac 同时充当服务器与客户端的用户。
+
+两份 App 的安装目录与数据目录相互独立,可以并存。客户端沿用历史安装身份,从旧版「夭夭」升级安装时原地替换、保留登录与配置。
+
+桌面安装包的签名、公证状态及自动升级可用范围见对应版本的 GitHub 发布说明。详细说明见 [macOS App](docs/macos-app.md)。
+
+## npm 安装
+
+已有 Node.js 24 环境时,服务端可通过 npm 一条命令安装:
+
+```sh
+npm install -g @lsamien/yaoyao
+yaoyao service install
+```
+
+安装后访问 `http://127.0.0.1:15300` 创建管理员账号。macOS 通过 LaunchAgent 开机自启;Linux 的服务安装支持在路线图中。详见 [npm 安装说明](docs/npm-install.md)。
 
 ## Docker 安装
 

+ 3 - 2
desktop/computer-helper.swift

@@ -34,8 +34,9 @@ func signingInfo(_ code: SecCode) -> [String: Any] {
 let parentInfo = signingInfo(parentCode), ownInfo = signingInfo(ownCode)
 guard let team = ownInfo[kSecCodeInfoTeamIdentifier as String] as? String,
       parentInfo[kSecCodeInfoTeamIdentifier as String] as? String == team,
-      // The full desktop and the client-only build share one signing team.
-      ["cn.samien.yaoyao.desktop", "cn.samien.yaoyao.desktop.client"]
+      // The client keeps the historical identifier for upgrade continuity;
+      // the full dual-mode build moved to its own identifier.
+      ["cn.samien.yaoyao.desktop", "cn.samien.yaoyao.desktop.full"]
         .contains(parentInfo[kSecCodeInfoIdentifier as String] as? String ?? "") else { fail("App 签名身份不匹配") }
 #endif
 let bytes = FileHandle.standardInput.readDataToEndOfFile()

+ 3 - 2
desktop/keychain-helper.swift

@@ -33,8 +33,9 @@ func signingInfo(_ code: SecCode) -> [String: Any] {
 let parentInfo = signingInfo(parentCode), ownInfo = signingInfo(ownCode)
 guard let team = ownInfo[kSecCodeInfoTeamIdentifier as String] as? String,
       parentInfo[kSecCodeInfoTeamIdentifier as String] as? String == team,
-      // The full desktop and the client-only build share one signing team.
-      ["cn.samien.yaoyao.desktop", "cn.samien.yaoyao.desktop.client"]
+      // The client keeps the historical identifier for upgrade continuity;
+      // the full dual-mode build moved to its own identifier.
+      ["cn.samien.yaoyao.desktop", "cn.samien.yaoyao.desktop.full"]
         .contains(parentInfo[kSecCodeInfoIdentifier as String] as? String ?? "") else { fail("App 签名身份不匹配") }
 #endif
 guard CommandLine.arguments.count == 3 else { fail("钥匙串请求格式无效") }

+ 5 - 1
desktop/main.mjs

@@ -29,8 +29,12 @@ const clientOnly = app.isPackaged
 const platform = desktopPlatform(process.platform, { clientOnly })
 const loginOptions = loginItemOptions()
 // Fixture identity also isolates Chromium cookies, window state and the app lock.
+// The shell package name distinguishes the variants: the client keeps the
+// historical 夭夭 identity (userData, logs, single-instance lock) so upgrades
+// stay in place, while the full dual-mode build uses its own 夭夭完整版 home.
+const productName = JSON.parse(readFileSync(join(desktopRoot, 'package.json'), 'utf8')).productName || '夭夭'
 if (fixtureHome) app.setPath('userData', join(fixtureHome, 'desktop'))
-app.setName('夭夭')
+app.setName(productName)
 if (platform.windows) app.setAppUserModelId('cn.samien.yaoyao.desktop')
 if (!app.requestSingleInstanceLock()) { app.quit() }
 else {

+ 4 - 2
docs/macos-app.md

@@ -1,6 +1,8 @@
-# macOS App 与本地服务
+# macOS 完整版 App 与本地服务
 
-App、Web 服务、Vue 界面和 Runner 同仓维护。正式 App 在每个构建首次启动时,将较旧的本机 Web 同步为配套构建;已有更新的 Web 保留。Web 由独立 LaunchAgent 运行,App 连接它。源码开发模式仍使用包内子进程。默认数据目录为 `~/.yaoyao`,默认 Web 端口为 15300。Hermes 是独立基础运行时,由本地服务检测并按既有托管逻辑启动;隔离电脑另需本机 Docker 或 Podman。
+本文档描述**夭夭完整版**(内嵌本机服务的双模式 App,安装名「夭夭完整版」)。连接远程服务器的纯客户端 App(主推的「夭夭」安装包)不运行本机服务,使用方式见 [README 的桌面版说明](../README.md#macos-桌面版);本文的更新、同步与服务生命周期章节仅适用于完整版。
+
+App、Web 服务、Vue 界面和 Runner 同仓维护。正式 App 在每个构建首次启动时,将较旧的本机 Web 同步为配套构建;已有更新的 Web 保留。Web 由独立 LaunchAgent 运行,App 连接它。源代码开发模式仍使用包内子进程。默认数据目录为 `~/.yaoyao`,默认 Web 端口为 15300。Hermes 是独立基础运行时,由本地服务检测并按既有托管逻辑启动;隔离电脑另需本机 Docker 或 Podman。
 
 ## 生命周期与权限
 

+ 65 - 0
docs/npm-install.md

@@ -0,0 +1,65 @@
+# npm 安装服务端
+
+适用于已有 Node.js 24 环境、希望在非 Docker 环境运行夭夭服务端的场景。macOS 与 Linux 均可。
+
+## 安装
+
+```sh
+npm install -g @lsamien/yaoyao
+```
+
+要求 Node.js 24 或更高版本(`node -v` 确认)。安装过程约 300 个依赖包;其中 Playwright 供托管浏览器功能使用,浏览器内核不随包安装,首次使用相关功能时按提示安装。
+
+### Windows 用户注意
+
+`@parcel/watcher` 的原生编译提示可以忽略:它只服务于开发模式的热更新,生产服务端不经过 Vite,缺失时自动回落,不影响运行。
+
+## 启动服务
+
+```sh
+yaoyao service install
+```
+
+- **macOS**:注册 LaunchAgent(`com.samien.hermes-yaoyao`)并立即启动,开机自启。
+- **Linux**:需要 systemd 支持(尚未提供,参见下文路线)。
+
+安装后访问 `http://127.0.0.1:15300`,首次打开自行创建管理员账号。
+
+其他命令:
+
+```sh
+yaoyao service status     # 查看运行状态
+yaoyao service stop       # 停止服务
+yaoyao service start      # 重新启动
+yaoyao service uninstall  # 卸载服务(数据保留)
+```
+
+## 配置
+
+| 环境变量 | 作用 | 默认 |
+| --- | --- | --- |
+| `YAOYAO_HOME` | 数据目录 | `~/.yaoyao` |
+| `HERMES_YAOYAO_PORT` | Web 端口 | `15300` |
+| `HERMES_YAOYAO_UPSTREAM` | Hermes 上游地址 | `http://127.0.0.1:9119` |
+
+LaunchAgent 安装时的环境会写入 plist;修改配置后执行 `yaoyao service stop && yaoyao service start` 生效。
+
+## 升级
+
+```sh
+npm update -g @lsamien/yaoyao
+yaoyao service stop && yaoyao service start
+```
+
+Web 界面中的「系统更新」入口面向源码/发布包安装;npm 安装的服务请直接用上述 npm 命令升级,避免两套版本并存。
+
+## 与其他安装方式的关系
+
+- **Docker**([Docker 部署说明](docker-install.md)):隔离性好,含电脑环境等全部能力,适合专用服务器。
+- **源码运行**(见 [README](../README.md)):开发与贡献场景。
+- npm 安装与 Docker 安装不要共用同一个数据目录。
+
+## 已知限制
+
+- Linux 的 `service install`(systemd user unit)尚未实现,当前 CLI 在 Linux 上会提示不支持;临时做法:手动运行 `npm root -g` 定位包目录,用进程管理器守护 `node dist-server/server/index.js`(工作目录设为包根目录)。
+- 通过 npm 安装的服务端不支持在 Web 界面内自更新(版本由 npm 管理)。

+ 3 - 3
electron-builder.client.yml

@@ -1,4 +1,4 @@
-appId: cn.samien.yaoyao.desktop.client
+appId: cn.samien.yaoyao.desktop
 productName: 夭夭
 directories:
   output: desktop-release-client
@@ -36,8 +36,8 @@ mac:
   extendInfo:
     NSMicrophoneUsageDescription: 夭夭使用麦克风接收语音消息。
 dmg:
-  title: 夭夭客户端 ${version}
-artifactName: Yaoyao-Client-${version}-${arch}.${ext}
+  title: 夭夭 ${version}
+artifactName: Yaoyao-${version}-${arch}.${ext}
 publish:
   - provider: github
     owner: Lsamien

+ 2 - 2
electron-builder.yml

@@ -1,5 +1,5 @@
-appId: cn.samien.yaoyao.desktop
-productName: 夭夭
+appId: cn.samien.yaoyao.desktop.full
+productName: 夭夭完整版
 afterPack: scripts/verify-desktop-runtime.mjs
 directories:
   output: desktop-release

+ 1 - 1
scripts/build-desktop.mjs

@@ -54,6 +54,6 @@ await chmod(resolve(service, 'node'), 0o755)
 await writeFile(resolve(service, 'package.json'), JSON.stringify({ name: 'yaoyao', version: manifest.webVersion, type: 'module' }))
 sealRuntime(service)
 await writeFile(resolve(shell, 'package.json'), JSON.stringify({ name: 'yaoyao-desktop', version: manifest.webVersion,
-  productName: '夭夭', description: '夭夭 AI:本地服务、多 Agent 团队与电脑工作空间',
+  productName: '夭夭完整版', description: '夭夭 AI:本地服务、多 Agent 团队与电脑工作空间',
   author: 'YaoYao contributors', license: 'Apache-2.0', main: 'main.mjs', type: 'module' }, null, 2))
 console.log('桌面服务和界面已打包到 .desktop-build')

+ 1 - 1
scripts/desktop-release.mjs

@@ -57,7 +57,7 @@ export async function packageDesktopRelease() {
     publish: 'never', config: signing })
   const directory = join(root, 'desktop-release')
   const names = await verifyUpdateArtifacts(directory, version)
-  const app = join(directory, 'mac-arm64', '夭夭.app')
+  const app = join(directory, 'mac-arm64', '夭夭完整版.app')
   const feed = load(await readFile(join(app, 'Contents/Resources/app-update.yml'), 'utf8'))
   if (feed.provider !== 'github' || feed.owner !== 'Lsamien' || feed.repo !== 'yaoyao') throw new Error('安装包中的更新源不正确')
   execFileSync('codesign', ['--verify', '--deep', '--strict', app], { stdio: 'inherit' })