Browse Source

发布 v0.4.0:新增 macOS 桌面版与自主团队及本地虚拟机

LSamien 1 tháng trước cách đây
mục cha
commit
6f69d6c255

+ 7 - 1
README.md

@@ -1,6 +1,6 @@
 # 夭夭 Web
 # 夭夭 Web
 
 
-当前发布版本:**v0.3.32**,配套 iOS **1.3(188)**。详见 [发布说明](docs/releases/v0.3.32.md)。
+当前 Web 与 macOS 桌面发布版本:**v0.4.0**。桌面版提供 Apple Silicon(arm64)DMG,详见 [发布说明](docs/releases/v0.4.0.md)。
 
 
 夭夭 Web 为网页、iOS 和安卓提供统一的 Agent、群聊、文件库及语音配置服务。Hermes 通过标准 9119 HTTP/WebSocket 接口执行任务。
 夭夭 Web 为网页、iOS 和安卓提供统一的 Agent、群聊、文件库及语音配置服务。Hermes 通过标准 9119 HTTP/WebSocket 接口执行任务。
 
 
@@ -34,6 +34,12 @@ Web 数据默认保存在 `~/.hermes-yaoyao`,可用 `HERMES_YAOYAO_HOME` 指
 
 
 由 Web 或 iOS 创建的普通聊天会按登录用户、Profile 和 Session ID 在 `chat-cache.sqlite3` 中登记所有权并持久化;后续续聊与列表展示以这份服务端登记为准,不再单独依赖 Hermes 的 `source` 标记。Web 的只读历史记录不进入该数据库,继续按需读取 9119。可通过 `HERMES_YAOYAO_CHAT_CACHE_MODE=upstream-only|shadow|prefer-local` 切换策略,默认 `prefer-local`。
 由 Web 或 iOS 创建的普通聊天会按登录用户、Profile 和 Session ID 在 `chat-cache.sqlite3` 中登记所有权并持久化;后续续聊与列表展示以这份服务端登记为准,不再单独依赖 Hermes 的 `source` 标记。Web 的只读历史记录不进入该数据库,继续按需读取 9119。可通过 `HERMES_YAOYAO_CHAT_CACHE_MODE=upstream-only|shadow|prefer-local` 切换策略,默认 `prefer-local`。
 
 
+## macOS 桌面版
+
+在 [v0.4.0 发布页](http://192.168.153.8:3000/samien/hermes-yaoyao/releases/tag/v0.4.0) 下载 `Yaoyao-0.4.0-arm64.dmg`,将“夭夭”拖到应用程序后打开。首次启动会核对并同步配套本机 Web 服务;普通退出保留后台运行,“停止后台服务并退出”可完整停止本应用管理的后台。详细说明见 [macOS App](docs/macos-app.md)。
+
+此 DMG 使用 Apple Development 开发签名,未做 Apple 公证。
+
 ## Docker 安装
 ## Docker 安装
 
 
 准备 Docker Engine 或 Docker Desktop,以及容器可访问的 Hermes 9119 服务。在仓库目录执行:
 准备 Docker Engine 或 Docker Desktop,以及容器可访问的 Hermes 9119 服务。在仓库目录执行:

+ 2 - 1
desktop/core-lifecycle.test.mjs

@@ -8,6 +8,7 @@ import {spawn} from 'node:child_process'
 import {_electron as electron} from '@playwright/test'
 import {_electron as electron} from '@playwright/test'
 import {localJSON,DesktopServiceManager} from './service-manager.mjs'
 import {localJSON,DesktopServiceManager} from './service-manager.mjs'
 const root=resolve(import.meta.dirname,'..')
 const root=resolve(import.meta.dirname,'..')
+const release=JSON.parse(await readFile(join(root,'release.json'),'utf8'))
 async function freePort(){const server=createServer();await new Promise(done=>server.listen(0,'127.0.0.1',done));const port=server.address().port;await new Promise(done=>server.close(done));return port}
 async function freePort(){const server=createServer();await new Promise(done=>server.listen(0,'127.0.0.1',done));const port=server.address().port;await new Promise(done=>server.close(done));return port}
 async function until(read,accept,timeout=30000){const end=Date.now()+timeout;while(Date.now()<end){try{const value=await read();if(accept(value))return value}catch{}await new Promise(done=>setTimeout(done,100))}throw new Error('等待桌面状态超时')}
 async function until(read,accept,timeout=30000){const end=Date.now()+timeout;while(Date.now()<end){try{const value=await read();if(accept(value))return value}catch{}await new Promise(done=>setTimeout(done,100))}throw new Error('等待桌面状态超时')}
 test('packaged core service starts, preserves window-close work, recovers, and stops only its own service',{timeout:60000},async()=>{
 test('packaged core service starts, preserves window-close work, recovers, and stops only its own service',{timeout:60000},async()=>{
@@ -20,7 +21,7 @@ test('packaged core service starts, preserves window-close work, recovers, and s
     const page=await app.firstWindow();await page.waitForURL(`http://127.0.0.1:${port}/**`)
     const page=await app.firstWindow();await page.waitForURL(`http://127.0.0.1:${port}/**`)
     const initial=JSON.parse(await readFile(join(home,'service-instance.json'),'utf8'));pid=initial.pid
     const initial=JSON.parse(await readFile(join(home,'service-instance.json'),'utf8'));pid=initial.pid
     const identity=await localJSON(`http://127.0.0.1:${port}/desktop/service`,{'x-yaoyao-desktop-token':initial.token})
     const identity=await localJSON(`http://127.0.0.1:${port}/desktop/service`,{'x-yaoyao-desktop-token':initial.token})
-    assert.equal(identity.desktopOwned,true);assert.equal(identity.pid,pid);assert.equal(identity.version,'0.3.32')
+    assert.equal(identity.desktopOwned,true);assert.equal(identity.pid,pid);assert.equal(identity.version,release.webVersion)
     assert.equal(await page.evaluate(async()=>{try{await window.yaoyaoDesktop.retry();return false}catch{return true}}),true)
     assert.equal(await page.evaluate(async()=>{try{await window.yaoyaoDesktop.retry();return false}catch{return true}}),true)
     await mkdir(join(root,'test-results/desktop'),{recursive:true});await page.screenshot({path:join(root,'test-results/desktop/packaged-core.png')})
     await mkdir(join(root,'test-results/desktop'),{recursive:true});await page.screenshot({path:join(root,'test-results/desktop/packaged-core.png')})
     await app.evaluate(({BrowserWindow})=>BrowserWindow.getAllWindows()[0].close())
     await app.evaluate(({BrowserWindow})=>BrowserWindow.getAllWindows()[0].close())

+ 5 - 3
desktop/service-sync.test.mjs

@@ -86,14 +86,16 @@ test('App first launch installs an independent Web; Web remains up after quit an
 
 
     // Install a later release through the same transaction engine used by Web's updater.
     // Install a later release through the same transaction engine used by Web's updater.
     const newer = join(home, 'updates', 'newer-package')
     const newer = join(home, 'updates', 'newer-package')
+    const [major, minor, patch] = original.version.split('.').map(Number)
+    const newerVersion = `${major}.${minor}.${patch + 1}`
     await cp(payloadRoot, newer, { recursive: true })
     await cp(payloadRoot, newer, { recursive: true })
-    await writeFile(join(newer, 'release.json'), JSON.stringify({ schemaVersion: 1, releaseVersion: '0.3.33', webVersion: '0.3.33', gitTag: 'v0.3.33' }))
+    await writeFile(join(newer, 'release.json'), JSON.stringify({ schemaVersion: 1, releaseVersion: newerVersion, webVersion: newerVersion, gitTag: `v${newerVersion}` }))
     const build = JSON.parse(await readFile(join(newer, 'build-info.json'), 'utf8'))
     const build = JSON.parse(await readFile(join(newer, 'build-info.json'), 'utf8'))
     await writeFile(join(newer, 'build-info.json'), JSON.stringify({ ...build, commit: 'c'.repeat(40), ancestors: [build.commit, ...build.ancestors], dirty: false }))
     await writeFile(join(newer, 'build-info.json'), JSON.stringify({ ...build, commit: 'c'.repeat(40), ancestors: [build.commit, ...build.ancestors], dirty: false }))
     sealRuntime(newer)
     sealRuntime(newer)
     await synchronizeDesktop({ home, runtimeRoot: newer, releaseRoot, driver })
     await synchronizeDesktop({ home, runtimeRoot: newer, releaseRoot, driver })
     const updated = JSON.parse(await readFile(join(home, 'service-instance.json'), 'utf8'))
     const updated = JSON.parse(await readFile(join(home, 'service-instance.json'), 'utf8'))
-    assert.equal(updated.version, '0.3.33'); assert.notEqual(updated.pid, record.pid)
+    assert.equal(updated.version, newerVersion); assert.notEqual(updated.pid, record.pid)
     assert.equal(driver.snapshot().plist.EnvironmentVariables.YAOYAO_TEST_PRESERVE, 'keep-existing-settings')
     assert.equal(driver.snapshot().plist.EnvironmentVariables.YAOYAO_TEST_PRESERVE, 'keep-existing-settings')
     app = await launch()
     app = await launch()
     const reopened = await app.firstWindow(); await reopened.waitForURL(`http://127.0.0.1:${port}/**`, { timeout: 90000 })
     const reopened = await app.firstWindow(); await reopened.waitForURL(`http://127.0.0.1:${port}/**`, { timeout: 90000 })
@@ -124,7 +126,7 @@ test('App first launch installs an independent Web; Web remains up after quit an
     app = await launch()
     app = await launch()
     const restarted = await app.firstWindow(); await restarted.waitForURL(`http://127.0.0.1:${port}/**`, { timeout: 90000 })
     const restarted = await app.firstWindow(); await restarted.waitForURL(`http://127.0.0.1:${port}/**`, { timeout: 90000 })
     const resumed = JSON.parse(await readFile(join(home, 'service-instance.json'), 'utf8'))
     const resumed = JSON.parse(await readFile(join(home, 'service-instance.json'), 'utf8'))
-    assert.equal(resumed.version, '0.3.33'); assert.equal(resumed.desktopOwned, false)
+    assert.equal(resumed.version, newerVersion); assert.equal(resumed.desktopOwned, false)
     assert.notEqual(resumed.pid, updated.pid)
     assert.notEqual(resumed.pid, updated.pid)
     await nativeQuit(app,true); app = undefined
     await nativeQuit(app,true); app = undefined
   } catch (error) {
   } catch (error) {

+ 2 - 2
docs/macos-app.md

@@ -38,7 +38,7 @@ npm run desktop:build
 npm run desktop:test
 npm run desktop:test
 npm run desktop:pack
 npm run desktop:pack
 codesign --verify --deep --strict desktop-release/mac-arm64/夭夭.app
 codesign --verify --deep --strict desktop-release/mac-arm64/夭夭.app
-hdiutil verify desktop-release/Yaoyao-0.3.32-arm64.dmg
+hdiutil verify desktop-release/Yaoyao-0.4.0-arm64.dmg
 ```
 ```
 
 
 开发 App 的 6 项测试通过:真实内置服务启动、账号与 Runner 连接、关闭窗口保持运行、服务崩溃恢复、外部客户端不误停服务、退出清理、加密配置再次启动、偏好保存及手动重新打开、版本不符拒绝、钥匙串失败保护。另有更新器 14 项定向测试通过,包含 App 禁用源码更新及回退。
 开发 App 的 6 项测试通过:真实内置服务启动、账号与 Runner 连接、关闭窗口保持运行、服务崩溃恢复、外部客户端不误停服务、退出清理、加密配置再次启动、偏好保存及手动重新打开、版本不符拒绝、钥匙串失败保护。另有更新器 14 项定向测试通过,包含 App 禁用源码更新及回退。
@@ -56,4 +56,4 @@ Electron 初始化时设置其钥匙串项目名称([v43.4.0 源码](https://g
 
 
 最终实际签名 App 的完整 Runner 导入、加密保存、服务崩溃恢复、窗口关闭保持连接、退出清理和再次打开自动连接均已通过;另有独立的核心服务生命周期测试通过。新加密助手在实际签名 App 中验证所属路径和签名团队。安装包经挂载检查,开发专用助手没有进入分发包。
 最终实际签名 App 的完整 Runner 导入、加密保存、服务崩溃恢复、窗口关闭保持连接、退出清理和再次打开自动连接均已通过;另有独立的核心服务生命周期测试通过。新加密助手在实际签名 App 中验证所属路径和签名团队。安装包经挂载检查,开发专用助手没有进入分发包。
 
 
-已有正式服务会被识别并复用,App 不会自动把它换成分支代码。需要独立预览时,先退出其他夭夭 App,再指定独立 `HERMES_YAOYAO_HOME` 和 `HERMES_YAOYAO_DESKTOP_PORT` 启动;交付目录 `README.txt` 提供具体命令。
+v0.4.0 同时发布 Web 与 macOS Apple Silicon DMG,见 [发布说明](releases/v0.4.0.md)。正式 App 首次启动按上文同步规则升级较旧的本机 Web;已有更新版本保留。独立预览应指定独立 `HERMES_YAOYAO_HOME` 和 `HERMES_YAOYAO_DESKTOP_PORT`。

+ 17 - 0
docs/releases/v0.4.0.md

@@ -0,0 +1,17 @@
+# v0.4.0
+
+本次同时发布 Web 与 macOS Apple Silicon(arm64)桌面版。
+
+- 新增“夭夭”macOS App,内置 Web、Node 运行时和 Runner;首次启动同步较旧的本机服务,支持后台运行、完整停止、登录启动设置及加密保存执行节点配置。
+- Bot 可在用户授权后组建成员与团队,创建有负责人、依赖和验收条件的任务;临时助手有独立标识及退役清理。
+- 新增本地虚拟机准备与维护、聊天内电脑面板、人工接管及交还控制;支持显式授权的跨成员共享电脑和持久工作目录。
+- 完善 Runner、隔离 Hermes Worker、受控网络访问、任务产物回传,以及归档 Bot、群聊和不存在会话的删除与清理。
+- 保留手动会话管理,撤回 v0.3.33 的自动清理行为。
+
+## 安装与升级
+
+桌面安装包:`Yaoyao-0.4.0-arm64.dmg`,附 SHA-256 校验文件。将 App 拖到“应用程序”后打开;本机服务同步规则见 [App 与 Web 版本同步](../app-web-sync.md)。
+
+DMG 使用 Apple Development 开发签名,未做 Apple 公证。此包适用于 macOS Apple Silicon;本次不提供 Intel、Windows 或 Linux 桌面安装包。Hermes 基础运行时仍需独立配置,本地虚拟机另需 Docker 或 Podman。
+
+Web 可通过既有系统升级入口更新;源码部署切换至 `v0.4.0` 后重新构建并重启,Docker 部署重新构建镜像。升级前备份整个 Web 数据目录。更多说明见 [macOS App](../macos-app.md)、[本地虚拟机](../local-vm.md) 和 [桌面生命周期](../desktop-lifecycle.md)。

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
 {
   "name": "hermes-yaoyao",
   "name": "hermes-yaoyao",
-  "version": "0.3.32",
+  "version": "0.4.0",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "hermes-yaoyao",
       "name": "hermes-yaoyao",
-      "version": "0.3.32",
+      "version": "0.4.0",
       "license": "BSL-1.1",
       "license": "BSL-1.1",
       "dependencies": {
       "dependencies": {
         "@koa/bodyparser": "^5.0.0",
         "@koa/bodyparser": "^5.0.0",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "hermes-yaoyao",
   "name": "hermes-yaoyao",
-  "version": "0.3.32",
+  "version": "0.4.0",
   "private": true,
   "private": true,
   "license": "BSL-1.1",
   "license": "BSL-1.1",
   "description": "夭夭 Web:直连 Hermes 9119 的聊天、团队、文件库与产物工作台",
   "description": "夭夭 Web:直连 Hermes 9119 的聊天、团队、文件库与产物工作台",

+ 1 - 1
playwright.config.ts

@@ -3,7 +3,7 @@ import { defineConfig, devices } from '@playwright/test'
 export default defineConfig({
 export default defineConfig({
   testDir: './tests/e2e',
   testDir: './tests/e2e',
   // These suites own different fixtures and run with their dedicated configurations.
   // These suites own different fixtures and run with their dedicated configurations.
-  testIgnore: ['**/workspace-chat.spec.ts', '**/avatarReference.spec.ts', '**/avatarRoundTrip.spec.ts', '**/serverIdentity.spec.ts'],
+  testIgnore: ['**/workspace-chat.spec.ts', '**/team-tools.spec.ts', '**/avatarReference.spec.ts', '**/avatarRoundTrip.spec.ts', '**/serverIdentity.spec.ts'],
   timeout: 30_000,
   timeout: 30_000,
   fullyParallel: false,
   fullyParallel: false,
   workers: 1,
   workers: 1,

+ 3 - 3
release.json

@@ -1,6 +1,6 @@
 {
 {
   "schemaVersion": 1,
   "schemaVersion": 1,
-  "releaseVersion": "0.3.32",
-  "webVersion": "0.3.32",
-  "gitTag": "v0.3.32"
+  "releaseVersion": "0.4.0",
+  "webVersion": "0.4.0",
+  "gitTag": "v0.4.0"
 }
 }