diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 73cc0b9..92432a5 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -7,9 +7,11 @@ # Required repository secrets: # TAURI_SIGNING_PRIVATE_KEY # TAURI_SIGNING_PRIVATE_KEY_PASSWORD -# macOS additionally requires: +# macOS optionally uses these secrets for Developer ID signing and notarization: # APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY # APPLE_ID, APPLE_PASSWORD, APPLE_TEAM_ID +# When they are absent, both native macOS packages use ad-hoc signing. Users +# must approve those builds through System Settings > Privacy & Security. # # To cut a release: # 1. Run `npm --prefix desktop run version:desktop -- X.Y.Z` and commit the @@ -147,18 +149,47 @@ jobs: - name: Verify dependency installation kept the checkout clean run: git diff --exit-code -- desktop/package-lock.json + # Do not pass empty APPLE_CERTIFICATE variables to tauri-action: their + # presence makes the action try to import an empty keychain certificate. + # Export the complete Developer ID environment only when every signing + # and notarization secret is available; otherwise request ad-hoc signing. + - name: Configure macOS signing + if: startsWith(matrix.platform, 'macos-') + shell: bash + env: + APPLE_CERTIFICATE_SECRET: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD_SECRET: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY_SECRET: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID_SECRET: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD_SECRET: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID_SECRET: ${{ secrets.APPLE_TEAM_ID }} + run: | + if [[ -n "$APPLE_CERTIFICATE_SECRET" \ + && -n "$APPLE_CERTIFICATE_PASSWORD_SECRET" \ + && -n "$APPLE_SIGNING_IDENTITY_SECRET" \ + && -n "$APPLE_ID_SECRET" \ + && -n "$APPLE_PASSWORD_SECRET" \ + && -n "$APPLE_TEAM_ID_SECRET" ]]; then + { + printf 'APPLE_CERTIFICATE=%s\n' "$APPLE_CERTIFICATE_SECRET" + printf 'APPLE_CERTIFICATE_PASSWORD=%s\n' "$APPLE_CERTIFICATE_PASSWORD_SECRET" + printf 'APPLE_SIGNING_IDENTITY=%s\n' "$APPLE_SIGNING_IDENTITY_SECRET" + printf 'APPLE_ID=%s\n' "$APPLE_ID_SECRET" + printf 'APPLE_PASSWORD=%s\n' "$APPLE_PASSWORD_SECRET" + printf 'APPLE_TEAM_ID=%s\n' "$APPLE_TEAM_ID_SECRET" + } >> "$GITHUB_ENV" + echo "Using Developer ID signing and notarization." + else + echo 'APPLE_SIGNING_IDENTITY=-' >> "$GITHUB_ENV" + echo "Apple credentials are incomplete; using ad-hoc signing." + fi + - name: Build and publish release uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} HUGAGENT_RELEASE_BUILD: "1" # tsc + Vite peaks near Node's default heap limit on hosted runners. NODE_OPTIONS: "--max-old-space-size=4096" diff --git a/desktop/README.md b/desktop/README.md index 0e91eff..7e0e6a3 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -158,7 +158,13 @@ npm --prefix desktop run lock:desktop `prepare-bundle.mjs` 会在耗时构建开始前校验三者一致。公开 CE 的 Desktop Release workflow 会在 启动 Windows x86_64、Linux x86_64、macOS arm64、macOS x86_64 四个原生目标前校验 release tag 必须精确等于 `desktop-v<上述版本号>`;版本或 tag 不一致时不会创建任何平台产物。工作流固定使用 -`uv 0.11.33`;macOS 正式发布还需配置 Apple 证书、签名身份与 notarization 所需 secrets。 +`uv 0.11.33`;macOS 正式发布建议配置 Apple 证书、签名身份与 notarization 所需 secrets。 + +Apple 凭据不是生成测试安装包的硬前置。公开 CE 的 Release workflow 在未配置 Apple secrets 时会 +自动使用 ad-hoc 身份(`-`)签名 App 及内置 Python runtime 的 Mach-O 文件,并分别生成 Apple +Silicon 与 Intel DMG;用户首次打开时需要在“系统设置 → 隐私与安全性”中选择“仍要打开”。配置 +Developer ID 与 notarization secrets 后,workflow 会自动改用正式签名,避免这一步人工放行。 +Tauri updater 的 `TAURI_SIGNING_PRIVATE_KEY` 是独立的更新包验签机制,仍为必需项。 ## 关键文件 diff --git a/desktop/package-lock.json b/desktop/package-lock.json index b92d7cd..211ab19 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "hugagent-desktop", - "version": "0.2.7", + "version": "0.2.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hugagent-desktop", - "version": "0.2.7", + "version": "0.2.8", "devDependencies": { "@tauri-apps/cli": "^2" } diff --git a/desktop/package.json b/desktop/package.json index 11c02cd..8ee84fe 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "hugagent-desktop", - "version": "0.2.7", + "version": "0.2.8", "private": true, "description": "HugAgentOS 桌面客户端(远程连接 + Windows/macOS/Linux 离线本机服务)", "scripts": { diff --git a/desktop/scripts/build-runtime.mjs b/desktop/scripts/build-runtime.mjs index c40eb6f..d17d960 100644 --- a/desktop/scripts/build-runtime.mjs +++ b/desktop/scripts/build-runtime.mjs @@ -204,9 +204,11 @@ function directorySize(root) { function signMacRuntime(root) { if (process.platform !== "darwin") return; - const identity = process.env.APPLE_SIGNING_IDENTITY || "-"; - if (process.env.HUGAGENT_RELEASE_BUILD === "1" && identity === "-") { - throw new Error("APPLE_SIGNING_IDENTITY is required for a macOS release runtime."); + const identity = process.env.APPLE_SIGNING_IDENTITY?.trim() || "-"; + if (identity === "-") { + console.warn( + "[desktop] Apple signing identity unavailable; using ad-hoc signing for the macOS runtime.", + ); } const files = []; const visit = (directory) => { diff --git a/desktop/scripts/mac-installer.test.mjs b/desktop/scripts/mac-installer.test.mjs index 9a7a9d5..ae0926c 100644 --- a/desktop/scripts/mac-installer.test.mjs +++ b/desktop/scripts/mac-installer.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; @@ -10,6 +10,7 @@ import { } from "./desktop-build-target.mjs"; const desktopDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const repoDir = resolve(desktopDir, ".."); const rustDir = join(desktopDir, "src-tauri", "src"); test("desktop local install is offline and shared by all three operating systems", () => { @@ -47,6 +48,51 @@ test("Windows macOS and Linux packages embed both source and runtime payloads", } }); +test("minimize-to-tray destroys the close confirmation window", () => { + const source = readFileSync(join(rustDir, "lib.rs"), "utf8"); + const decisionStart = source.indexOf( + 'if let Some(cw) = app2.get_webview_window("close-confirm")', + ); + assert.notEqual(decisionStart, -1); + const decision = source.slice(decisionStart, source.indexOf("if exit", decisionStart)); + assert.match(decision, /cw\.hide\(\)/); + assert.match(decision, /cw\.close\(\)/); + assert.ok(decision.indexOf("cw.hide()") < decision.indexOf("cw.close()")); +}); + +test("CE project creation follows the provisioned desktop mode", () => { + const deploymentStore = readFileSync( + join(repoDir, "src", "frontend", "src", "stores", "deploymentModeStore.ts"), + "utf8", + ); + const projectComponent = (name) => { + const overlayPath = join( + repoDir, + "ce", + "overlay", + "src", + "frontend", + "src", + "components", + "projects", + name, + ); + return existsSync(overlayPath) + ? overlayPath + : join(repoDir, "src", "frontend", "src", "components", "projects", name); + }; + const projectsPanel = readFileSync(projectComponent("ProjectsPanel.tsx"), "utf8"); + const createModal = readFileSync(projectComponent("CreateProjectModal.tsx"), "utf8"); + + assert.match(deploymentStore, /local_only'.*cloud: false, local: true/); + assert.match(deploymentStore, /dual'.*cloud: true, local: true/); + assert.match(projectsPanel, /projectCreationTargets\(isDesktop, provisionMode\)/); + assert.match(projectsPanel, /canCreateCloudProject && canCreateLocalProject/); + assert.match(projectsPanel, /\/__desktop\/pick-local-folder/); + assert.match(createModal, /if \(!canCreateCloudProject\)/); + assert.match(createModal, /if \(!canCreateCloudProject\) return null/); +}); + test("release builder validates dependencies and relocatable runtime before archiving", () => { const builder = readFileSync(join(desktopDir, "scripts", "build-runtime.mjs"), "utf8"); const smoke = readFileSync(join(desktopDir, "scripts", "runtime-smoke.py"), "utf8"); @@ -58,6 +104,40 @@ test("release builder validates dependencies and relocatable runtime before arch assert.match(smoke, /import_module\("cli"\)/); }); +test("macOS release falls back to ad-hoc signing without Apple credentials", () => { + const builder = readFileSync(join(desktopDir, "scripts", "build-runtime.mjs"), "utf8"); + const overlayWorkflow = join( + repoDir, + "ce", + "overlay", + ".github", + "workflows", + "desktop-release.yml", + ); + const workflow = readFileSync( + existsSync(overlayWorkflow) + ? overlayWorkflow + : join(repoDir, ".github", "workflows", "desktop-release.yml"), + "utf8", + ); + + assert.match(builder, /APPLE_SIGNING_IDENTITY\?\.trim\(\) \|\| "-"/); + assert.doesNotMatch(builder, /APPLE_SIGNING_IDENTITY is required/); + assert.match(builder, /\["--force", "--sign", identity\]/); + + const configureStepStart = workflow.indexOf("- name: Configure macOS signing"); + const buildStepStart = workflow.indexOf("- name: Build and publish release"); + assert.notEqual(configureStepStart, -1); + assert.ok(buildStepStart > configureStepStart); + + const configureStep = workflow.slice(configureStepStart, buildStepStart); + const buildStep = workflow.slice(buildStepStart); + assert.match(configureStep, /APPLE_CERTIFICATE_SECRET/); + assert.match(configureStep, /echo 'APPLE_SIGNING_IDENTITY=-'/); + assert.doesNotMatch(buildStep, /APPLE_CERTIFICATE:/); + assert.doesNotMatch(buildStep, /APPLE_CERTIFICATE_PASSWORD:/); +}); + test("CE generation uses an isolated pinned release-builder dependency", () => { const prepare = readFileSync(join(desktopDir, "scripts", "prepare-bundle.mjs"), "utf8"); const requirements = readFileSync( diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 43f2800..0b1ddf5 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1687,7 +1687,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hugagent-desktop" -version = "0.2.7" +version = "0.2.8" dependencies = [ "axum", "bytes", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index d4f93d7..d370050 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hugagent-desktop" -version = "0.2.7" +version = "0.2.8" description = "HugAgentOS桌面客户端" edition = "2021" rust-version = "1.77" diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 436df9d..54dd5aa 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -672,9 +672,11 @@ fn open_close_confirm(app: &tauri::AppHandle) { ); } if let Some(cw) = app2.get_webview_window("close-confirm") { - // Reuse the fully-loaded WebView next time. Rebuilding it is - // both slower and the main source of close-dialog flicker. + // Destroy the confirmation window after making it invisible. + // Keeping the loaded WebView around races its page-load handler: + // it can show itself again after the main window was hidden. let _ = cw.hide(); + let _ = cw.close(); } if exit { app2.exit(0); diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 915bedf..9ef3afc 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "HugAgentOS", - "version": "0.2.7", + "version": "0.2.8", "identifier": "com.hugagent.desktop", "build": { "frontendDist": "../../src/frontend/dist", diff --git a/document/en/deployment/windows-deployment.md b/document/en/deployment/windows-deployment.md index 59678d3..4b2e3a0 100644 --- a/document/en/deployment/windows-deployment.md +++ b/document/en/deployment/windows-deployment.md @@ -1,6 +1,6 @@ # Windows installation and deployment -> Last updated: July 23, 2026 | [简体中文](../../zh-CN/deployment/windows-deployment.md) | Back to [Deployment Guide](README.md) +> Last updated: July 31, 2026 | [简体中文](../../zh-CN/deployment/windows-deployment.md) | Back to [Deployment Guide](README.md) Windows users can run a Docker-free, desktop-managed local service or deploy the team service through Docker Desktop and WSL2. The local option is for personal use; @@ -47,7 +47,9 @@ don't need to reinstall the desktop client. The local service listens only on `http://127.0.0.1:32101`; it isn't exposed to the LAN. Exiting the desktop app stops the service process. Minimizing to the tray keeps -it running for background automations. +it running for background automations. After **Minimize to tray** is selected, the +close-confirmation window is destroyed immediately and does not reappear when the +main window is restored from the tray. The runtime and data live under this directory: diff --git a/document/en/modules/projects-myspace.md b/document/en/modules/projects-myspace.md index 0ab6142..1b7a3df 100644 --- a/document/en/modules/projects-myspace.md +++ b/document/en/modules/projects-myspace.md @@ -1,6 +1,6 @@ # Projects & MySpace -> Last updated: 2026-07-30 +> Last updated: 2026-07-31 HugAgentOS provides two tiers of personal workspace: @@ -74,6 +74,20 @@ shortcuts on the project row: deletes the project, and existing conversations return to the regular chat history. +### Desktop project-creation entry points + +Desktop creation controls strictly follow the provision mode selected during +installation, with the same behavior on the Projects page and in the chat +composer's project menu: + +- **Local only (`local_only`)** shows only **New Local Project** and uses the + system folder picker; the cloud-project modal is unavailable. +- **Cloud only (`cloud_only`)** shows only **New Cloud Project** and offers no + local-folder project action. +- **Dual mode (`dual`)** offers both cloud and local project actions. +- The regular Web application has no desktop folder picker and keeps the cloud + personal/team project flow. + ## MySpace ### File assets and personal folders diff --git a/document/zh-CN/deployment/windows-deployment.md b/document/zh-CN/deployment/windows-deployment.md index ff7f575..df78936 100644 --- a/document/zh-CN/deployment/windows-deployment.md +++ b/document/zh-CN/deployment/windows-deployment.md @@ -1,6 +1,6 @@ # Windows 安装与部署 -> 最后更新:2026-07-23 | [English](../../en/deployment/windows-deployment.md) | 返回 [部署指南](README.md) +> 最后更新:2026-07-31 | [English](../../en/deployment/windows-deployment.md) | 返回 [部署指南](README.md) Windows 用户可以选择桌面端托管的无 Docker 单机服务,也可以通过 Docker Desktop + WSL2 运行团队版 Compose 服务。前者适合个人使用,后者适合多人协作与生产。 @@ -40,7 +40,8 @@ Windows x86_64 个人用户可以直接使用 NSIS 桌面安装包。安装包 ### 运行与数据 本机服务只监听 `http://127.0.0.1:32101`,不会向局域网暴露。客户端退出时会回收服务进程; -最小化到托盘时服务继续运行,以支持后台自动化任务。 +最小化到托盘时服务继续运行,以支持后台自动化任务。选择“最小化到托盘”后,退出确认窗口会立即 +关闭并销毁;从托盘恢复主窗口时不会残留上一次的退出提示。 数据与运行环境位于: diff --git a/document/zh-CN/modules/projects-myspace.md b/document/zh-CN/modules/projects-myspace.md index 57fa2dc..781e7c4 100644 --- a/document/zh-CN/modules/projects-myspace.md +++ b/document/zh-CN/modules/projects-myspace.md @@ -1,6 +1,6 @@ # 项目空间与我的空间 -> 最后更新:2026-07-30 +> 最后更新:2026-07-31 HugAgentOS 提供两级个人化工作区: @@ -70,6 +70,15 @@ teams ─────────┬── team_members(role: owner/admin/memb - 打开更多菜单,可将项目置顶、在项目面板中打开,或移除项目。置顶与移除仅对 项目管理员可用;移除会软删除项目,已有对话会回落到普通历史对话列表。 +### 桌面端新建项目入口 + +桌面端的新建入口严格跟随安装时选定的运行形态,项目列表页与聊天输入区的项目下拉保持一致: + +- **纯本机(`local_only`)**:只显示“新建本地项目”,通过系统文件夹选择器把本机目录建立为项目;不显示、也不能打开云端项目创建弹窗; +- **纯云端(`cloud_only`)**:只显示“新建云端项目”,不提供本机文件夹项目入口; +- **双模式(`dual`)**:同时提供云端项目和本地项目,由用户在新建时选择; +- **普通 Web**:没有桌面文件夹选择器,因此保持云端个人/团队项目创建流程。 + ## 我的空间(MySpace) ### 文件资产与个人文件夹 diff --git a/src/frontend/src/components/chat/InputArea.tsx b/src/frontend/src/components/chat/InputArea.tsx index 4b8ad11..c64136d 100644 --- a/src/frontend/src/components/chat/InputArea.tsx +++ b/src/frontend/src/components/chat/InputArea.tsx @@ -10,7 +10,7 @@ import { } from '@ant-design/icons'; import { useChatStore, useFileStore, useUIStore, useModelCapabilitiesStore, useCatalogStore, useAuthStore, usePluginStore, useEditionStore } from '../../stores'; import { useProjectStore } from '../../stores/projectStore'; -import { useDeploymentModeStore } from '../../stores/deploymentModeStore'; +import { projectCreationTargets, useDeploymentModeStore } from '../../stores/deploymentModeStore'; import { useAgentStore } from '../../stores/agentStore'; import type { UserAgentItem } from '../../stores/agentStore'; import type { ChatMode } from '../../stores/chatStore'; @@ -254,8 +254,12 @@ export function InputArea({ const activeLocalMode = useDeploymentModeStore((s) => s.activeLocal); const provisionMode = useDeploymentModeStore((s) => s.provisionMode); const isDesktopShell = useDeploymentModeStore((s) => s.isDesktop); + const { + cloud: canCreateCloudProject, + local: canCreateLocalProject, + } = projectCreationTargets(isDesktopShell, provisionMode); // 混合架构:双模式=云端身份 + 本机执行面,本地项目能力在 dual 下同样可用。 - const localCapable = activeLocalMode || provisionMode === 'dual'; + const localCapable = canCreateLocalProject; const refreshDeploymentMode = useDeploymentModeStore((s) => s.refresh); useEffect(() => { refreshDeploymentMode(); @@ -777,40 +781,36 @@ export function InputArea({ ], }, { type: 'divider' as const }, - // 新建入口:桌面壳里区分「云端项目 / 本地项目」(本地项目=选电脑上的一个 - // 文件夹作为项目);网页端保持原来的「新建个人项目」。跨模式点击时沿用壳的 - // 切换哨兵(activate-local / connect-server,会重启应用进入对应模式)。 + // 新建入口严格跟随安装时选择的运行形态:纯本机只显示本地项目,纯云端 + // 只显示云端项目,只有双模式同时显示两者。网页端保持原来的个人项目入口。 ...(isDesktopShell ? [ - { - key: 'proj-new-cloud', - label: ( -