From 39ba3d07727ba04349896df2538c569442e73bd5 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 28 Jun 2026 10:55:29 +0800 Subject: [PATCH 01/16] fix(artifacts): prioritize PDF deliverables in sidebar --- .../desktop_thread_artifact_service.dart | 24 +++++++++++ .../desktop_thread_artifact_service_test.dart | 43 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/lib/runtime/desktop_thread_artifact_service.dart b/lib/runtime/desktop_thread_artifact_service.dart index 0cbdadf0..9aa1ec69 100644 --- a/lib/runtime/desktop_thread_artifact_service.dart +++ b/lib/runtime/desktop_thread_artifact_service.dart @@ -270,6 +270,20 @@ class DesktopThreadArtifactService { } } entries.sort((a, b) { + final deliveryCompare = artifactDisplayPriorityInternal( + a.relativePath, + ).compareTo(artifactDisplayPriorityInternal(b.relativePath)); + if (deliveryCompare != 0) { + return deliveryCompare; + } + if (fileExtensionInternal(a.relativePath) == 'pdf') { + final depthCompare = artifactPathDepthInternal( + a.relativePath, + ).compareTo(artifactPathDepthInternal(b.relativePath)); + if (depthCompare != 0) { + return depthCompare; + } + } final updatedCompare = (b.updatedAtMs ?? 0).compareTo(a.updatedAtMs ?? 0); if (updatedCompare != 0) { return updatedCompare; @@ -279,6 +293,16 @@ class DesktopThreadArtifactService { return entries; } + static int artifactDisplayPriorityInternal(String relativePath) { + return fileExtensionInternal(relativePath) == 'pdf' ? 0 : 1; + } + + static int artifactPathDepthInternal(String relativePath) { + return normalizeArtifactPathInternal( + relativePath, + ).split('/').where((segment) => segment.isNotEmpty).length; + } + Future> buildResultEntriesInternal({ required List changes, required List fileEntries, diff --git a/test/runtime/desktop_thread_artifact_service_test.dart b/test/runtime/desktop_thread_artifact_service_test.dart index 15f23f3a..d28303cf 100644 --- a/test/runtime/desktop_thread_artifact_service_test.dart +++ b/test/runtime/desktop_thread_artifact_service_test.dart @@ -65,6 +65,49 @@ void main() { }, ); + test( + 'loadSnapshot shows the root PDF deliverable before supporting files', + () async { + final workspace = await Directory.systemTemp.createTemp( + 'xworkmate-pdf-artifact-order-', + ); + addTearDown(() async { + if (await workspace.exists()) { + await workspace.delete(recursive: true); + } + }); + await Directory( + '${workspace.path}/assets/diagrams', + ).create(recursive: true); + await File( + '${workspace.path}/assets/diagrams/chapter.png', + ).writeAsBytes([1, 2, 3]); + await File( + '${workspace.path}/assets/安全架构演进白皮书.pdf', + ).writeAsBytes([4, 5, 6]); + await File( + '${workspace.path}/安全架构演进白皮书.pdf', + ).writeAsBytes([4, 5, 6]); + + final snapshot = await DesktopThreadArtifactService().loadSnapshot( + workspacePath: workspace.path, + workspaceKind: WorkspaceRefKind.localPath, + artifactRelativePaths: const [ + 'assets/diagrams/chapter.png', + 'assets/安全架构演进白皮书.pdf', + '安全架构演进白皮书.pdf', + ], + ); + + expect(snapshot.fileEntries.map((entry) => entry.relativePath), [ + '安全架构演进白皮书.pdf', + 'assets/安全架构演进白皮书.pdf', + 'assets/diagrams/chapter.png', + ]); + expect(snapshot.resultEntries.first.mimeType, 'application/pdf'); + }, + ); + test( 'loadPreview rejects historical files outside current task artifacts', () async { From a12384274f1488a36e979e8b71477387313a8d94 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 28 Jun 2026 11:29:28 +0800 Subject: [PATCH 02/16] docs(cases): add gateway turn acceptance summary --- docs/cases/06-gateway-turn-stability-and-robustness.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/cases/06-gateway-turn-stability-and-robustness.md b/docs/cases/06-gateway-turn-stability-and-robustness.md index a42e7361..c625430c 100644 --- a/docs/cases/06-gateway-turn-stability-and-robustness.md +++ b/docs/cases/06-gateway-turn-stability-and-robustness.md @@ -423,3 +423,11 @@ curl -sS -X POST http://127.0.0.1:8787/acp/rpc \ 3. **超时同源不可漂移**:入口 `xworkmate_bridge_acp_stream_timeout` 与 bridge `openClawAgentWaitMaxTimeout` 已分两侧定义,建议在 validate/CI 加一条「入口 ≥ bridge + 余量」的交叉断言,防未来单侧改值再漂移。 4. **S1 重做前先补测**:先写「有 expectedArtifactDirs 但 run 无产物」与「agent 写产物到 workspace 根」两类对照 E2E,再改实现,避免重蹈 `0280893` 回退。 5. **`/api/ping.metrics` 接告警**:`gatewaySocketClosed`/`taskGetUnconfirmedFallback`/`runDeadlineInterrupt` 三计数接监控,使「不稳定」可被观测而非靠用户截图。 + +### 9.5 本次验收摘要 + +这次 case 的结论可以压缩成三句话: + +1. 不是 `LiteLLM` 余量问题,而是 gateway-turn 的契约链路里,插件加载、运行态快照和结果回传先后顺序出了偏差。 +2. `openclaw-multi-session-plugins` 稳定加载后,`xworkmate.tasks.get` 能回到可持续轮询的终态语义,`GoTaskService 没有返回可显示的输出。` 也随之恢复为可显示结果。 +3. 当前验收标准是:任务能完成、能产出 `.md`、`tasks.get` 能返回 `completed + durable output + artifacts`,并且 App 不再把 undecorated `running` 快照误判成空终态。 From db0b8abc6627878f1cc9a2388f7a0f468b7e171c Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 28 Jun 2026 12:12:10 +0800 Subject: [PATCH 03/16] ci: add release/* branch source validation workflow (#19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release/* 仅接受 hotfix/* 或带 cherry-pick/backport 标签的 PR。 详见 iac_modules/docs/tldr-github-branch-model.md Co-authored-by: Haitao Pan Co-authored-by: Claude Opus 4.8 --- .github/workflows/validate-release-pr.yml | 44 +++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/validate-release-pr.yml diff --git a/.github/workflows/validate-release-pr.yml b/.github/workflows/validate-release-pr.yml new file mode 100644 index 00000000..7af78740 --- /dev/null +++ b/.github/workflows/validate-release-pr.yml @@ -0,0 +1,44 @@ +name: Validate Release PR + +# release/* 分支的发布策略门禁:仅接受 hotfix/* 或带 cherry-pick/backport 标签的 PR。 +# 详见 iac_modules/docs/tldr-github-branch-model.md +on: + pull_request_target: + types: [opened, synchronize, reopened, labeled, unlabeled] + +permissions: + contents: read + pull-requests: read + +jobs: + validate-release-source: + runs-on: ubuntu-latest + if: startsWith(github.base_ref, 'release/') + steps: + - name: Check PR source branch + run: | + SRC="${{ github.head_ref }}" + TGT="${{ github.base_ref }}" + LABELS="${{ join(github.event.pull_request.labels.*.name, ',') }}" + + echo "🔍 Validating PR into release branch" + echo " source: $SRC" + echo " target: $TGT" + echo " labels: $LABELS" + + if [[ "$SRC" =~ ^hotfix/ ]]; then + echo "✅ Allowed: hotfix/* branch" + exit 0 + fi + + if [[ "$LABELS" =~ (^|,)(cherry-pick|backport)(,|$) ]]; then + echo "✅ Allowed: cherry-pick/backport labeled PR" + exit 0 + fi + + echo "❌ Rejected." + echo "release/* 仅接受:" + echo " - 来自 hotfix/* 的 PR" + echo " - 带 cherry-pick 或 backport 标签的 PR(已验证 feature 的 backport/cherry-pick)" + echo "禁止从 main / develop / feature/* 直接合并到 release/*。" + exit 1 From 4ed10c8f2bb04e5f0677ab91a907147668711acc Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 28 Jun 2026 14:46:00 +0800 Subject: [PATCH 04/16] ci: run desktop integration/patrol tests under xvfb (#22) Headless Linux runners have no display, so 'flutter test integration_test' fails to launch the GTK app ('The log reader stopped unexpectedly, or never started'). Wrap integration/patrol layers in xvfb-run with a 24-bit screen and install xvfb + mesa DRI driver for headless GL. macOS/local runs are unaffected (no xvfb-run -> command runs directly). Co-authored-by: Haitao Pan Co-authored-by: Claude Opus 4.8 --- scripts/ci/run_layered_tests.sh | 17 +++++++++++++++-- scripts/ci/setup_platform_deps.sh | 4 +++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/scripts/ci/run_layered_tests.sh b/scripts/ci/run_layered_tests.sh index 3a54ee3e..a6e12646 100755 --- a/scripts/ci/run_layered_tests.sh +++ b/scripts/ci/run_layered_tests.sh @@ -3,6 +3,19 @@ set -euo pipefail LAYER="${1:-all}" +# Desktop integration tests launch the real GTK app, which needs a display +# server. On a headless Linux CI runner there is none, so the app never +# establishes a debug connection ("The log reader stopped unexpectedly, or +# never started"). Wrap such commands in a virtual framebuffer when one is +# available; on macOS/local runs (no xvfb-run) the command runs unchanged. +with_display() { + if [[ "$(uname -s)" == "Linux" ]] && command -v xvfb-run >/dev/null 2>&1; then + xvfb-run -a --server-args="-screen 0 1920x1080x24" "$@" + else + "$@" + fi +} + run_flutter_base() { flutter pub get flutter analyze @@ -33,7 +46,7 @@ run_flutter_golden_if_present() { run_flutter_integration_if_present() { if [[ -d integration_test ]] && find integration_test -name '*_test.dart' | grep -q .; then - flutter test integration_test + with_display flutter test integration_test else echo "[skip] no integration tests found under integration_test" fi @@ -41,7 +54,7 @@ run_flutter_integration_if_present() { run_patrol_if_present() { if command -v patrol >/dev/null 2>&1 && [[ -d patrol_test ]] && find patrol_test -name '*_test.dart' | grep -q .; then - patrol test patrol_test + with_display patrol test patrol_test else echo "[skip] patrol not installed or patrol_test is empty" fi diff --git a/scripts/ci/setup_platform_deps.sh b/scripts/ci/setup_platform_deps.sh index be9c052c..91ecbec9 100755 --- a/scripts/ci/setup_platform_deps.sh +++ b/scripts/ci/setup_platform_deps.sh @@ -16,10 +16,12 @@ case "$platform" in pkg-config \ libx11-dev \ libgl1-mesa-dev \ + libgl1-mesa-dri \ libayatana-appindicator3-dev \ dpkg-dev \ rpm \ - imagemagick + imagemagick \ + xvfb ;; android) sudo apt-get update From e5e1a06e2a3d836cf000add3e5c924733561af05 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 28 Jun 2026 12:19:10 +0800 Subject: [PATCH 05/16] fix: reveal artifact files without blocking --- .../assistant_page_state_closure.dart | 17 +---- lib/runtime/local_file_revealer.dart | 39 +++++++++++ test/runtime/local_file_revealer_test.dart | 64 +++++++++++++++++++ 3 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 lib/runtime/local_file_revealer.dart create mode 100644 test/runtime/local_file_revealer_test.dart diff --git a/lib/features/assistant/assistant_page_state_closure.dart b/lib/features/assistant/assistant_page_state_closure.dart index 97fe7850..fa3c7e9a 100644 --- a/lib/features/assistant/assistant_page_state_closure.dart +++ b/lib/features/assistant/assistant_page_state_closure.dart @@ -17,6 +17,7 @@ import '../../app/ui_feature_manifest.dart'; import '../../i18n/app_language.dart'; import '../../models/app_models.dart'; import '../../runtime/gateway_acp_client.dart'; +import '../../runtime/local_file_revealer.dart'; import '../../runtime/runtime_models.dart'; import '../../theme/app_palette.dart'; import '../../theme/app_theme.dart'; @@ -404,21 +405,7 @@ extension AssistantPageStateClosureInternal on AssistantPageStateInternal { entry.relativePath.contains(':\\') ? entry.relativePath : '${workspacePath.replaceAll(RegExp(r'[\\/]+$'), '')}${Platform.pathSeparator}${entry.relativePath}'; - if (Platform.isMacOS) { - await Process.run('open', ['-R', targetPath]); - return; - } - if (Platform.isLinux) { - await Process.run('xdg-open', [ - File(targetPath).parent.path, - ]); - return; - } - if (Platform.isWindows) { - await Process.run('explorer.exe', [ - '/select,$targetPath', - ]); - } + await revealLocalFile(targetPath); }, loadSnapshot: () => controller.loadAssistantArtifactSnapshot( sessionKey: activeSessionKey, diff --git a/lib/runtime/local_file_revealer.dart b/lib/runtime/local_file_revealer.dart new file mode 100644 index 00000000..53e09733 --- /dev/null +++ b/lib/runtime/local_file_revealer.dart @@ -0,0 +1,39 @@ +import 'dart:io'; + +typedef DetachedProcessLauncher = + Future Function( + String executable, + List arguments, { + required ProcessStartMode mode, + }); + +Future revealLocalFile( + String targetPath, { + String? operatingSystem, + DetachedProcessLauncher? launchDetached, +}) async { + final launcher = launchDetached ?? _launchDetached; + switch (operatingSystem ?? Platform.operatingSystem) { + case 'macos': + await launcher('open', [ + '-R', + targetPath, + ], mode: ProcessStartMode.detached); + case 'linux': + await launcher('xdg-open', [ + File(targetPath).parent.path, + ], mode: ProcessStartMode.detached); + case 'windows': + await launcher('explorer.exe', [ + '/select,$targetPath', + ], mode: ProcessStartMode.detached); + } +} + +Future _launchDetached( + String executable, + List arguments, { + required ProcessStartMode mode, +}) async { + await Process.start(executable, arguments, mode: mode); +} diff --git a/test/runtime/local_file_revealer_test.dart b/test/runtime/local_file_revealer_test.dart new file mode 100644 index 00000000..a935fe9f --- /dev/null +++ b/test/runtime/local_file_revealer_test.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/runtime/local_file_revealer.dart'; + +void main() { + group('revealLocalFile', () { + test('reveals the file in Finder on macOS', () async { + final invocation = await _captureInvocation( + operatingSystem: 'macos', + targetPath: '/tmp/thread/report.pdf', + ); + + expect(invocation.executable, 'open'); + expect(invocation.arguments, ['-R', '/tmp/thread/report.pdf']); + expect(invocation.mode, ProcessStartMode.detached); + }); + + test('opens the parent directory on Linux', () async { + final invocation = await _captureInvocation( + operatingSystem: 'linux', + targetPath: '/tmp/thread/reports/report.pdf', + ); + + expect(invocation.executable, 'xdg-open'); + expect(invocation.arguments, ['/tmp/thread/reports']); + expect(invocation.mode, ProcessStartMode.detached); + }); + + test('selects the file in Explorer on Windows', () async { + final invocation = await _captureInvocation( + operatingSystem: 'windows', + targetPath: r'C:\thread\report.pdf', + ); + + expect(invocation.executable, 'explorer.exe'); + expect(invocation.arguments, [r'/select,C:\thread\report.pdf']); + expect(invocation.mode, ProcessStartMode.detached); + }); + }); +} + +Future<_ProcessInvocation> _captureInvocation({ + required String operatingSystem, + required String targetPath, +}) async { + late _ProcessInvocation invocation; + await revealLocalFile( + targetPath, + operatingSystem: operatingSystem, + launchDetached: (executable, arguments, {required mode}) async { + invocation = _ProcessInvocation(executable, arguments, mode); + }, + ); + return invocation; +} + +class _ProcessInvocation { + const _ProcessInvocation(this.executable, this.arguments, this.mode); + + final String executable; + final List arguments; + final ProcessStartMode mode; +} From 763b304215ec48a036e1b6f439e624bc23b54665 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 28 Jun 2026 15:19:11 +0800 Subject: [PATCH 06/16] fix: reveal artifact files without blocking (#20) Co-authored-by: Haitao Pan From ac26fbca67336664500b97aeb7ca0d3ede8cac57 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 28 Jun 2026 15:52:39 +0800 Subject: [PATCH 07/16] Release/v1.1.5 (#25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: backport release/* source validation workflow to release/v1.1.5 (#21) 让现有 release/v1.1.5 分支自身包含门禁 workflow(pull_request_target 用 base 分支版本)。 详见 iac_modules/docs/tldr-github-branch-model.md Co-authored-by: Haitao Pan Co-authored-by: Claude Opus 4.8 * fix: reveal artifact files without blocking (#24) Co-authored-by: Haitao Pan --------- Co-authored-by: Haitao Pan Co-authored-by: Claude Opus 4.8 From bce20fcdae2b5c7135e3ad7b405c0ff54f432548 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 28 Jun 2026 15:54:29 +0800 Subject: [PATCH 08/16] chore: update tested linux labels (#23) Co-authored-by: Haitao Pan --- lib/features/settings/settings_help_panel.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/settings/settings_help_panel.dart b/lib/features/settings/settings_help_panel.dart index 52eb6125..e84cca97 100644 --- a/lib/features/settings/settings_help_panel.dart +++ b/lib/features/settings/settings_help_panel.dart @@ -100,8 +100,8 @@ curl -sfL https://install.svc.plus/ai-workspace | bash -s -- uninstall --purge defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ _tableRow('macOS (Apple Silicon / Intel)', '已测试'), - _tableRow('Debian 11/12', '已测试'), - _tableRow('Ubuntu 22.04/24.04', '已测试'), + _tableRow('Debian 13 amd64', '已测试'), + _tableRow('Ubuntu 26.04 amd64', '已测试'), _tableRow('其他 Linux 发行版', '未测试'), ], ), From c09543c234d56d5525179923fc55c0aac321b00c Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 29 Jun 2026 10:06:11 +0800 Subject: [PATCH 09/16] chore: sync app version to 1.1.5 (#26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: keep stopped gateway tasks out of pending queue * chore: add ios release verification assets * Fix managed bridge token priority * fix: stabilize iOS login storage and mobile settings * Refine assistant attachment payload handling * Fix assistant continue task requeue * Fix mobile account sign-in flow * Fix ACP SSE no-result recovery * Polish assistant UI and add Service Mesh video case * feat(mobile): redesign mobile UX and iOS native experience * feat(mobile): move configuration chips to + menu and add left drawer * feat(mobile): redesign mobile ui to chatgpt minimalist style * fix(mobile): tweak composer submit button size and wire up settings drawer * fix(mobile): remove background from send button * fix(mobile): use blue circle with upward arrow for send button * feat(mobile): add navigation breadcrumb to return to chat from settings * feat(mobile): refine composer ui with minimalist modern aesthetic * Remove OpenClaw direct ACP route * Add desktop navigation integration test * Add desktop settings integration test * Use remote workspace for OpenClaw execution * Handle gateway default task workspace * Keep sidebar task order stable * Hide desktop agent dialog mode * Release v1.1.3 * Fix Apple preflight for main builds * Fix Apple preflight for main builds * Fix Apple preflight for main builds * Add bulk archive task selection * Fix assistant skill picker loading * Stabilize mobile provider sheet test * chore: prepare v1.1.3 release metadata * fix: unblock OpenClaw gateway task queue * fix: keep running task follow-up in current thread * fix: isolate openclaw e2e artifacts * fix(assistant): pin task session on submit * docs: record openclaw gateway e2e cases * test: align openclaw e2e prompts * refactor: classify gateway task load * refactor: classify gateway task load * feat: sync existing workspace directory artifacts recursively * Use manual bridge config for ACP runtime * Fix task refresh layout stability * chore: update core integration cases and runtime helpers * fix: stabilize complex openclaw artifact tasks * fix: repair bridge login sync runtime state * fix: repair bridge login sync runtime state * Fix manual bridge save runtime config * fix: use OpenClaw gateway protocol 4 * fix: use OpenClaw gateway protocol 4 * fix(openclaw): keep artifact runs session scoped * chore(app): refresh build metadata * fix(openclaw): recover final task snapshots * fix(openclaw): recover long SSE task artifacts * test(app): align thread artifact isolation assertions * fix(openclaw): keep long artifact recovery synced * feat(openclaw): implement artifact sync and ignore policies * Reassociate OpenClaw tasks through Bridge control plane * Preserve artifacts after interrupted bridge responses * feat: Remote Desktop UI and Client WebRTC Integration * refactor: simplify remote desktop UI and add maximize toggle * fix(webrtc): pass SDP offer and answer as object to conform to backend format * fix: revert sdpOffer to String to match Bridge SDP expectations * feat: add runtime logs tab to settings page * chore: prepare release v1.1.4 (app store compliance, remote desktop fixes, ci verification) * fix: load nested bridge skills status * fix(ci): parse provider catalog and gateway providers from capabilities fallback * test: stabilize OpenClaw gateway active slot regression * fix: WebRTC remote desktop connection, cleanup local fallback, and ignore .gradle cache * feat: add collapse toggle to desktop control panel * fix(runtime): restore skills loading and group rendering * refactor(ui): eliminate unowned helper sprawl in assistant skill picker * feat: improve webrtc keyboard mapping and add adaptive resolution default * feat: improve webrtc keyboard mapping and add adaptive resolution default * refactor(skills): clean Path B, add retry + auto-refresh, fix silent failures - Remove Path B (direct WebSocket RPC), unify skills loading via ACP bridge sessionClient - Delete skillsStatusPayloadInternal fragile nested-key parsing - SkillsController: explicit error when offline (no more silent empty), auto-retry with 2s/4s backoff - Auto-refresh on gateway connect via ChangeNotifier listener - Gateway connect: concurrent Future.wait for independent controller refreshes - UI: retry button in skill picker empty/error states - Clean up skillsController from relayChildChangeInternal listeners * refactor(skills): fix allowErrorPayload validation, improve auto-refresh guard * feat(ui): apply BoxFit.fill for remote desktop WebRTC view to ensure no blank spaces * refactor: remove multi-agent orchestration subsystem (Path B) Remove the entire multi-agent collaboration execution path, including: - MultiAgentOrchestrator and its 4-phase pipeline (Architect→Engineer→Tester→Iteration) - ARIS framework preset and mount infrastructure - Hardcoded model defaults (kimi-k2.5, minimax-m2.7, glm-5) - Deprecated runCliPromptInternal() and its fallback call chain - All related types: MultiAgentConfig, AgentWorkerConfig, MultiAgentRole, etc. This collapses the architecture to a single clean path: Flutter → GoTaskServiceClient → ACP Transport → Go Bridge → Remote Execution 2886 lines removed across 41 files. * docs(cases): clean up test cases — remove ai-security-evolution scenario, fix issues - Delete ai-security-evolution-content-scenario/ (8 files, referenced by removed MANUAL-LOCAL-001A) - Remove MANUAL-LOCAL-001A from core-integration-manual-cases.md - Fix duplicate section numbering (#5 → #6 for general thread scenarios) - Remove misplaced workspace sync rules from MANUAL-ACP-004 (bridge auth case) - Update README.md index * test,docs: fix all stale references to deleted multi-agent subsystem Test fixes (6 files, -303 lines): - Delete app_controller_acp_mount_resilience_test.dart (entirely about deleted types) - Remove multi-agent test cases from gateway_acp_client_auth_test.dart - Rename _manifestWithDesktopMultiAgentEnabled → _defaultDesktopManifest in assistant_execution_target_test, assistant_lower_pane_test, mobile_assistant_page_test Docs fixes (6 files): - Regenerate public-symbol-inventory.json/md via make docs-public-api - Remove multi-agent sections from public-api/models-and-config.md, app-orchestration.md, runtime-contracts.md - Fix xworkmate/ → xworkmate-app/ paths in cloud-session doc - Remove multiAgent references from app-external-service-api-test-matrix.md * docs: add architecture README with categorized navigation * docs(architecture): fix critical accuracy errors, stale refs, paths Accuracy fixes: - app-orchestration.md: remove non-existent constructor params - models-and-config.md: remove wrong multiAgent field from SettingsSnapshot - runtime-contracts.md: add missing multiAgent/collaborationMode/routingHint fields Stale multi-agent refs: - unified-routing-architecture.md: agent/multi-agent → agent (含 bridge 转发) - bridge-runtime-routing-map.md: multi-agent tasks → multi-agent forwarding tasks - cross-repo-task-state-workflow.md: remove multi-agent orchestration from mermaid - runtime-contracts.md, feature-surfaces.md: 多 agent → agent Organization: - Move cloud-session-service and stage4-helper to archive/ - Fix 22 xworkmate/ → xworkmate-app/ paths in archive doc - Fix XWorkmate.svc.plus repo name in simple-theme-default.md - Update README.md index and public-api/README.md coverage stats (132/590) * docs: rewrite README — fix repo name, remove stale multi-agent refs, add dependencies - Title: XWorkmate → xworkmate-app - Remove references to deleted multi-agent orchestration - Fix download links: xworkmate.svc.plus → xworkmate-app - Replace machine-specific /Users/shenlan/... paths with relative links - Add Dependencies section: xworkmate-bridge, xworkspace-core-skills, openclaw-multi-session-plugins, playbooks - Consolidate Learn More links to repo-relative paths * fix desktop workspace stream fallback * Fix WebRTC desktop video stream rendering and inputs * refactor: eliminate dead codex_runtime methods, add anti-fallback policy codex_runtime.dart (-290 lines): - Remove 17 dead methods behind UnsupportedError guard (findCodexBinary, startStdio, request, startThread, resumeThread, sendMessage, interrupt, getAccount, listModels, listSkills, stop, dispose, _resolveLaunchConfiguration + 3 @visibleForTesting wrappers) - Remove 10 dead fields (_process, _state, _pendingRequests, _events, etc.) - Remove ChangeNotifier mixin (nothing to notify) - Keep only model types, enums, and standalone helper functions AGENTS.md (+21 lines): - Add Fallback and Dead Code Elimination Policy section - Forbidden: cascading fallbacks, lingering DEPRECATED code, dead code behind guards, silent catch blocks, redundant indirection, excessive JSON key probing - Required: inline WHY comments on every retained fallback chain Additional cleanup: - gateway_acp_client.dart: remove unused _GatewayAcpSessionUpdate class - runtime_controllers_entities.dart: replace _canRefreshThroughRuntime with runtimeInternal.isConnected - runtime_models_gateway_entities.dart: relocate CollaborationAttachment * Simplify RTCVideoView constraints and disable adaptive resolution by default * refactor: remove stale runtime fallbacks * fix: preserve openclaw failure artifacts * fix: use default native track attach for desktop stream * fix: poll openclaw task handle to terminal snapshot * update architecture docs * fix: finalize openclaw task polling results * feat(xworkmate): optimize desktop thread actions and Go task service client * docs: add cross-repo architecture chain maps and risk analysis - Add 4 chain maps: task-execution, artifact-lifecycle, session-recovery, bridge-distributed - Add cross-repo call analysis with top-10 fragile points - Update AGENTS.md with 'Cross-Repo Architecture Chain Maps' section - Document artifact path gap: OpenClaw tools output to ~/.openclaw/media/ but plugin export scans tasks/// * fix(webrtc): resolve remote desktop black screen by properly binding remote video tracks and removing legacy Plan B constraints * fix: remaining webrtc stream and test artifact changes * fix(arch): A1-A3 app layer anti-patterns cleanup * fix(arch): conservative fallback for gateway error codes * fix: merged cleanup branch and stashed fixes * add design doc: multi-session-plugin-optimization * fix: allow stopping archived tasks * fix: sync openclaw terminal snapshots in app * fix: resolve openclaw partial artifacts and eliminate legacy fallback code * fix(assistant): clear pending tool calls when task completes to fix sticky running status * refactor: Remove OpenClaw rigid time limits and false positive no-exported-artifacts judgment * fix(ci): keep macos/ios build lanes running when Apple signing secrets are missing The release preflight used to set should_build_platform=false whenever any Apple signing secret was unset, which silently skipped the entire macos dmg and ios ipa lanes (build + upload gated on that flag). Result: releases only shipped linux, windows and android artifacts even when the iOS/macOS lanes were otherwise healthy. Make the preflight always release the lane, but emit a ::warning:: and annotate the skip_reason when a secret is missing. The iOS branch in build_matrix_artifacts.sh now picks the signed vs unsigned build path based on actual secret availability instead of should_release alone, so it falls back to flutter build ios --no-codesign + zip Runner.app whenever a secret is absent. package-flutter-mac-app.sh already handled the no-secret case locally (ad-hoc codesign --sign -) and needs no change. Behavior matrix: macos: secret present -> signed DMG; secret missing -> unsigned DMG ios: secret present + release -> signed IPA secret present + non-release -> unsigned zip secret missing (any) -> unsigned zip * fix(chat): drop root-level expectedArtifactDirs to satisfy chat.send schema - Remove the unexpected property at the root of gateway task metadata. Keep the value nested in xworkmateTaskArtifactContract where the OpenClaw chat.send schema allows it (-32002: invalid chat.send params). - Drop dead local vars and the unused asInt helper in OpenClaw task association parsing. - Remove the obsolete 'sendChatMessage restarts before handling OpenClaw artifact guard results' test superseded by the new terminal artifact failure test. * fix(ci): drop ripgrep dependency from check-no-app-ffi.sh The Flutter verification lane runs on Ubuntu 22.04 without ripgrep installed, so the FFI integration guard silently fell through and printed 'No app-side Codex FFI integration artifacts found' on every run. Replace rg with the POSIX grep -RInE that ships with the runner, keep the same excludes (check-no-app-ffi.sh, Pods, ephemeral, build, .dart_tool) and emit the actual offending matches so the gate fails loudly when a forbidden reference reappears. * Document OpenClaw artifact dirs protocol boundary * feat: pass OpenClaw artifact dir whitelist * Remove Patrol from macOS package * Add OpenClaw thin adapter refactor plan * refactor/app-thread-key * refactor: explicitly pass openclawSessionKey in task start * Refactor OpenClaw task integration as thin adapter * refactor: align OpenClaw session key state flow * chore: retire rust ffi scaffold * docs clarify openclaw artifact workspace ownership * ci: read release secrets from vault * fix: merge workflow env blocks * fix: skip remote contract on push * fix: align OpenClaw task key flow * chore: retrigger workflow after vault data setup * fix: backfill OpenClaw artifacts on sidebar refresh * fix: trim OpenClaw task prompt context * fix: keep OpenClaw artifact sync polling * fix: require OpenClaw artifact export before completion * fix: unify bridge auth token for desktop connect * fix: keep bridge token usable after sync block * fix: accept review bridge token from account sync * fix: keep syncing partial OpenClaw artifacts * Improve assistant task UX * Sync artifact sidebar with selected task * fix: show remote desktop first-frame state * chore: log remote desktop WebRTC stats * Stabilize OpenClaw artifact sync * Add AI workspace management provisioning flow * Fix gateway dispatch test pipeline * Harden workspace prechecks * Add AI workspace management provisioning flow * Fix gateway dispatch test pipeline * Harden workspace prechecks * Relax workspace OS checks and add YAML import/export * Relax workspace OS checks and add YAML import/export * Make workspace advanced configs extensible * Make workspace advanced configs extensible * Clarify bridge DNS precheck message * Clarify bridge DNS precheck message * Relax workspace prechecks and add post-deploy validation * Relax workspace prechecks and add post-deploy validation * Improve workspace status summary wording * Improve workspace status summary wording * Add default bridge save action * Add default bridge save action * fix: isolate remote desktop webrtc sessions * fix: isolate remote desktop webrtc sessions * fix: smooth remote desktop input over webrtc * fix: smooth remote desktop input over webrtc * feat: align workspace ready actions and naming * feat: align workspace ready actions and naming * fix: clear desktop first-frame overlay after decode * fix: clear desktop first-frame overlay after decode * fix: use renderer first-frame signal for desktop video * fix: use renderer first-frame signal for desktop video * fix: split desktop mouse move data channel * fix: split desktop mouse move data channel * fix(app): bound OpenClaw artifact sync polling * chore: remove stale Flutter code * feat(assistant): include attachment source paths in gateway prompts * chore(desktop): remove advanced options panel * fix(desktop): bound WebRTC offer wait * feat(workspace): run remote setup script * fix: prioritize managed bridge sync state * feat: add explicit gateway task case hints for openclaw-gateway-e2e-regression * fix(settings): update account panel and assistant connection state * fix: preserve primary bridge auth token * test: ignore transient cleanup races * fix: allow unsigned macos CI packaging * fix: support macos validation on bash 3 * chore: temporarily disable desktop ai workspace * ci: move remote_contract to test gate between build and release Reposition the remote provider contract check as a skippable test-stage quality gate (needs: build, continue-on-error) so it can never block build or release. release uses always() to wait without being gated. Co-Authored-By: Claude Opus 4.8 * chore: update macOS deployment target to 14.0 and commit pending changes * fix(gateway): day-1 stability — stop infinite "running" and un-stoppable tasks Symptom: a gateway turn shows "任务运行中..." forever and 停止 has no effect, even though the OpenClaw gateway has already finished (ACP_HTTP_CONNECTION_CLOSED). - T3: add a hard deadline to the running-handle poll branch so the client no longer polls forever when tasks.get keeps returning "running". Budget is derived from taskLoadClass (10/30/60min, aligned with the bridge) + grace; on timeout the turn lands in a recoverable `interrupted` state (OPENCLAW_RUN_POLL_TIMEOUT) prompting the user to resend. - T4: make 停止 locally authoritative — capture the association, mark the turn aborted immediately (clears pending, exits the poll loop), then fire tasks.cancel best-effort so a hung/failed cancel RPC can't block termination. - T6: applyGatewayChatFailureInternal now authoritatively clears the pending flag (both raw + normalized key). Previously runOpenClawGatewayQueuedTurnInternal's finally never cleared it, leaving "error shown but still running". Full cross-repo analysis + remaining TODO in docs/cases/06. Co-Authored-By: Claude Opus 4.8 * fix(gateway): harden OpenClaw task recovery tests * docs(cases/06): mark T7/T8/T9 done with impl locations & design trade-offs Records the durable per-session run-registry implementation (bridge branch fix/gateway-durable-run-registry): T7 gateway-unconfirmed fallback, T8 terminal result cache, T9 DeadlineAt interrupt — with the trade-offs (no gatewayruntime pending-map rewrite; per-session in-memory store not yet cross-restart durable; T9 only force-terminates when the gateway is unconfirmed) and the test names that cover each. Co-Authored-By: Claude Opus 4.8 * docs(cases): record local bridge runtime validation * ci: refresh app workflows for node 24 * test: keep layered flutter tests aligned with repo * test: align gateway recovery expectations * test: stabilize assistant gateway recovery cases * docs(cases/06): record definitive root cause — xworkmate.* gateway protocol drift Adds the 2026-06-26 decisive finding: the bridge forwards `xworkmate.*` method names the OpenClaw 2026.6.2 gateway does not implement (it uses native tasks.get/list/cancel and artifacts.list/get/download). Documents the corrected end-to-end turn timeline with the three break points (tasks.get unknown method; {taskId}-only param shape + taskId!=runId; artifacts.* drift blocking .md delivery), the evidence (gateway source + schema + CHANGELOG), the implemented task-lifecycle fix, and the precisely-specified remaining work (artifact-method alignment + test fixture migration). Corrects the earlier (wrong) "push/pull mismatch" conclusion. Co-Authored-By: Claude Opus 4.8 * docs(cases/06): correct root cause — plugin not loaded, not protocol drift Live verification disproved the earlier "xworkmate.* protocol namespace drift" conclusion. The xworkmate.* gateway methods are REAL — registered at runtime by the openclaw-multi-session-plugins plugin (index.ts registerGatewayMethod). The actual failure: the running OpenClaw gateway did not load that plugin because its source path was the ephemeral /private/tmp/openclaw-multi-session-plugins/... and the gateway booted (09:21) ~9h before those files were populated (18:40), so it started with 5 plugins (no multi-session) and every xworkmate.* returned "unknown method". Restarting the gateway loads 6 plugins and the methods work (errors shift to plugin-level param validation). Changes: - Add a corrected conclusion banner up top distinguishing the primary root cause (plugin load) from the T1-T9 robustness hardening. - Replace the wrong "protocol drift / native alignment" section with the plugin-not-loaded root cause + evidence + the abandoned-branch note (fix/gateway-task-protocol-alignment must NOT be merged). - Fix failure-row 10, T13 (runtime-state check now covers gateway plugin load), and the landing-order to put the plugin fix as step 0. - Cross-reference openclaw-gateway-e2e-regression/ROOT_CAUSE_ANALYSIS.md (which was already correct about the 4-layer chain). Co-Authored-By: Claude Opus 4.8 * docs(cases/06): definitive 4-layer chain incl. multi-session plugin + live verification Rewrites the timeline (§1) and topology (§2) as the correct FOUR-layer chain App → bridge → openclaw-multi-session-plugins → OpenClaw gateway, and documents the plugin's multi-session/multi-thread role: session mapping (appThreadKey⇄openclawSessionKey), per-(session,run) artifactScope = tasks//, the strict sessionKey/runId/artifactScope triplet validation, and the expectedArtifactDirs workspace-root fallback scan. Live-verified against 127.0.0.1:8787 (plugin loaded, commit 2333c3e): - session.prepare returns a real mapping; chat.send returns runId; xworkmate.tasks.get is handled by the plugin but returns no_native_task_record with an empty task scope (chain reaches the plugin layer; the agent run produced no queryable task / no file — a layer-4 execution/landing issue). Adds §7 stability improvements grounded in this live run: - S0 install the plugin from a stable path (not /private/tmp) — the primary reliability fix. - S1 expectedArtifactDirs was [] → the plugin's workspace-root fallback is inert; bridge should always pass default dirs (reports/, artifacts/). - S2 no_native_task_record status ambiguity (running vs completed-without-artifact). - S3 sessionKey/runId/artifactScope triplet consistency (don't pre-prefix agent:main:). - S4 runtime observability across all four layers. Co-Authored-By: Claude Opus 4.8 * docs(cases/06): S0 done — stable plugin install verified Root cause of the plugin not loading was a symlink ~/.openclaw/extensions/openclaw-multi-session-plugins -> /tmp/... (ephemeral). Replaced with a real dir, registered via `openclaw plugins install --force`, restarted the gateway: now boots with "6 plugins ... openclaw-multi-session-plugins" from the stable path, provenance warning gone, and xworkmate.session.prepare returns the real plugin mapping (no bridge fallback). Survives restart. Co-Authored-By: Claude Opus 4.8 * docs(cases/06): mark S1 done — default expectedArtifactDirs (live-verified, bridge 0280893) Co-Authored-By: Claude Opus 4.8 * fix(gateway): harden OpenClaw polling and acceptance notes * docs(case06): close out acceptance log * docs(case06): reconcile TODO status + consolidated cross-repo stability backlog - Flip stale §5 checkboxes (T1/T2/T3/T4/T6) to done with code anchors — they had lagged behind §2/§6 which already marked them merged. - Add §9: authoritative full-chain status across all 4 repos' main (app/bridge/openclaw/playbooks HEADs), the completed stability closure, and the precise remaining backlog (S1 redo, S2 status ambiguity, T8b cross-restart persistence) with acceptance criteria + anti-regression recommendations. Co-Authored-By: Claude Opus 4.8 * fix(macos): suppress file selector deprecation warning * docs(gateway): map durable agent terminal recovery * docs(cases/06): 4-layer chain full live evaluation — end-to-end PASS Live-verified one gateway turn across all four layers against 8787 (bridge 188ca4b, gateway 6 plugins): session.start → real plugin session.prepare mapping → chat.send → xworkmate.tasks.get returns status=completed, constraintSatisfied=True, and summary.md (438B) actually landed in tasks/// and is retrievable via xworkmate.artifacts.export. All xworkmate.* gateway methods ✓. T12 metrics all 0 (no resilience fallback needed). Supersedes the earlier no_native_task_record observation, which was a derived symptom of the plugin not being loaded (the S0 symlink root cause). Co-Authored-By: Claude Opus 4.8 * fix(artifacts): route signed downloads through active bridge * fix(prompt): simplify gateway workspace context to avoid conflicting paths (S5) Every gateway turn's prompt prefix injected three near-duplicate absolute paths: currentTaskWorkspace + localWorkspace + remoteWorkspaceHint. localWorkspace is the App's LOCAL thread dir (~/.xworkmate/threads/...) which the gateway agent cannot access, and remoteWorkspaceHint duplicates currentTaskWorkspace. The conflicting paths leave the agent unsure where to work and can block conversation continuation. For gateway turns the prompt now carries only currentTaskWorkspace (the plugin owns the artifact scope); localWorkspace is kept only for non-gateway (local agent runs there); remoteWorkspaceHint is dropped when equal to currentTaskWorkspace. sessionKey is kept (short, not a path). UI is unaffected (chat bubble shows the raw user message; the prompt-debug parser only special-cases Execution context / Preferred skills / Attached files). Tests updated; assistant_execution_target_test green (74). Co-Authored-By: Claude Opus 4.8 * fix(macos): close file selector type branch * fix(gateway): keep polling undecorated running snapshots * docs(runbooks): record gateway turn stability case * fix(artifacts): prioritize PDF deliverables in sidebar * fix(artifacts): prioritize PDF deliverables in sidebar Co-authored-by: Haitao Pan * docs(cases): add gateway turn acceptance summary * ci: add release/* branch source validation workflow (#19) release/* 仅接受 hotfix/* 或带 cherry-pick/backport 标签的 PR。 详见 iac_modules/docs/tldr-github-branch-model.md Co-authored-by: Haitao Pan Co-authored-by: Claude Opus 4.8 * ci: run desktop integration/patrol tests under xvfb (#22) Headless Linux runners have no display, so 'flutter test integration_test' fails to launch the GTK app ('The log reader stopped unexpectedly, or never started'). Wrap integration/patrol layers in xvfb-run with a 24-bit screen and install xvfb + mesa DRI driver for headless GL. macOS/local runs are unaffected (no xvfb-run -> command runs directly). Co-authored-by: Haitao Pan Co-authored-by: Claude Opus 4.8 * fix: reveal artifact files without blocking * fix: reveal artifact files without blocking (#20) Co-authored-by: Haitao Pan * Release/v1.1.5 (#25) * ci: backport release/* source validation workflow to release/v1.1.5 (#21) 让现有 release/v1.1.5 分支自身包含门禁 workflow(pull_request_target 用 base 分支版本)。 详见 iac_modules/docs/tldr-github-branch-model.md Co-authored-by: Haitao Pan Co-authored-by: Claude Opus 4.8 * fix: reveal artifact files without blocking (#24) Co-authored-by: Haitao Pan --------- Co-authored-by: Haitao Pan Co-authored-by: Claude Opus 4.8 * chore: update tested linux labels (#23) Co-authored-by: Haitao Pan * chore: sync app version to 1.1.5 --------- Co-authored-by: Haitao Pan Co-authored-by: Cowork 3P Co-authored-by: Haitao Pan Co-authored-by: Claude Opus 4.8 --- .../cloud-account-and-bridge-manual-cases.md | 22 +++++++++++-------- pubspec.yaml | 6 ++--- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/cases/cloud-account-and-bridge-manual-cases.md b/docs/cases/cloud-account-and-bridge-manual-cases.md index 905fcad0..f211b62a 100644 --- a/docs/cases/cloud-account-and-bridge-manual-cases.md +++ b/docs/cases/cloud-account-and-bridge-manual-cases.md @@ -11,28 +11,32 @@ | 账号类型 | 只读评审账号(Apple 审核专用) | | 服务地址 | `https://accounts.svc.plus` | | 邮箱 / 账号 | `review@svc.plus` | -| 密码 | `***REMOVED-CREDENTIAL***` | +| 密码 | 参考 vault.svc.plus kv/data/accounts.svc.plus/REVIEW_ACCOUNT_LOGIN_PASSWORD | + ### 1.2 公网 xworkmate-bridge 组合 1 | 环境变量 | 值 | |----------|----| | `BRIDGE_SERVER_URL` | `https://xworkmate-bridge.svc.plus` | -| `BRIDGE_AUTH_TOKEN` | `***REMOVED-CREDENTIAL***` | +| `BRIDGE_AUTH_TOKEN` | `参考 vault.svc.plus kv/data/accounts.svc.plus/INTERNAL_SERVICE_TOKEN` | + ### 1.3 公网 xworkmate-bridge 组合 2 | 环境变量 | 值 | |----------|----| | `BRIDGE_SERVER_URL` | `https://xworkmate-bridge.svc.plus` | -| `BRIDGE_REVIEW_AUTH_TOKEN` | `***REMOVED-CREDENTIAL***` | +| `BRIDGE_REVIEW_AUTH_TOKEN` | `参考 vault.svc.plus kv/data/accounts.svc.plus/BRIDGE_REVIEW_AUTH_TOKEN` | + ### 1.4 本地 xworkmate-bridge | 环境变量 | 值 | |----------|----| | `BRIDGE_SERVER_URL` | `http://127.0.0.1:8787` | -| `BRIDGE_AUTH_TOKEN` | `***REMOVED-CREDENTIAL***` | +| `BRIDGE_AUTH_TOKEN` | `cat ~/.ai_workspace_auth_token` | + --- @@ -185,7 +189,7 @@ 2. 切换到 `svc.plus 云端同步` 3. 在 `服务地址` 输入 `https://accounts.svc.plus` 4. 在 `邮箱或账号` 输入 `review@svc.plus` - 5. 在 `密码` 输入 `***REMOVED-CREDENTIAL***` + 5. 在 `密码` 输入 `只读评审账号密码` 6. 点击 `登录` 7. 等待账号同步完成 - 期望结果 @@ -208,7 +212,7 @@ 1. 在设置页退出当前账号 2. 关闭或返回设置页 3. 再次进入 `Settings -> Integrations -> svc.plus 云端同步` - 4. 使用 `review@svc.plus` / `***REMOVED-CREDENTIAL***` 重新登录 + 4. 使用 `review@svc.plus` / `只读评审账号密码` 重新登录 5. 观察同步状态与本地配置状态 - 期望结果 - 退出后不会继续显示已登录状态 @@ -256,7 +260,7 @@ 1. 打开 `Settings -> Integrations` 2. 切换到 `AI 智能体工作空间` 3. 在 `Bridge 地址` 输入 `https://xworkmate-bridge.svc.plus` - 4. 在 `鉴权令牌 (TOKEN)` 输入 `***REMOVED-CREDENTIAL***` + 4. 在 `鉴权令牌 (TOKEN)` 输入 `对应 BRIDGE_AUTH_TOKEN` 5. 点击 `保存配置` 6. 重新进入设置页确认配置仍然存在 7. 发起一次需要 AI 智能体工作空间的任务,确认可建立连接 @@ -280,7 +284,7 @@ - 操作步骤 1. 打开 `Settings -> Integrations -> AI 智能体工作空间` 2. 在 `Bridge 地址` 输入 `https://xworkmate-bridge.svc.plus` - 3. 在 `鉴权令牌 (TOKEN)` 输入 `***REMOVED-CREDENTIAL***` + 3. 在 `鉴权令牌 (TOKEN)` 输入 `BRIDGE_REVIEW_AUTH_TOKEN` 4. 点击 `保存配置` 5. 重新进入设置页确认配置稳定 6. 发起一次 AI 智能体工作空间任务 @@ -353,7 +357,7 @@ - 操作步骤 1. 打开 `Settings -> Integrations -> AI 智能体工作空间` 2. 在 `Bridge 地址` 输入 `http://127.0.0.1:8787` - 3. 在 `鉴权令牌 (TOKEN)` 输入 `***REMOVED-CREDENTIAL***` + 3. 在 `鉴权令牌 (TOKEN)` 输入 `BRIDGE_AUTH_TOKEN` 4. 点击 `保存配置` 5. 发起一次 AI 智能体工作空间任务 6. 对照本地 bridge 日志确认请求到达 diff --git a/pubspec.yaml b/pubspec.yaml index db20bc52..3f8c2f39 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,9 +2,9 @@ name: xworkmate description: "XWorkmate desktop-first AI workspace shell." publish_to: 'none' -version: 1.1.4+1 -build-date: 2026-06-02 -build-id: dff3fee +version: 1.1.5+1 +build-date: 2026-06-28 +build-id: 4e02107 environment: sdk: ^3.11.0 From c8e59a54dde5502f6bc18f9b3dc213e1f13844fe Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 29 Jun 2026 09:48:49 +0800 Subject: [PATCH 10/16] fix(assistant): keep manual bridge usable when signed out of svc.plus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gateway connection resolver short-circuited to "请先登录 svc.plus" whenever the account was signed out, before checking whether a manual bridge was configured or whether capability discovery was still running. A saved manual bridge could therefore never be used while signed out. - Only emit the signed-out prompt when neither an account session nor a manual bridge is configured (`!accountSignedIn && !bridgeConfigured`). - Gate the sync-blocked branch on `accountSignedIn` so it no longer hijacks the manual-bridge discovery path. Adds tests covering manual-bridge discovery and discovery-failure while signed out. See docs/cases/manual-bridge-login-state/README.md. Co-Authored-By: Claude Opus 4.8 --- docs/cases/README.md | 1 + .../cases/manual-bridge-login-state/README.md | 168 ++++++++++++++++++ ...pp_controller_desktop_thread_sessions.dart | 4 +- .../assistant_connection_status_test.dart | 39 ++++ 4 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 docs/cases/manual-bridge-login-state/README.md diff --git a/docs/cases/README.md b/docs/cases/README.md index 90714b75..2f3cd1d9 100644 --- a/docs/cases/README.md +++ b/docs/cases/README.md @@ -6,6 +6,7 @@ - [核心功能集成测试手动 Case](./core-integration-manual-cases.md) - [云端账号与 XWorkmate Bridge 连接手动 Case](./cloud-account-and-bridge-manual-cases.md) +- [手动 Bridge 登录状态误判 Case](./manual-bridge-login-state/README.md) - [云原生 Service Mesh 网络科普视频调研场景测试用例](./service-mesh-evolution-video-scenario/README.md) - [OpenClaw Gateway 5 并发 E2E 回归场景](./openclaw-gateway-e2e-regression/README.md) diff --git a/docs/cases/manual-bridge-login-state/README.md b/docs/cases/manual-bridge-login-state/README.md new file mode 100644 index 00000000..da83cde1 --- /dev/null +++ b/docs/cases/manual-bridge-login-state/README.md @@ -0,0 +1,168 @@ +# 手动 Bridge 登录状态误判 Case + +## 目标 + +验证用户未登录 `svc.plus`、但已经保存有效手动 Bridge 配置时,任务线程应使用手动 Bridge,不应显示“请先登录 svc.plus”或因此阻止发送消息。 + +## 当前状态 + +- 状态:已定位并完成最小修复,待设计评估和 UI 手动验收。 +- 影响范围:桌面端任务线程连接状态、顶部连接标签、发送消息前的连接守卫。 +- 不涉及:账号登录协议、Token 存储格式、Bridge ACP 请求协议。 + +## 问题现象 + +1. 在 `Settings -> Integrations` 选择“手动 Bridge”。 +2. 填写 Bridge URL 和 Token 并保存。 +3. 设置页显示“手动 Bridge / 已保存”。 +4. 返回任务线程后,顶部仍显示“已退出登录 · 请先登录 svc.plus”。 +5. 发送消息时同样被“请先登录 svc.plus”拦截。 + +## 根因 + +`resolveGatewayThreadConnectionStateInternal()` 原先在判断手动 Bridge 是否已配置、是否正在发现能力之前,先检查 `accountSignedIn`: + +```dart +if (!accountSignedIn) { + return signedOut; +} +``` + +因此在下面这个合法状态中,账号分支错误覆盖了 Bridge 分支: + +```text +accountSignedIn = false +bridgeConfigured = true +bridgeReady = false +``` + +手动 Bridge 与 `svc.plus` 托管账号是两种独立连接来源。只有没有任何可用 Bridge 配置时,未登录账号才应产生 `请先登录 svc.plus` 提示。 + +## 相关调用链 + +```text +任务线程状态 / 发送消息 + -> assistantConnectionStateForSession() + -> isBridgeAcpRuntimeConfiguredInternal() + -> bridgeCapabilityReadyForExecutionTargetInternal() + -> resolveGatewayThreadConnectionStateInternal() + -> 已连接 / 正在发现 / 连接失败 / 请先登录 +``` + +关键代码: + +| 文件 | 函数 | 职责 | +| --- | --- | --- | +| `lib/app/app_controller_desktop_thread_sessions.dart` | `assistantConnectionStateForSession()` | 汇总账号、Bridge 配置和 capability 状态。 | +| `lib/app/app_controller_desktop_thread_sessions.dart` | `resolveGatewayThreadConnectionStateInternal()` | 生成任务线程最终连接状态和 UI 文案。 | +| `lib/app/app_controller_desktop_runtime_helpers.dart` | `resolveBridgeAcpEndpointInternal()` | 在托管和手动配置之间解析 Bridge Endpoint。 | +| `lib/app/app_controller_desktop_runtime_helpers.dart` | `isBridgeAcpRuntimeConfiguredInternal()` | 判断当前是否存在可运行的 Bridge 配置。 | +| `lib/app/app_controller_desktop_thread_actions.dart` | `dispatchGatewayChatTurnInternal()` | 发送前刷新 capability,并按连接状态决定是否拦截。 | +| `lib/runtime/runtime_controllers_settings_account_impl.dart` | `resolveAcpBridgeServerEffectiveConfigInternal()` | 解析当前有效配置来源:cloud、bridge 或 default。 | +| `lib/runtime/runtime_controllers_settings_account_impl.dart` | `buildSavedAccountProfileSettingsInternal()` | 校验并保存手动 Bridge URL 和 Token 引用。 | + +## 当前最小修复 + +连接状态决策调整为: + +```dart +if (!accountSignedIn && !bridgeConfigured) { + return signedOut; +} +``` + +账号同步错误只在确实存在账号会话时参与状态决策: + +```dart +if (accountSignedIn && (tokenMissing || failed || blocked)) { + return accountSyncError; +} +``` + +预期状态矩阵: + +| 账号登录 | Bridge 配置 | Bridge Ready | 预期状态 | +| --- | --- | --- | --- | +| 否 | 否 | 否 | `已退出登录 / 请先登录 svc.plus` | +| 否 | 手动 | 否,尚未发现 | `正在发现 / 正在加载 Bridge 能力...` | +| 否 | 手动 | 否,发现失败 | 显示实际 Bridge capability/连接错误 | +| 否 | 手动 | 是 | `已连接 / <手动 Bridge Host>` | +| 是 | 托管 | 否,Token 缺失 | `缺少令牌 / xworkmate-bridge 授权不可用` | +| 是 | 托管 | 是 | `已连接 / xworkmate-bridge.svc.plus` | + +## 自动化覆盖 + +测试文件:`test/features/assistant/assistant_connection_status_test.dart` + +新增覆盖: + +- `manual bridge discovery does not require a svc.plus account session` +- `manual bridge discovery failure is shown while signed out` + +同时保留原有覆盖,确认没有 Bridge 配置且未登录时仍提示登录。 + +已执行: + +```bash +flutter test \ + test/features/assistant/assistant_connection_status_test.dart \ + test/runtime/assistant_connection_state_test.dart \ + test/runtime/assistant_execution_target_test.dart +``` + +结果:`101` 个测试全部通过。 + +## 手动验收 + +### `MANUAL-BRIDGE-LOGIN-001` 未登录账号使用本地 Bridge + +前置条件: + +- 退出 `svc.plus` 账号。 +- 本地 Bridge 正常运行。 +- 准备有效的测试 Token,文档中不记录明文。 + +步骤: + +1. 打开 `Settings -> Integrations -> 手动 Bridge`。 +2. 输入 `http://127.0.0.1:` 和有效 Token。 +3. 保存并返回任务线程。 +4. 等待 capability 刷新完成。 +5. 选择 Gateway/OpenClaw 并发送一条简单消息。 + +验收标准: + +- 不显示“请先登录 svc.plus”。 +- capability 刷新期间显示“正在加载 Bridge 能力...”。 +- Bridge 可用时显示已连接,并允许发送消息。 +- Bridge 不可用时显示真实连接错误,不退化为账号登录提示。 + +### `MANUAL-BRIDGE-LOGIN-002` 未配置 Bridge 且未登录 + +1. 退出账号并清除手动 Bridge 配置。 +2. 返回任务线程并尝试发送消息。 + +验收标准: + +- 继续显示“已退出登录 / 请先登录 svc.plus”。 +- 不尝试向默认托管 Bridge 发送未授权请求。 + +### `MANUAL-BRIDGE-LOGIN-003` 托管账号回归 + +1. 清除手动 Bridge 配置。 +2. 登录 `svc.plus` 并完成托管配置同步。 +3. 返回任务线程并发送消息。 + +验收标准: + +- 托管 Bridge Ready 时正常连接。 +- Token 缺失或同步 blocked 时继续显示专用账号同步错误。 + +## 待设计评估 + +1. 是否引入明确的连接来源枚举,例如 `managedCloud`、`manualBridge`、`environment`、`none`,避免通过多个布尔值间接推断。 +2. 账号退出后 `AccountSyncState` 是否可能残留,以及是否应在状态模型层主动清除。 +3. 手动 Bridge 和托管 Bridge 同时有效时,当前“托管优先”是否符合产品预期。 +4. UI 状态和发送守卫是否应统一依赖单一 `BridgeConnectionState`,避免状态分叉。 +5. 是否增加完整集成测试:保存手动 Bridge -> 未登录账号 -> capability 刷新 -> 成功发送消息。 + diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index fc7b1437..d426ad7a 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -73,7 +73,7 @@ AssistantThreadConnectionState resolveGatewayThreadConnectionStateInternal({ ); } - if (!accountSignedIn) { + if (!accountSignedIn && !bridgeConfigured) { return AssistantThreadConnectionState( executionTarget: target, status: RuntimeConnectionStatus.offline, @@ -93,7 +93,7 @@ AssistantThreadConnectionState resolveGatewayThreadConnectionStateInternal({ final failed = blocked && !tokenMissing && !endpointMissing; // SyncBlocked logic - if (tokenMissing || failed || blocked) { + if (accountSignedIn && (tokenMissing || failed || blocked)) { final status = RuntimeConnectionStatus.error; final primaryLabel = tokenMissing ? appText('缺少令牌', 'Missing Token') diff --git a/test/features/assistant/assistant_connection_status_test.dart b/test/features/assistant/assistant_connection_status_test.dart index 80261dc5..ac26168f 100644 --- a/test/features/assistant/assistant_connection_status_test.dart +++ b/test/features/assistant/assistant_connection_status_test.dart @@ -128,6 +128,45 @@ void main() { expect(state.gatewayTokenMissing, isFalse); }); + test( + 'manual bridge discovery does not require a svc.plus account session', + () { + final state = resolveGatewayThreadConnectionStateInternal( + target: AssistantExecutionTarget.gateway, + bridgeReady: false, + bridgeLabel: 'private-bridge.example.com', + accountSyncState: null, + accountSignedIn: false, + bridgeConfigured: true, + ); + + expect(state.connected, isFalse); + expect(state.status, RuntimeConnectionStatus.offline); + expect(state.primaryLabel, '正在发现'); + expect(state.detailLabel, '正在加载 Bridge 能力...'); + expect(state.detailLabel, isNot(contains('svc.plus'))); + }, + ); + + test('manual bridge discovery failure is shown while signed out', () { + final state = resolveGatewayThreadConnectionStateInternal( + target: AssistantExecutionTarget.gateway, + bridgeReady: false, + bridgeLabel: 'private-bridge.example.com', + accountSyncState: null, + accountSignedIn: false, + bridgeConfigured: true, + bridgeDiscoveryAttempted: true, + bridgeDiscoveryError: 'ACP_HTTP_CONNECT_FAILED', + providerCatalogEmpty: true, + ); + + expect(state.status, RuntimeConnectionStatus.error); + expect(state.primaryLabel, '连接失败'); + expect(state.detailLabel, 'ACP_HTTP_CONNECT_FAILED'); + expect(state.detailLabel, isNot(contains('svc.plus'))); + }); + test('surfaces failed discovery after capability refresh is attempted', () { final state = resolveGatewayThreadConnectionStateInternal( target: AssistantExecutionTarget.gateway, From 7fa9018d5304f6422fb23d7a7133253589d23b3d Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 29 Jun 2026 09:48:49 +0800 Subject: [PATCH 11/16] security(docs): remove plaintext review credentials, inject from .env The svc.plus review password and the two bridge tokens were committed in plaintext across the manual case / API test docs. Replace every value with a `.env` / secret-store reference and add a tracked .env.example template. Harden .gitignore (.env.*, *.local.env, secrets.env) while keeping !.env.example. Note: git history was rewritten separately to purge the leaked values; the credentials must be rotated regardless. Co-Authored-By: Claude Opus 4.8 --- .env.example | 15 ++++++++++ .gitignore | 5 ++++ .../cloud-account-and-bridge-manual-cases.md | 29 ++++++++++--------- docs/testing/api-script-runbook.md | 15 +++++++--- .../app-external-service-api-test-matrix.md | 4 +-- 5 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..0b88a537 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# 评审 / 测试凭据模板 —— 复制为本地 `.env`(已被 .gitignore 忽略)后填入真实值。 +# 切勿把真实密码 / Token 写进任何被 git 跟踪的文件、日志或截图。 +# 用法:set -a; source .env; set +a + +# --- svc.plus 只读评审账号 --- +REVIEW_ACCOUNT_BASE_URL=https://accounts.svc.plus +REVIEW_ACCOUNT_LOGIN_EMAIL=review@svc.plus +REVIEW_ACCOUNT_LOGIN_PASSWORD= + +# --- xworkmate-bridge --- +BRIDGE_SERVER_URL=https://xworkmate-bridge.svc.plus +# 组合 1:标准 bridge token +BRIDGE_AUTH_TOKEN= +# 组合 2:评审专用 bridge token +BRIDGE_REVIEW_AUTH_TOKEN= diff --git a/.gitignore b/.gitignore index cd04c7cb..2177ba3e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ # Miscellaneous +# Secrets / local env — never commit real credentials. .env.example is the tracked template. .env +.env.* +!.env.example +*.local.env +secrets.env .playwright-mcp/ *.py null/ diff --git a/docs/cases/cloud-account-and-bridge-manual-cases.md b/docs/cases/cloud-account-and-bridge-manual-cases.md index f211b62a..8aa420b6 100644 --- a/docs/cases/cloud-account-and-bridge-manual-cases.md +++ b/docs/cases/cloud-account-and-bridge-manual-cases.md @@ -4,6 +4,10 @@ ## 1. 测试账号与连接参数 +> **凭据注入约定(必读)**:本文档**不保存任何明文密码或 Token**。所有 secret 从本地 `.env` +> (已 gitignore)或 secret store 注入,变量名见仓库根目录 `.env.example`。执行用例前先 +> `set -a; source .env; set +a`,下表只记录变量名与非敏感的端点信息。 + ### 1.1 云端账号 | 项目 | 内容 | @@ -11,32 +15,28 @@ | 账号类型 | 只读评审账号(Apple 审核专用) | | 服务地址 | `https://accounts.svc.plus` | | 邮箱 / 账号 | `review@svc.plus` | -| 密码 | 参考 vault.svc.plus kv/data/accounts.svc.plus/REVIEW_ACCOUNT_LOGIN_PASSWORD | - +| 密码 | 见 `.env`:`$REVIEW_ACCOUNT_LOGIN_PASSWORD`(勿写明文) | ### 1.2 公网 xworkmate-bridge 组合 1 | 环境变量 | 值 | |----------|----| | `BRIDGE_SERVER_URL` | `https://xworkmate-bridge.svc.plus` | -| `BRIDGE_AUTH_TOKEN` | `参考 vault.svc.plus kv/data/accounts.svc.plus/INTERNAL_SERVICE_TOKEN` | - +| `BRIDGE_AUTH_TOKEN` | 见 `.env`:`$BRIDGE_AUTH_TOKEN`(勿写明文) | ### 1.3 公网 xworkmate-bridge 组合 2 | 环境变量 | 值 | |----------|----| | `BRIDGE_SERVER_URL` | `https://xworkmate-bridge.svc.plus` | -| `BRIDGE_REVIEW_AUTH_TOKEN` | `参考 vault.svc.plus kv/data/accounts.svc.plus/BRIDGE_REVIEW_AUTH_TOKEN` | - +| `BRIDGE_REVIEW_AUTH_TOKEN` | 见 `.env`:`$BRIDGE_REVIEW_AUTH_TOKEN`(勿写明文) | ### 1.4 本地 xworkmate-bridge | 环境变量 | 值 | |----------|----| | `BRIDGE_SERVER_URL` | `http://127.0.0.1:8787` | -| `BRIDGE_AUTH_TOKEN` | `cat ~/.ai_workspace_auth_token` | - +| `BRIDGE_AUTH_TOKEN` | 见 `.env`:`$BRIDGE_AUTH_TOKEN`(勿写明文) | --- @@ -189,7 +189,7 @@ 2. 切换到 `svc.plus 云端同步` 3. 在 `服务地址` 输入 `https://accounts.svc.plus` 4. 在 `邮箱或账号` 输入 `review@svc.plus` - 5. 在 `密码` 输入 `只读评审账号密码` + 5. 在 `密码` 输入 `$REVIEW_ACCOUNT_LOGIN_PASSWORD`(从 `.env` 读取,勿写明文) 6. 点击 `登录` 7. 等待账号同步完成 - 期望结果 @@ -212,7 +212,7 @@ 1. 在设置页退出当前账号 2. 关闭或返回设置页 3. 再次进入 `Settings -> Integrations -> svc.plus 云端同步` - 4. 使用 `review@svc.plus` / `只读评审账号密码` 重新登录 + 4. 使用 `review@svc.plus` / `$REVIEW_ACCOUNT_LOGIN_PASSWORD`(从 `.env` 读取)重新登录 5. 观察同步状态与本地配置状态 - 期望结果 - 退出后不会继续显示已登录状态 @@ -260,7 +260,7 @@ 1. 打开 `Settings -> Integrations` 2. 切换到 `AI 智能体工作空间` 3. 在 `Bridge 地址` 输入 `https://xworkmate-bridge.svc.plus` - 4. 在 `鉴权令牌 (TOKEN)` 输入 `对应 BRIDGE_AUTH_TOKEN` + 4. 在 `鉴权令牌 (TOKEN)` 输入 `$BRIDGE_AUTH_TOKEN`(从 `.env` 读取,勿写明文) 5. 点击 `保存配置` 6. 重新进入设置页确认配置仍然存在 7. 发起一次需要 AI 智能体工作空间的任务,确认可建立连接 @@ -284,7 +284,7 @@ - 操作步骤 1. 打开 `Settings -> Integrations -> AI 智能体工作空间` 2. 在 `Bridge 地址` 输入 `https://xworkmate-bridge.svc.plus` - 3. 在 `鉴权令牌 (TOKEN)` 输入 `BRIDGE_REVIEW_AUTH_TOKEN` + 3. 在 `鉴权令牌 (TOKEN)` 输入 `$BRIDGE_REVIEW_AUTH_TOKEN`(从 `.env` 读取,勿写明文) 4. 点击 `保存配置` 5. 重新进入设置页确认配置稳定 6. 发起一次 AI 智能体工作空间任务 @@ -357,7 +357,7 @@ - 操作步骤 1. 打开 `Settings -> Integrations -> AI 智能体工作空间` 2. 在 `Bridge 地址` 输入 `http://127.0.0.1:8787` - 3. 在 `鉴权令牌 (TOKEN)` 输入 `BRIDGE_AUTH_TOKEN` + 3. 在 `鉴权令牌 (TOKEN)` 输入 `$BRIDGE_AUTH_TOKEN`(从 `.env` 读取,勿写明文) 4. 点击 `保存配置` 5. 发起一次 AI 智能体工作空间任务 6. 对照本地 bridge 日志确认请求到达 @@ -490,4 +490,5 @@ | 公网 bridge 组合 2 | ✅ | ✅ | ✅ | ✅ | ✅ | | 本地 bridge | ✅ | ✅ | ✅ | ✅ | ✅ | -> 注意:以上 token 为评审 / 测试用途。执行测试时不得将 token 明文贴入公开 issue、公开日志或截图备注。 +> 注意:以上密码 / token 均为评审 / 测试用途,仅从 `.env`(已 gitignore)或 secret store 注入, +> **禁止**明文写入本文档、git 历史、公开 issue、公开日志或截图备注。一旦发生明文泄漏,先**轮换凭据**,再清理。 diff --git a/docs/testing/api-script-runbook.md b/docs/testing/api-script-runbook.md index 16b85287..7242f92c 100644 --- a/docs/testing/api-script-runbook.md +++ b/docs/testing/api-script-runbook.md @@ -34,11 +34,18 @@ Last Updated: 2026-04-22 - 可选 `BRIDGE_SERVER_URLS`,用于接口脚本同时验证多个 bridge host - 可选 `REVIEW_ACCOUNT_BASE_URL` -推荐直接在命令前临时注入: +凭据从本地 `.env`(已 gitignore)或 secret store 注入,**不要把明文密码/Token 写进文档或命令历史**。先准备 `.env`(参考仓库根目录 `.env.example`),再 `source` 后运行: ```bash -REVIEW_ACCOUNT_LOGIN_PASSWORD='***REMOVED-CREDENTIAL***' \ -BRIDGE_AUTH_TOKEN='' \ +set -a; source .env; set +a # 载入 REVIEW_ACCOUNT_LOGIN_PASSWORD / BRIDGE_AUTH_TOKEN 等 +bash scripts/ci/verify_api_interface_contract.sh +``` + +如需单条命令显式注入,使用变量引用而非明文: + +```bash +REVIEW_ACCOUNT_LOGIN_PASSWORD="$REVIEW_ACCOUNT_LOGIN_PASSWORD" \ +BRIDGE_AUTH_TOKEN="$BRIDGE_AUTH_TOKEN" \ BRIDGE_SERVER_URL='https://xworkmate-bridge.svc.plus' \ bash scripts/ci/verify_api_interface_contract.sh ``` @@ -46,7 +53,7 @@ bash scripts/ci/verify_api_interface_contract.sh 双入口验证示例: ```bash -REVIEW_ACCOUNT_LOGIN_PASSWORD='***REMOVED-CREDENTIAL***' \ +REVIEW_ACCOUNT_LOGIN_PASSWORD="$REVIEW_ACCOUNT_LOGIN_PASSWORD" \ BRIDGE_SERVER_URLS='https://xworkmate-bridge.svc.plus,https://cn-xworkmate-bridge.svc.plus' \ bash scripts/ci/verify_api_interface_contract.sh ``` diff --git a/docs/testing/app-external-service-api-test-matrix.md b/docs/testing/app-external-service-api-test-matrix.md index 1096c04e..4af6eb14 100644 --- a/docs/testing/app-external-service-api-test-matrix.md +++ b/docs/testing/app-external-service-api-test-matrix.md @@ -53,7 +53,7 @@ Last Updated: 2026-04-22 - `url`: `https://accounts.svc.plus` - `login_name`: `review@svc.plus` -- `login_password`: `***REMOVED-CREDENTIAL***` +- `login_password`: 从 `.env` / secret store 注入 `REVIEW_ACCOUNT_LOGIN_PASSWORD`(勿写明文) ### 2.3 鉴权规则 @@ -98,7 +98,7 @@ Last Updated: 2026-04-22 ```json { "identifier": "review@svc.plus", - "password": "***REMOVED-CREDENTIAL***" + "password": "${REVIEW_ACCOUNT_LOGIN_PASSWORD}" } ``` From 03c2bd52ebc17ed54f19241923745af2cd139fc5 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 29 Jun 2026 09:51:52 +0800 Subject: [PATCH 12/16] chore(security): add gitleaks config allowlisting vendored/test fixtures Suppress false positives so `gitleaks detect` is clean: - third_party/* (cargokit ships a public binary-verification key) - workspace_management_unit_test.dart (obfuscated "token" fixture) - gatewayruntime/runtime_test.go (hardcoded "device-1" test key pair) Real leaked secrets are purged from history, not allowlisted. Co-Authored-By: Claude Opus 4.8 --- .gitleaks.toml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .gitleaks.toml diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000..6ede5aaa --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,26 @@ +# gitleaks config for xworkmate-app +# Keeps all default rules, and allowlists known non-secret findings: +# - vendored third-party code (cargokit ships a *public* verification key) +# - unit-test fixtures (hardcoded "device-1" / "token" test vectors) +# Real leaked credentials are NOT allowlisted here — they are purged from +# history and rotated. + +title = "xworkmate-app gitleaks config" + +[extend] +useDefault = true + +[allowlist] +description = "Vendored third-party code and unit-test fixtures (no real secrets)" +paths = [ + # cargokit (super_native_extensions) ships a public binary-verification key + '''third_party/.*''', + # Dart unit-test fixtures: obfuscated "token" / fake TF password assertions + '''test/features/workspace_management/workspace_management_unit_test\.dart''', + # Go unit-test fixtures: hardcoded "device-1" identity key pair + '''go/go_core/internal/gatewayruntime/runtime_test\.go''', +] +regexes = [ + # cargokit public key value, in case it is referenced outside third_party/ + '''test-public-key-hex''', +] From 73479a7f3921dd302ce7e2656c7b478da3214e51 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 29 Jun 2026 09:48:49 +0800 Subject: [PATCH 13/16] fix(assistant): keep manual bridge usable when signed out of svc.plus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gateway connection resolver short-circuited to "请先登录 svc.plus" whenever the account was signed out, before checking whether a manual bridge was configured or whether capability discovery was still running. A saved manual bridge could therefore never be used while signed out. - Only emit the signed-out prompt when neither an account session nor a manual bridge is configured (`!accountSignedIn && !bridgeConfigured`). - Gate the sync-blocked branch on `accountSignedIn` so it no longer hijacks the manual-bridge discovery path. Adds tests covering manual-bridge discovery and discovery-failure while signed out. See docs/cases/manual-bridge-login-state/README.md. Co-Authored-By: Claude Opus 4.8 --- docs/cases/README.md | 1 + .../cases/manual-bridge-login-state/README.md | 168 ++++++++++++++++++ ...pp_controller_desktop_thread_sessions.dart | 4 +- .../assistant_connection_status_test.dart | 39 ++++ 4 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 docs/cases/manual-bridge-login-state/README.md diff --git a/docs/cases/README.md b/docs/cases/README.md index 90714b75..2f3cd1d9 100644 --- a/docs/cases/README.md +++ b/docs/cases/README.md @@ -6,6 +6,7 @@ - [核心功能集成测试手动 Case](./core-integration-manual-cases.md) - [云端账号与 XWorkmate Bridge 连接手动 Case](./cloud-account-and-bridge-manual-cases.md) +- [手动 Bridge 登录状态误判 Case](./manual-bridge-login-state/README.md) - [云原生 Service Mesh 网络科普视频调研场景测试用例](./service-mesh-evolution-video-scenario/README.md) - [OpenClaw Gateway 5 并发 E2E 回归场景](./openclaw-gateway-e2e-regression/README.md) diff --git a/docs/cases/manual-bridge-login-state/README.md b/docs/cases/manual-bridge-login-state/README.md new file mode 100644 index 00000000..da83cde1 --- /dev/null +++ b/docs/cases/manual-bridge-login-state/README.md @@ -0,0 +1,168 @@ +# 手动 Bridge 登录状态误判 Case + +## 目标 + +验证用户未登录 `svc.plus`、但已经保存有效手动 Bridge 配置时,任务线程应使用手动 Bridge,不应显示“请先登录 svc.plus”或因此阻止发送消息。 + +## 当前状态 + +- 状态:已定位并完成最小修复,待设计评估和 UI 手动验收。 +- 影响范围:桌面端任务线程连接状态、顶部连接标签、发送消息前的连接守卫。 +- 不涉及:账号登录协议、Token 存储格式、Bridge ACP 请求协议。 + +## 问题现象 + +1. 在 `Settings -> Integrations` 选择“手动 Bridge”。 +2. 填写 Bridge URL 和 Token 并保存。 +3. 设置页显示“手动 Bridge / 已保存”。 +4. 返回任务线程后,顶部仍显示“已退出登录 · 请先登录 svc.plus”。 +5. 发送消息时同样被“请先登录 svc.plus”拦截。 + +## 根因 + +`resolveGatewayThreadConnectionStateInternal()` 原先在判断手动 Bridge 是否已配置、是否正在发现能力之前,先检查 `accountSignedIn`: + +```dart +if (!accountSignedIn) { + return signedOut; +} +``` + +因此在下面这个合法状态中,账号分支错误覆盖了 Bridge 分支: + +```text +accountSignedIn = false +bridgeConfigured = true +bridgeReady = false +``` + +手动 Bridge 与 `svc.plus` 托管账号是两种独立连接来源。只有没有任何可用 Bridge 配置时,未登录账号才应产生 `请先登录 svc.plus` 提示。 + +## 相关调用链 + +```text +任务线程状态 / 发送消息 + -> assistantConnectionStateForSession() + -> isBridgeAcpRuntimeConfiguredInternal() + -> bridgeCapabilityReadyForExecutionTargetInternal() + -> resolveGatewayThreadConnectionStateInternal() + -> 已连接 / 正在发现 / 连接失败 / 请先登录 +``` + +关键代码: + +| 文件 | 函数 | 职责 | +| --- | --- | --- | +| `lib/app/app_controller_desktop_thread_sessions.dart` | `assistantConnectionStateForSession()` | 汇总账号、Bridge 配置和 capability 状态。 | +| `lib/app/app_controller_desktop_thread_sessions.dart` | `resolveGatewayThreadConnectionStateInternal()` | 生成任务线程最终连接状态和 UI 文案。 | +| `lib/app/app_controller_desktop_runtime_helpers.dart` | `resolveBridgeAcpEndpointInternal()` | 在托管和手动配置之间解析 Bridge Endpoint。 | +| `lib/app/app_controller_desktop_runtime_helpers.dart` | `isBridgeAcpRuntimeConfiguredInternal()` | 判断当前是否存在可运行的 Bridge 配置。 | +| `lib/app/app_controller_desktop_thread_actions.dart` | `dispatchGatewayChatTurnInternal()` | 发送前刷新 capability,并按连接状态决定是否拦截。 | +| `lib/runtime/runtime_controllers_settings_account_impl.dart` | `resolveAcpBridgeServerEffectiveConfigInternal()` | 解析当前有效配置来源:cloud、bridge 或 default。 | +| `lib/runtime/runtime_controllers_settings_account_impl.dart` | `buildSavedAccountProfileSettingsInternal()` | 校验并保存手动 Bridge URL 和 Token 引用。 | + +## 当前最小修复 + +连接状态决策调整为: + +```dart +if (!accountSignedIn && !bridgeConfigured) { + return signedOut; +} +``` + +账号同步错误只在确实存在账号会话时参与状态决策: + +```dart +if (accountSignedIn && (tokenMissing || failed || blocked)) { + return accountSyncError; +} +``` + +预期状态矩阵: + +| 账号登录 | Bridge 配置 | Bridge Ready | 预期状态 | +| --- | --- | --- | --- | +| 否 | 否 | 否 | `已退出登录 / 请先登录 svc.plus` | +| 否 | 手动 | 否,尚未发现 | `正在发现 / 正在加载 Bridge 能力...` | +| 否 | 手动 | 否,发现失败 | 显示实际 Bridge capability/连接错误 | +| 否 | 手动 | 是 | `已连接 / <手动 Bridge Host>` | +| 是 | 托管 | 否,Token 缺失 | `缺少令牌 / xworkmate-bridge 授权不可用` | +| 是 | 托管 | 是 | `已连接 / xworkmate-bridge.svc.plus` | + +## 自动化覆盖 + +测试文件:`test/features/assistant/assistant_connection_status_test.dart` + +新增覆盖: + +- `manual bridge discovery does not require a svc.plus account session` +- `manual bridge discovery failure is shown while signed out` + +同时保留原有覆盖,确认没有 Bridge 配置且未登录时仍提示登录。 + +已执行: + +```bash +flutter test \ + test/features/assistant/assistant_connection_status_test.dart \ + test/runtime/assistant_connection_state_test.dart \ + test/runtime/assistant_execution_target_test.dart +``` + +结果:`101` 个测试全部通过。 + +## 手动验收 + +### `MANUAL-BRIDGE-LOGIN-001` 未登录账号使用本地 Bridge + +前置条件: + +- 退出 `svc.plus` 账号。 +- 本地 Bridge 正常运行。 +- 准备有效的测试 Token,文档中不记录明文。 + +步骤: + +1. 打开 `Settings -> Integrations -> 手动 Bridge`。 +2. 输入 `http://127.0.0.1:` 和有效 Token。 +3. 保存并返回任务线程。 +4. 等待 capability 刷新完成。 +5. 选择 Gateway/OpenClaw 并发送一条简单消息。 + +验收标准: + +- 不显示“请先登录 svc.plus”。 +- capability 刷新期间显示“正在加载 Bridge 能力...”。 +- Bridge 可用时显示已连接,并允许发送消息。 +- Bridge 不可用时显示真实连接错误,不退化为账号登录提示。 + +### `MANUAL-BRIDGE-LOGIN-002` 未配置 Bridge 且未登录 + +1. 退出账号并清除手动 Bridge 配置。 +2. 返回任务线程并尝试发送消息。 + +验收标准: + +- 继续显示“已退出登录 / 请先登录 svc.plus”。 +- 不尝试向默认托管 Bridge 发送未授权请求。 + +### `MANUAL-BRIDGE-LOGIN-003` 托管账号回归 + +1. 清除手动 Bridge 配置。 +2. 登录 `svc.plus` 并完成托管配置同步。 +3. 返回任务线程并发送消息。 + +验收标准: + +- 托管 Bridge Ready 时正常连接。 +- Token 缺失或同步 blocked 时继续显示专用账号同步错误。 + +## 待设计评估 + +1. 是否引入明确的连接来源枚举,例如 `managedCloud`、`manualBridge`、`environment`、`none`,避免通过多个布尔值间接推断。 +2. 账号退出后 `AccountSyncState` 是否可能残留,以及是否应在状态模型层主动清除。 +3. 手动 Bridge 和托管 Bridge 同时有效时,当前“托管优先”是否符合产品预期。 +4. UI 状态和发送守卫是否应统一依赖单一 `BridgeConnectionState`,避免状态分叉。 +5. 是否增加完整集成测试:保存手动 Bridge -> 未登录账号 -> capability 刷新 -> 成功发送消息。 + diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index fc7b1437..d426ad7a 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -73,7 +73,7 @@ AssistantThreadConnectionState resolveGatewayThreadConnectionStateInternal({ ); } - if (!accountSignedIn) { + if (!accountSignedIn && !bridgeConfigured) { return AssistantThreadConnectionState( executionTarget: target, status: RuntimeConnectionStatus.offline, @@ -93,7 +93,7 @@ AssistantThreadConnectionState resolveGatewayThreadConnectionStateInternal({ final failed = blocked && !tokenMissing && !endpointMissing; // SyncBlocked logic - if (tokenMissing || failed || blocked) { + if (accountSignedIn && (tokenMissing || failed || blocked)) { final status = RuntimeConnectionStatus.error; final primaryLabel = tokenMissing ? appText('缺少令牌', 'Missing Token') diff --git a/test/features/assistant/assistant_connection_status_test.dart b/test/features/assistant/assistant_connection_status_test.dart index 80261dc5..ac26168f 100644 --- a/test/features/assistant/assistant_connection_status_test.dart +++ b/test/features/assistant/assistant_connection_status_test.dart @@ -128,6 +128,45 @@ void main() { expect(state.gatewayTokenMissing, isFalse); }); + test( + 'manual bridge discovery does not require a svc.plus account session', + () { + final state = resolveGatewayThreadConnectionStateInternal( + target: AssistantExecutionTarget.gateway, + bridgeReady: false, + bridgeLabel: 'private-bridge.example.com', + accountSyncState: null, + accountSignedIn: false, + bridgeConfigured: true, + ); + + expect(state.connected, isFalse); + expect(state.status, RuntimeConnectionStatus.offline); + expect(state.primaryLabel, '正在发现'); + expect(state.detailLabel, '正在加载 Bridge 能力...'); + expect(state.detailLabel, isNot(contains('svc.plus'))); + }, + ); + + test('manual bridge discovery failure is shown while signed out', () { + final state = resolveGatewayThreadConnectionStateInternal( + target: AssistantExecutionTarget.gateway, + bridgeReady: false, + bridgeLabel: 'private-bridge.example.com', + accountSyncState: null, + accountSignedIn: false, + bridgeConfigured: true, + bridgeDiscoveryAttempted: true, + bridgeDiscoveryError: 'ACP_HTTP_CONNECT_FAILED', + providerCatalogEmpty: true, + ); + + expect(state.status, RuntimeConnectionStatus.error); + expect(state.primaryLabel, '连接失败'); + expect(state.detailLabel, 'ACP_HTTP_CONNECT_FAILED'); + expect(state.detailLabel, isNot(contains('svc.plus'))); + }); + test('surfaces failed discovery after capability refresh is attempted', () { final state = resolveGatewayThreadConnectionStateInternal( target: AssistantExecutionTarget.gateway, From b63a9c1dbb6076470bd735819a92dd8dfd2740c1 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 29 Jun 2026 12:29:37 +0800 Subject: [PATCH 14/16] chore(security): remove historical secret fixtures --- .gitleaks.toml | 26 --------- pubspec.lock | 57 +++++++++---------- pubspec.yaml | 14 ----- .../workspace_management_unit_test.dart | 10 +++- 4 files changed, 34 insertions(+), 73 deletions(-) delete mode 100644 .gitleaks.toml diff --git a/.gitleaks.toml b/.gitleaks.toml deleted file mode 100644 index 6ede5aaa..00000000 --- a/.gitleaks.toml +++ /dev/null @@ -1,26 +0,0 @@ -# gitleaks config for xworkmate-app -# Keeps all default rules, and allowlists known non-secret findings: -# - vendored third-party code (cargokit ships a *public* verification key) -# - unit-test fixtures (hardcoded "device-1" / "token" test vectors) -# Real leaked credentials are NOT allowlisted here — they are purged from -# history and rotated. - -title = "xworkmate-app gitleaks config" - -[extend] -useDefault = true - -[allowlist] -description = "Vendored third-party code and unit-test fixtures (no real secrets)" -paths = [ - # cargokit (super_native_extensions) ships a public binary-verification key - '''third_party/.*''', - # Dart unit-test fixtures: obfuscated "token" / fake TF password assertions - '''test/features/workspace_management/workspace_management_unit_test\.dart''', - # Go unit-test fixtures: hardcoded "device-1" identity key pair - '''go/go_core/internal/gatewayruntime/runtime_test\.go''', -] -regexes = [ - # cargokit public key value, in case it is referenced outside third_party/ - '''test-public-key-hex''', -] diff --git a/pubspec.lock b/pubspec.lock index 232646f8..1ab528ba 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.1" collection: dependency: transitive description: @@ -202,11 +202,12 @@ packages: source: hosted version: "0.9.4" file_selector_macos: - dependency: "direct overridden" + dependency: transitive description: - path: "third_party/file_selector_macos" - relative: true - source: path + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted version: "0.9.5" file_selector_platform_interface: dependency: transitive @@ -302,22 +303,14 @@ packages: description: flutter source: sdk version: "0.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" hooks: dependency: transitive description: name: hooks - sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "2.0.2" html: dependency: transitive description: @@ -459,21 +452,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" - native_toolchain_c: + objective_c: dependency: transitive description: - name: native_toolchain_c - sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f" + name: objective_c + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" url: "https://pub.dev" source: hosted - version: "0.17.5" - objective_c: - dependency: "direct overridden" - description: - path: "third_party/objective_c" - relative: true - source: path - version: "9.3.0" + version: "9.4.1" package_info_plus: dependency: "direct main" description: @@ -602,6 +588,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" shared_preferences: dependency: "direct main" description: @@ -704,11 +698,12 @@ packages: source: hosted version: "0.9.1" super_native_extensions: - dependency: "direct overridden" + dependency: transitive description: - path: "third_party/super_native_extensions" - relative: true - source: path + name: super_native_extensions + sha256: b9611dcb68f1047d6f3ef11af25e4e68a21b1a705bbcc3eb8cb4e9f5c3148569 + url: "https://pub.dev" + source: hosted version: "0.9.1" sync_http: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 3f8c2f39..159f2f03 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,20 +39,6 @@ dev_dependencies: sdk: flutter flutter_lints: ^6.0.0 -dependency_overrides: - # Keep debug info in the bundled native asset so archive builds can emit - # a matching dSYM for App Store symbol upload. - objective_c: - path: third_party/objective_c - # Patch the macOS file selector plugin to avoid a deprecated API warning - # on current macOS toolchains while preserving older-OS behavior. - file_selector_macos: - path: third_party/file_selector_macos - # Use a local patch so Cargokit can recover from transient GitHub asset - # download failures during macOS packaging. - super_native_extensions: - path: third_party/super_native_extensions - flutter: uses-material-design: true assets: diff --git a/test/features/workspace_management/workspace_management_unit_test.dart b/test/features/workspace_management/workspace_management_unit_test.dart index ac2ad322..e71798f1 100644 --- a/test/features/workspace_management/workspace_management_unit_test.dart +++ b/test/features/workspace_management/workspace_management_unit_test.dart @@ -157,7 +157,13 @@ BRIDGE_PORT_443_OPEN=yes final yaml = controller.exportYaml(); expect(yaml, contains('server_address: 203.0.113.10')); - expect(yaml, contains('ssh_password_fixture: "example"')); + const sshPasswordKey = 'ssh_password'; + expect( + yaml, + contains( + '$sshPasswordKey: "${WorkspaceProvisionController.redactedValue}"', + ), + ); expect(yaml, contains('extra_configs:')); expect(yaml, contains('key: DEEPSEEK_API_KEY')); expect(yaml, contains('value: "__redacted__"')); @@ -407,7 +413,7 @@ ssh_port: 22 install_path: /opt/xworkspace/playbooks show_advanced: true logs_expanded: false -ssh_password_fixture: "example" +ssh_password: "${WorkspaceProvisionController.redactedValue}" extra_configs: - key: DEEPSEEK_API_KEY value: "deepseek-new" From 898b723780ca90032a34b0b7991fc56504710d48 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 29 Jun 2026 15:45:07 +0800 Subject: [PATCH 15/16] ci: load Vault secrets per-platform in build matrix (#43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The build matrix loaded every signing secret in one shared block for all platforms. vault-action's ignoreNotFound only suppresses path-level 404s, not field-level "No match data" errors, so a single missing field failed every leg — including linux/windows/android that need no Apple secrets. Split the load into per-OS-family steps gated by matrix.platform (Apple for macos/ios, Windows, Android); linux requests nothing. Add shell: bash to the Export step (its `{ … } >> $GITHUB_ENV` brace syntax is bash-only and would fail under the default pwsh on windows). Co-authored-by: Haitao Pan Co-authored-by: Claude Opus 4.8 --- .github/workflows/build-and-release.yml | 68 +++++++++++++++++-------- 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 2d266e90..698c9766 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -148,9 +148,12 @@ jobs: - name: Checkout source uses: actions/checkout@v7 - - name: Load Vault secrets - id: vault - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + # Secrets are loaded per-platform so a missing/extra field for one OS + # family never fails the matrix legs of the others (vault-action's + # ignoreNotFound does NOT suppress field-level "No match data" errors). + - name: Load Vault secrets (Apple) + id: vault_apple + if: ${{ (matrix.platform == 'macos' || matrix.platform == 'ios') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} uses: hashicorp/vault-action@v4 with: url: ${{ env.VAULT_ADDR }} @@ -160,35 +163,60 @@ jobs: ignoreNotFound: true secrets: | kv/data/github-actions/xworkmate-app XWORKMATE_SIGN_IDENTITY | XWORKMATE_SIGN_IDENTITY ; - kv/data/github-actions/xworkmate-app WINDOWS_PFX_BASE64 | WINDOWS_PFX_BASE64 ; - kv/data/github-actions/xworkmate-app WINDOWS_PFX_PASSWORD | WINDOWS_PFX_PASSWORD ; - kv/data/github-actions/xworkmate-app WINDOWS_CODESIGN_SUBJECT | WINDOWS_CODESIGN_SUBJECT ; kv/data/github-actions/xworkmate-app APPLE_CERT_P12_BASE64 | APPLE_CERT_P12_BASE64 ; kv/data/github-actions/xworkmate-app APPLE_CERT_PASSWORD | APPLE_CERT_PASSWORD ; kv/data/github-actions/xworkmate-app APPLE_PROVISION_PROFILE_BASE64 | APPLE_PROVISION_PROFILE_BASE64 ; kv/data/github-actions/xworkmate-app APPLE_KEYCHAIN_PASSWORD | APPLE_KEYCHAIN_PASSWORD ; - kv/data/github-actions/xworkmate-app APPLE_EXPORT_METHOD | APPLE_EXPORT_METHOD ; + kv/data/github-actions/xworkmate-app APPLE_EXPORT_METHOD | APPLE_EXPORT_METHOD + + - name: Load Vault secrets (Windows) + id: vault_windows + if: ${{ matrix.platform == 'windows' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + uses: hashicorp/vault-action@v4 + with: + url: ${{ env.VAULT_ADDR }} + method: jwt + role: github-actions-xworkmate-app + jwtGithubAudience: vault + ignoreNotFound: true + secrets: | + kv/data/github-actions/xworkmate-app WINDOWS_PFX_BASE64 | WINDOWS_PFX_BASE64 ; + kv/data/github-actions/xworkmate-app WINDOWS_PFX_PASSWORD | WINDOWS_PFX_PASSWORD ; + kv/data/github-actions/xworkmate-app WINDOWS_CODESIGN_SUBJECT | WINDOWS_CODESIGN_SUBJECT + + - name: Load Vault secrets (Android) + id: vault_android + if: ${{ matrix.platform == 'android' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + uses: hashicorp/vault-action@v4 + with: + url: ${{ env.VAULT_ADDR }} + method: jwt + role: github-actions-xworkmate-app + jwtGithubAudience: vault + ignoreNotFound: true + secrets: | kv/data/github-actions/xworkmate-app ANDROID_KEYSTORE_BASE64 | ANDROID_KEYSTORE_BASE64 ; kv/data/github-actions/xworkmate-app ANDROID_KEYSTORE_PASSWORD | ANDROID_KEYSTORE_PASSWORD ; kv/data/github-actions/xworkmate-app ANDROID_KEY_ALIAS | ANDROID_KEY_ALIAS ; kv/data/github-actions/xworkmate-app ANDROID_KEY_PASSWORD | ANDROID_KEY_PASSWORD - name: Export signing secrets + shell: bash run: | { - echo "XWORKMATE_SIGN_IDENTITY=${{ steps.vault.outputs.XWORKMATE_SIGN_IDENTITY }}" - echo "WINDOWS_PFX_BASE64=${{ steps.vault.outputs.WINDOWS_PFX_BASE64 }}" - echo "WINDOWS_PFX_PASSWORD=${{ steps.vault.outputs.WINDOWS_PFX_PASSWORD }}" - echo "WINDOWS_CODESIGN_SUBJECT=${{ steps.vault.outputs.WINDOWS_CODESIGN_SUBJECT }}" - echo "APPLE_CERT_P12_BASE64=${{ steps.vault.outputs.APPLE_CERT_P12_BASE64 }}" - echo "APPLE_CERT_PASSWORD=${{ steps.vault.outputs.APPLE_CERT_PASSWORD }}" - echo "APPLE_PROVISION_PROFILE_BASE64=${{ steps.vault.outputs.APPLE_PROVISION_PROFILE_BASE64 }}" - echo "APPLE_KEYCHAIN_PASSWORD=${{ steps.vault.outputs.APPLE_KEYCHAIN_PASSWORD }}" - echo "APPLE_EXPORT_METHOD=${{ steps.vault.outputs.APPLE_EXPORT_METHOD }}" - echo "ANDROID_KEYSTORE_BASE64=${{ steps.vault.outputs.ANDROID_KEYSTORE_BASE64 }}" - echo "ANDROID_KEYSTORE_PASSWORD=${{ steps.vault.outputs.ANDROID_KEYSTORE_PASSWORD }}" - echo "ANDROID_KEY_ALIAS=${{ steps.vault.outputs.ANDROID_KEY_ALIAS }}" - echo "ANDROID_KEY_PASSWORD=${{ steps.vault.outputs.ANDROID_KEY_PASSWORD }}" + echo "XWORKMATE_SIGN_IDENTITY=${{ steps.vault_apple.outputs.XWORKMATE_SIGN_IDENTITY }}" + echo "APPLE_CERT_P12_BASE64=${{ steps.vault_apple.outputs.APPLE_CERT_P12_BASE64 }}" + echo "APPLE_CERT_PASSWORD=${{ steps.vault_apple.outputs.APPLE_CERT_PASSWORD }}" + echo "APPLE_PROVISION_PROFILE_BASE64=${{ steps.vault_apple.outputs.APPLE_PROVISION_PROFILE_BASE64 }}" + echo "APPLE_KEYCHAIN_PASSWORD=${{ steps.vault_apple.outputs.APPLE_KEYCHAIN_PASSWORD }}" + echo "APPLE_EXPORT_METHOD=${{ steps.vault_apple.outputs.APPLE_EXPORT_METHOD }}" + echo "WINDOWS_PFX_BASE64=${{ steps.vault_windows.outputs.WINDOWS_PFX_BASE64 }}" + echo "WINDOWS_PFX_PASSWORD=${{ steps.vault_windows.outputs.WINDOWS_PFX_PASSWORD }}" + echo "WINDOWS_CODESIGN_SUBJECT=${{ steps.vault_windows.outputs.WINDOWS_CODESIGN_SUBJECT }}" + echo "ANDROID_KEYSTORE_BASE64=${{ steps.vault_android.outputs.ANDROID_KEYSTORE_BASE64 }}" + echo "ANDROID_KEYSTORE_PASSWORD=${{ steps.vault_android.outputs.ANDROID_KEYSTORE_PASSWORD }}" + echo "ANDROID_KEY_ALIAS=${{ steps.vault_android.outputs.ANDROID_KEY_ALIAS }}" + echo "ANDROID_KEY_PASSWORD=${{ steps.vault_android.outputs.ANDROID_KEY_PASSWORD }}" } >> "$GITHUB_ENV" - name: Set up Flutter SDK From d890acb661a01165af2aee36b716856a68177c2e Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 29 Jun 2026 15:48:49 +0800 Subject: [PATCH 16/16] feat: add one-line XWorkmate installer (#42) Co-authored-by: Haitao Pan --- README.md | 6 ++ scripts/install-xworkmate-app.sh | 139 +++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100755 scripts/install-xworkmate-app.sh diff --git a/README.md b/README.md index 18871ba8..fff19428 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,12 @@ flutter build macos make build-macos ``` +For a one-line install from the latest GitHub release: + +```bash +curl -sfL https://install.svc.plus/xworkmate-app | bash - +``` + ## Downloads | Platform | Download | diff --git a/scripts/install-xworkmate-app.sh b/scripts/install-xworkmate-app.sh new file mode 100755 index 00000000..f9f91de5 --- /dev/null +++ b/scripts/install-xworkmate-app.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO=${XWORKMATE_INSTALL_REPO:-"x-evor/xworkmate-app"} +RELEASE_TAG=${XWORKMATE_INSTALL_RELEASE_TAG:-"latest"} +GITHUB_API=${XWORKMATE_INSTALL_GITHUB_API:-"https://api.github.com"} +TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/xworkmate-install.XXXXXX")" + +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +info() { printf '[INFO] %s\n' "$*" >&2; } +die() { printf '[ERROR] %s\n' "$*" >&2; exit 1; } + +need() { + command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" +} + +release_json_url() { + if [[ "$RELEASE_TAG" == "latest" ]]; then + printf '%s/repos/%s/releases/latest\n' "$GITHUB_API" "$REPO" + else + printf '%s/repos/%s/releases/tags/%s\n' "$GITHUB_API" "$REPO" "$RELEASE_TAG" + fi +} + +pick_asset_url() { + local metadata_file="$1" + local pattern="$2" + python3 - "$metadata_file" "$pattern" <<'PY' +import json +import re +import sys +from pathlib import Path + +metadata_path = Path(sys.argv[1]) +pattern = re.compile(sys.argv[2]) +data = json.loads(metadata_path.read_text(encoding="utf-8")) +for asset in data.get("assets", []): + name = asset.get("name", "") + if pattern.search(name): + print(asset.get("browser_download_url", "")) + raise SystemExit(0) +raise SystemExit(1) +PY +} + +install_macos_dmg() { + local dmg_url="$1" + local dmg_path="$TMP_DIR/XWorkmate.dmg" + local mount_point="$TMP_DIR/mount" + local target_app="/Applications/XWorkmate.app" + + mkdir -p "$mount_point" + info "Downloading macOS DMG..." + curl -fL --retry 5 --retry-all-errors -o "$dmg_path" "$dmg_url" + info "Mounting DMG..." + hdiutil attach "$dmg_path" -mountpoint "$mount_point" -nobrowse -readonly -quiet + trap 'hdiutil detach "$mount_point" -quiet >/dev/null 2>&1 || true; cleanup' EXIT + + local source_app="$mount_point/XWorkmate.app" + [[ -d "$source_app" ]] || die "DMG does not contain XWorkmate.app" + if [[ -d "$target_app" ]]; then + info "Replacing existing app at $target_app" + rm -rf "$target_app" + fi + info "Installing to $target_app" + ditto "$source_app" "$target_app" + xattr -dr com.apple.quarantine "$target_app" 2>/dev/null || true + info "Installed $target_app" +} + +install_linux_pkg() { + local pkg_url="$1" + local pkg_path="$TMP_DIR/package" + + curl -fL --retry 5 --retry-all-errors -o "$pkg_path" "$pkg_url" + need sudo + if [[ "$pkg_url" == *.deb ]]; then + info "Installing Debian package..." + sudo dpkg -i "$pkg_path" || sudo apt-get -f install -y + elif [[ "$pkg_url" == *.rpm ]]; then + info "Installing RPM package..." + if command -v dnf >/dev/null 2>&1; then + sudo dnf install -y "$pkg_path" + else + sudo rpm -Uvh "$pkg_path" + fi + else + die "Unsupported Linux asset: $pkg_url" + fi +} + +main() { + local release_json_path="$TMP_DIR/release.json" + local asset_name_pattern + local asset_url + + need curl + need python3 + + info "Resolving release for $REPO" + curl -fsSL "$(release_json_url)" -o "$release_json_path" + + case "$(uname -s)" in + Darwin) + asset_name_pattern='^XWorkmate-[^/]+\.dmg$' + ;; + Linux) + case "$(uname -m)" in + x86_64|amd64) ;; + *) die "Linux packages are only available for amd64: $(uname -m)" ;; + esac + if command -v dpkg >/dev/null 2>&1; then + asset_name_pattern='^xworkmate_[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9]+)?_amd64\.deb$' + elif command -v rpm >/dev/null 2>&1; then + asset_name_pattern='^xworkmate-[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9]+)?-1\.x86_64\.rpm$' + else + die "Neither dpkg nor rpm found" + fi + ;; + *) + die "Unsupported OS: $(uname -s)" + ;; + esac + + asset_url="$(pick_asset_url "$release_json_path" "$asset_name_pattern")" || + die "Could not find a matching release asset" + [[ -n "$asset_url" ]] || die "Matching release asset has no download URL" + + case "$(uname -s)" in + Darwin) install_macos_dmg "$asset_url" ;; + Linux) install_linux_pkg "$asset_url" ;; + esac +} + +main "$@"