From 9eda4ab077a1853ed036f8e8f4ae7b166da5dc33 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Fri, 20 Mar 2026 18:54:31 +0800 Subject: [PATCH 01/34] fix(demo): stabilize Electron Windows packaging and build flow Use the backend uv environment for PyInstaller, enforce the correct packaging working directory, and add China mirror envs for Electron/electron-builder downloads to avoid startup and packaging failures. Made-with: Cursor --- .../openagent_demo/electron/package-lock.json | 4 +- libs/openagent_demo/electron/package.json | 3 +- .../electron/scripts/build-all.ps1 | 47 ++++++++++++++++ .../electron/scripts/build-backend.ps1 | 9 +-- libs/openagent_demo/frontend/eslint.config.js | 28 ++++++++++ .../openagent_demo/frontend/package-lock.json | 55 ++++++++++++++----- libs/openagent_demo/frontend/package.json | 3 + 7 files changed, 128 insertions(+), 21 deletions(-) create mode 100644 libs/openagent_demo/electron/scripts/build-all.ps1 create mode 100644 libs/openagent_demo/frontend/eslint.config.js diff --git a/libs/openagent_demo/electron/package-lock.json b/libs/openagent_demo/electron/package-lock.json index 2c14eb11..86887901 100644 --- a/libs/openagent_demo/electron/package-lock.json +++ b/libs/openagent_demo/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "openagent", - "version": "0.1.0", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openagent", - "version": "0.1.0", + "version": "0.0.1", "dependencies": { "tree-kill": "^1.2.2" }, diff --git a/libs/openagent_demo/electron/package.json b/libs/openagent_demo/electron/package.json index ea5e8d62..af28a707 100644 --- a/libs/openagent_demo/electron/package.json +++ b/libs/openagent_demo/electron/package.json @@ -66,7 +66,8 @@ "arch": ["x64"] } ], - "icon": "build/icon.ico" + "icon": "build/icon.ico", + "signAndEditExecutable": false }, "nsis": { "oneClick": false, diff --git a/libs/openagent_demo/electron/scripts/build-all.ps1 b/libs/openagent_demo/electron/scripts/build-all.ps1 new file mode 100644 index 00000000..2fd33da8 --- /dev/null +++ b/libs/openagent_demo/electron/scripts/build-all.ps1 @@ -0,0 +1,47 @@ +$ErrorActionPreference = 'Stop' + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$ElectronDir = Resolve-Path "$ScriptDir\.." +$Target = if ($args.Count -gt 0) { $args[0] } else { 'win' } + +Write-Host '=========================================' +Write-Host ' OpenAgent Desktop - Build ('$Target')' +Write-Host '=========================================' +Write-Host '' + +Write-Host '[1/3] Building frontend...' +# Check if npm command exists +if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { + Write-Error "npm command not found, please make sure Node.js is installed" +} + +# Build frontend +Set-Location "$ElectronDir\..\frontend" +npm install +npm run build + +Write-Host '' +Write-Host '[2/3] Skipping electron dependencies (already installed)...' +Set-Location $ElectronDir + +Write-Host '' +Write-Host '[2.5/3] Building backend...' +# Call PowerShell version of backend build script +& "$ScriptDir\build-backend.ps1" +Write-Host 'Backend build completed successfully!' +Set-Location $ElectronDir + +Write-Host '' +Write-Host '[3/3] Packaging Windows x64 installer...' +$env:ELECTRON_MIRROR = 'https://npmmirror.com/mirrors/electron/' +$env:ELECTRON_BUILDER_BINARIES_MIRROR = 'https://npmmirror.com/mirrors/electron-builder-binaries/' +npx electron-builder --win --x64 --publish never +Write-Host 'Electron packaging completed successfully!' + +Write-Host '' +Write-Host '=========================================' +Write-Host ' Build complete! Output in dist/' +Write-Host '=========================================' + +# List build artifacts +Get-ChildItem "$ElectronDir\dist\*.exe", "$ElectronDir\dist\*.blockmap" | Format-Table -AutoSize \ No newline at end of file diff --git a/libs/openagent_demo/electron/scripts/build-backend.ps1 b/libs/openagent_demo/electron/scripts/build-backend.ps1 index bc5da703..c4e64c03 100644 --- a/libs/openagent_demo/electron/scripts/build-backend.ps1 +++ b/libs/openagent_demo/electron/scripts/build-backend.ps1 @@ -5,12 +5,11 @@ $ElectronDir = Resolve-Path "$ScriptDir\.." $BackendDir = Resolve-Path "$ElectronDir\..\backend" Write-Host "==> Installing PyInstaller..." -pip install pyinstaller - -Write-Host "==> Building backend with PyInstaller..." Set-Location $BackendDir +uv pip install pyinstaller -pyinstaller ` +Write-Host "==> Building backend with PyInstaller..." +uv run pyinstaller ` --name openagent_api_server ` --onedir ` --noconfirm ` @@ -31,6 +30,8 @@ pyinstaller ` --hidden-import uvicorn.lifespan.on ` --hidden-import uvicorn.lifespan.off ` --collect-submodules openagent_api ` + --collect-submodules openagent ` + --collect-data openagent ` --add-data "skills;skills" ` openagent_api/server.py diff --git a/libs/openagent_demo/frontend/eslint.config.js b/libs/openagent_demo/frontend/eslint.config.js new file mode 100644 index 00000000..e05f7651 --- /dev/null +++ b/libs/openagent_demo/frontend/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist', 'build'] }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + languageOptions: { + ecmaVersion: 2022, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/libs/openagent_demo/frontend/package-lock.json b/libs/openagent_demo/frontend/package-lock.json index f587e473..872c9609 100644 --- a/libs/openagent_demo/frontend/package-lock.json +++ b/libs/openagent_demo/frontend/package-lock.json @@ -1,17 +1,18 @@ { "name": "openagent-demo-frontend", - "version": "0.1.0", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openagent-demo-frontend", - "version": "0.1.0", + "version": "0.0.1", "dependencies": { "docx-preview": "^0.3.7", "echarts": "^6.0.0", "lucide-react": "^0.577.0", "mermaid": "^11.13.0", + "pdfjs-dist": "^5.4.296", "pptx-viewer": "^0.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -26,6 +27,8 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/echarts": "^4.9.22", + "@types/mermaid": "^9.1.0", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@types/react-syntax-highlighter": "^15.5.13", @@ -2029,6 +2032,16 @@ "@types/ms": "*" } }, + "node_modules/@types/echarts": { + "version": "4.9.22", + "resolved": "https://registry.npmjs.org/@types/echarts/-/echarts-4.9.22.tgz", + "integrity": "sha512-7Fo6XdWpoi8jxkwP7BARUOM7riq8bMhmsCtSG8gzUcJmFhLo387tihoBYS/y5j7jl3PENT5RxeWZdN9RiwO7HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/zrender": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2075,6 +2088,13 @@ "@types/unist": "*" } }, + "node_modules/@types/mermaid": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@types/mermaid/-/mermaid-9.1.0.tgz", + "integrity": "sha512-rc8QqhveKAY7PouzY/p8ljS+eBSNCv7o79L97RSub/Ic2SQ34ph1Ng3s8wFLWVjvaEt6RLOWtSCsgYWd95NY8A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -2129,6 +2149,13 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/zrender": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/zrender/-/zrender-4.0.6.tgz", + "integrity": "sha512-1jZ9bJn2BsfmYFPBHtl5o3uV+ILejAtGrDcYSpT4qaVKEI/0YY+arw3XHU04Ebd8Nca3SQ7uNcLaqiL+tTFVMg==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", @@ -5678,6 +5705,18 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pdfjs-dist": { + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.80" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5885,18 +5924,6 @@ } } }, - "node_modules/react-pdf/node_modules/pdfjs-dist": { - "version": "5.4.296", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", - "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=20.16.0 || >=22.3.0" - }, - "optionalDependencies": { - "@napi-rs/canvas": "^0.1.80" - } - }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", diff --git a/libs/openagent_demo/frontend/package.json b/libs/openagent_demo/frontend/package.json index c14a0bc8..0a735da4 100644 --- a/libs/openagent_demo/frontend/package.json +++ b/libs/openagent_demo/frontend/package.json @@ -14,6 +14,7 @@ "echarts": "^6.0.0", "lucide-react": "^0.577.0", "mermaid": "^11.13.0", + "pdfjs-dist": "^5.4.296", "pptx-viewer": "^0.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -28,6 +29,8 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/echarts": "^4.9.22", + "@types/mermaid": "^9.1.0", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@types/react-syntax-highlighter": "^15.5.13", From f3f358dec9c61dcb8b5229341dbd5f9bb9e8ab21 Mon Sep 17 00:00:00 2001 From: aone Date: Tue, 24 Mar 2026 10:24:44 +0800 Subject: [PATCH 02/34] mod: windows wsl and vm install GUI and fix bind user folder. --- .../openagent/computer/local/_wsl.py | 40 +- .../openagent/computer/local/vm_win.py | 8 +- .../openagent/harness/environment.py | 16 +- .../tests/unit_tests/computer/test_vm.py | 1 - .../tests/unit_tests/computer/test_wsl.py | 25 +- .../unit_tests/harness/test_environment.py | 12 +- .../backend/openagent_api/agent_manager.py | 66 +++- .../backend/openagent_api/routes/setup.py | 342 +++++++++++++++++- .../openagent_demo/electron/package-lock.json | 64 ++-- libs/openagent_demo/electron/package.json | 11 +- .../frontend/src/components/SettingsModal.tsx | 41 ++- 11 files changed, 539 insertions(+), 87 deletions(-) diff --git a/libs/openagent/openagent/computer/local/_wsl.py b/libs/openagent/openagent/computer/local/_wsl.py index 8ed05e10..5724ac0d 100644 --- a/libs/openagent/openagent/computer/local/_wsl.py +++ b/libs/openagent/openagent/computer/local/_wsl.py @@ -19,6 +19,7 @@ import asyncio import contextlib import json +import logging import os import re import shlex @@ -32,6 +33,8 @@ from openagent.exceptions import MissingDependencyError, UnsupportedPlatformError, WslError from openagent.types import CLIResult +logger = logging.getLogger(__name__) + # UNC path prefixes for accessing WSL filesystem from Windows. # Modern Windows 11 uses ``wsl.localhost``; older builds use ``wsl$``. _UNC_PREFIXES = (r"\\wsl.localhost", r"\\wsl$") @@ -199,12 +202,11 @@ def write_mounts(self, mounts: list[ResolvedMount]) -> None: The change takes effect on the next distro restart (via ``apply_mounts`` or ``start``). - Raises: - WslError: If the config directory does not exist. + Creates the config directory if missing. This can happen when a distro + was installed outside ``build()`` (for example via setup migration or + manual WSL import) and cowork mounts are configured later. """ - if not self._config_dir.exists(): - msg = f"Config directory not found for instance '{self._instance}'" - raise WslError(msg) + self._config_dir.mkdir(parents=True, exist_ok=True) entries = [ { @@ -489,6 +491,8 @@ async def _apply_bind_mounts(self) -> None: if not mounts: return + logger.debug("WSL applying %d bind mount(s) from mounts.json", len(mounts)) + for m in mounts: wsl_host = _win_path_to_wsl(m.host_path) qguest = shlex.quote(m.guest_path) @@ -497,6 +501,12 @@ async def _apply_bind_mounts(self) -> None: # Skip if already mounted. check = await self.shell(f"mountpoint -q {qguest}", user="root") if check.exit_code == 0: + logger.info( + "WSL bind-mount already active: guest=%s wsl_source=%s host_win=%s", + m.guest_path, + wsl_host, + m.host_path, + ) continue cmd = f"mkdir -p {qguest} && mount --bind {qhost} {qguest}" @@ -508,6 +518,26 @@ async def _apply_bind_mounts(self) -> None: msg = f"Failed to bind-mount {m.host_path} → {m.guest_path}: {result.stderr}" raise WslError(msg) + # Post-verify so logs show whether the guest path is really a mount (debug empty ls issues). + verify = await self.shell(f"findmnt -n {qguest}", user="root") + if verify.exit_code == 0 and (verify.stdout or "").strip(): + logger.info( + "WSL bind-mount applied+verified: guest=%s host_win=%s findmnt=%s", + m.guest_path, + m.host_path, + verify.stdout.strip().replace("\n", " | "), + ) + else: + logger.warning( + "WSL bind-mount mount(8) succeeded but findmnt could not confirm guest=%s " + "(host_win=%s, wsl_source=%s, findmnt_exit=%s, findmnt_stderr=%s)", + m.guest_path, + m.host_path, + wsl_host, + verify.exit_code, + (verify.stderr or "").strip() or "(empty)", + ) + async def _resolve_unc_prefix(self) -> str: r"""Resolve and cache the working UNC prefix for this system. diff --git a/libs/openagent/openagent/computer/local/vm_win.py b/libs/openagent/openagent/computer/local/vm_win.py index 670af4bc..5dce2468 100644 --- a/libs/openagent/openagent/computer/local/vm_win.py +++ b/libs/openagent/openagent/computer/local/vm_win.py @@ -73,11 +73,12 @@ class _VMSessionComputer(AsyncComputerMixin): session_name: Linux username for this session. """ - def __init__(self, *, vm: WslVM, session_name: str) -> None: + def __init__(self, *, vm: WslVM, session_name: str, default_cwd: str | None = None) -> None: """Initialize with a VM backend and session name.""" self._vm = vm self._session_name = session_name self._active = True + self._default_cwd = default_cwd @property def session_name(self) -> str: @@ -89,6 +90,10 @@ def is_running(self) -> bool: """True if this handle is active.""" return self._active + def set_default_cwd(self, cwd: str | None) -> None: + """Set the default working directory for future commands.""" + self._default_cwd = cwd + async def start(self) -> None: """Health check — verify the distro is running and session user exists. @@ -131,6 +136,7 @@ async def run( return await self._vm.shell( command, user=self._session_name, + cwd=self._default_cwd, timeout=timeout_ms / 1000, ) except VMError as e: diff --git a/libs/openagent/openagent/harness/environment.py b/libs/openagent/openagent/harness/environment.py index 56ef438b..c5724e41 100644 --- a/libs/openagent/openagent/harness/environment.py +++ b/libs/openagent/openagent/harness/environment.py @@ -72,13 +72,15 @@ async def resolve(self) -> EnvironmentContext: # Parse into a timezone-aware datetime. # Shell outputs ISO 8601 with numeric offset, e.g. "2026-02-14T10:30:00-0800". raw_dt = values[5] - if not raw_dt: - msg = f"Environment shell returned empty datetime (raw output: {result.stdout!r})" - raise ValueError(msg) - try: - now = datetime.strptime(raw_dt, "%Y-%m-%dT%H:%M:%S%z") - except ValueError: - now = datetime.strptime(raw_dt[:19], "%Y-%m-%dT%H:%M:%S") # noqa: DTZ007 + if raw_dt: + try: + now = datetime.strptime(raw_dt, "%Y-%m-%dT%H:%M:%S%z") + except ValueError: + now = datetime.strptime(raw_dt[:19], "%Y-%m-%dT%H:%M:%S") # noqa: DTZ007 + else: + # Some hosts occasionally return empty stdout for this probe. + # Degrade gracefully so environment detection does not block task execution. + now = datetime.now().astimezone() return EnvironmentContext( working_dir=values[0], diff --git a/libs/openagent/tests/unit_tests/computer/test_vm.py b/libs/openagent/tests/unit_tests/computer/test_vm.py index 516e9d1c..b73fab91 100644 --- a/libs/openagent/tests/unit_tests/computer/test_vm.py +++ b/libs/openagent/tests/unit_tests/computer/test_vm.py @@ -195,7 +195,6 @@ async def test_run_translates_vm_error_to_cli_error(self) -> None: with pytest.raises(CLIError, match="boom"): await computer.run("bad") - class TestUpload: """Tests for upload().""" diff --git a/libs/openagent/tests/unit_tests/computer/test_wsl.py b/libs/openagent/tests/unit_tests/computer/test_wsl.py index 9ef9e67c..0479f56e 100644 --- a/libs/openagent/tests/unit_tests/computer/test_wsl.py +++ b/libs/openagent/tests/unit_tests/computer/test_wsl.py @@ -179,6 +179,25 @@ async def test_run_translates_vm_error_to_cli_error(self) -> None: with pytest.raises(CLIError, match="boom"): await computer.run("bad") + async def test_run_passes_default_cwd_to_vm_shell(self) -> None: + vm = _mock_vm() + computer = _VMSessionComputer(vm=vm, session_name="test-session", default_cwd="/sessions/test-session/mnt/code") + + await computer.run("pwd") + + call_kwargs = vm.shell.call_args.kwargs + assert call_kwargs["cwd"] == "/sessions/test-session/mnt/code" + + async def test_set_default_cwd_updates_future_run_calls(self) -> None: + vm = _mock_vm() + computer = _make_computer(vm) + computer.set_default_cwd("/sessions/test-session/mnt/project") + + await computer.run("pwd") + + call_kwargs = vm.shell.call_args.kwargs + assert call_kwargs["cwd"] == "/sessions/test-session/mnt/project" + class TestUpload: """Tests for upload().""" @@ -626,20 +645,22 @@ async def test_applies_bind_mounts(self) -> None: mock_shell.side_effect = [ _fail(), # mountpoint -q /mnt/code -> not mounted _ok(), # mount --bind ... /mnt/code + _ok(stdout="/mnt/c/Users/foo/code"), # findmnt -n /mnt/code _fail(), # mountpoint -q /mnt/data -> not mounted _ok(), # mount --bind ... /mnt/data (+ remount ro) + _ok(stdout="/mnt/d/data"), # findmnt -n /mnt/data ] await vm._apply_bind_mounts() - assert mock_shell.await_count == 4 + assert mock_shell.await_count == 6 # Check writable mount (no remount) mount_call_1 = mock_shell.call_args_list[1].args[0] assert "mount --bind" in mount_call_1 assert "/mnt/c/Users/foo/code" in mount_call_1 assert "remount,ro" not in mount_call_1 # Check read-only mount (remount ro) - mount_call_2 = mock_shell.call_args_list[3].args[0] + mount_call_2 = mock_shell.call_args_list[4].args[0] assert "mount --bind" in mount_call_2 assert "remount,ro" in mount_call_2 diff --git a/libs/openagent/tests/unit_tests/harness/test_environment.py b/libs/openagent/tests/unit_tests/harness/test_environment.py index d17a3da1..db9da23a 100644 --- a/libs/openagent/tests/unit_tests/harness/test_environment.py +++ b/libs/openagent/tests/unit_tests/harness/test_environment.py @@ -83,10 +83,10 @@ async def test_datetime_without_timezone_fallback(self) -> None: assert env.today_date.year == 2026 assert env.today_date.tzinfo is None - async def test_empty_datetime_raises(self) -> None: + async def test_empty_datetime_fallback_now(self) -> None: computer = _mock_computer(_make_stdout(date="")) - with pytest.raises(ValueError, match="empty datetime"): - await EnvironmentResolver(computer).resolve() + env = await EnvironmentResolver(computer).resolve() + assert isinstance(env.today_date, datetime) async def test_pads_missing_parts(self) -> None: """When stdout has fewer delimiters, missing fields are padded.""" @@ -94,9 +94,9 @@ async def test_pads_missing_parts(self) -> None: stdout = f"/home/user\n{_DELIM}\ntrue" computer = _mock_computer(stdout) - # Date will be empty → raises ValueError - with pytest.raises(ValueError, match="empty datetime"): - await EnvironmentResolver(computer).resolve() + # Date will be empty -> fallback to current time. + env = await EnvironmentResolver(computer).resolve() + assert isinstance(env.today_date, datetime) async def test_darwin_platform(self) -> None: computer = _mock_computer(_make_stdout(platform="darwin", os_version="Darwin 25.3.0")) diff --git a/libs/openagent_demo/backend/openagent_api/agent_manager.py b/libs/openagent_demo/backend/openagent_api/agent_manager.py index 9786791d..f2ba3733 100644 --- a/libs/openagent_demo/backend/openagent_api/agent_manager.py +++ b/libs/openagent_demo/backend/openagent_api/agent_manager.py @@ -20,6 +20,7 @@ import json import logging import os +import sys from typing import Any from collections.abc import AsyncIterator @@ -48,6 +49,8 @@ def __init__(self) -> None: self._agent_locks: dict[tuple[str, str], asyncio.Lock] = {} # session_name -> (working_dir_source, mount_target) self._session_working_dirs: dict[str, tuple[str, str]] = {} + # Background warm-up task: auto-start VM manager after app startup. + self._vm_warm_task: asyncio.Task[None] | None = None def conversation_lock(self, conversation_id: str) -> asyncio.Lock: """Per-conversation lock to serialise prepare/send/mount operations.""" @@ -55,6 +58,13 @@ def conversation_lock(self, conversation_id: str) -> asyncio.Lock: self._conv_locks[conversation_id] = asyncio.Lock() return self._conv_locks[conversation_id] + @staticmethod + def _set_computer_default_cwd(computer: Any, cwd: str | None) -> None: + """Best-effort hook for session computers that support default cwd.""" + setter = getattr(computer, "set_default_cwd", None) + if callable(setter): + setter(cwd) + # ── Computer management ── async def _ensure_computer( @@ -111,10 +121,15 @@ async def _ensure_computer( if session_name and session_name in self._computers: return self._computers[session_name], session_name - # Guard: check that limactl is available before attempting VM ops + # Guard: check that the platform VM backend is available before VM ops import shutil - if not shutil.which("limactl"): + vm_backend_ready = ( + bool(shutil.which("wsl.exe") or shutil.which("wsl")) + if sys.platform == "win32" + else bool(shutil.which("limactl")) + ) + if not vm_backend_ready: raise RuntimeError( "Cowork mode requires VM setup. " "Please install and configure it in Settings \u2192 Sandbox." @@ -134,10 +149,18 @@ async def _ensure_computer( from openagent.computer import Mount session_mounts: list[Mount] | None = None + default_cwd: str | None = None if working_dir: - session_mounts = [Mount(source=working_dir, target=Path(working_dir).name, writable=True)] + mount_target = Path(working_dir).name + session_mounts = [Mount(source=working_dir, target=mount_target, writable=True)] + default_cwd = f"/sessions/{{session}}/mnt/{mount_target}" logger.info("Creating new session (mounts=%s)...", session_mounts) computer = await self._vm_manager.computer(mounts=session_mounts) + if default_cwd is not None: + self._set_computer_default_cwd( + computer, + default_cwd.format(session=computer.session_name), + ) except FileNotFoundError: raise RuntimeError( "Cowork mode requires VM setup. " @@ -392,16 +415,38 @@ async def start(self) -> None: from dotenv import load_dotenv load_dotenv() - # VM setup is lazy — _ensure_vm_manager() is called on demand - # when a cowork-mode conversation starts. + # Keep startup fast: warm VM in background (non-blocking). Cowork + # still lazily initializes on first use if warm-up fails. + self._schedule_vm_warmup() + logger.info("Agent manager initialized.") + + def _schedule_vm_warmup(self) -> None: + """Best-effort background VM warm-up on supported hosts.""" + if self._vm_warm_task is not None and not self._vm_warm_task.done(): + return + + import shutil + + vm_backend_available = ( + bool(shutil.which("wsl.exe") or shutil.which("wsl")) + if sys.platform == "win32" + else bool(shutil.which("limactl")) + ) + if not vm_backend_available: + return + + self._vm_warm_task = asyncio.create_task(self._warm_vm_manager()) + + async def _warm_vm_manager(self) -> None: + """Background VM initialization used to auto-start installed VMs.""" try: await self._ensure_vm_manager() + logger.info("Background VM warm-up completed.") except Exception: logger.warning( - "VM manager not available (Lima not installed?). " - "Cowork mode will be unavailable; chat mode still works." + "Background VM warm-up failed; cowork mode will initialize VM on demand.", + exc_info=True, ) - logger.info("Agent manager initialized.") async def ensure_agent( self, @@ -451,6 +496,9 @@ async def stream_response( async def stop(self) -> None: """Shut down all agents and computers.""" + if self._vm_warm_task is not None and not self._vm_warm_task.done(): + self._vm_warm_task.cancel() + self._vm_warm_task = None for key, agent in self._agents.items(): logger.info("Closing agent for %s...", key) await agent.aclose() @@ -526,6 +574,8 @@ async def mount_working_dir(self, session_name: str, working_dir: str) -> None: mount = Mount(source=working_dir, target=new_target, writable=True) await self._vm_manager.mount([mount], session=session_name) self._session_working_dirs[session_name] = (working_dir, new_target) + computer = self._computers.get(session_name) + self._set_computer_default_cwd(computer, f"/sessions/{session_name}/mnt/{new_target}") logger.info("Mounted working dir %s for session %s", working_dir, session_name) # Remove the stale mount-point directory left behind after unmount. diff --git a/libs/openagent_demo/backend/openagent_api/routes/setup.py b/libs/openagent_demo/backend/openagent_api/routes/setup.py index 78c8f648..9841ee2f 100644 --- a/libs/openagent_demo/backend/openagent_api/routes/setup.py +++ b/libs/openagent_demo/backend/openagent_api/routes/setup.py @@ -25,7 +25,7 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import Response, StreamingResponse -from openagent_api.paths import deps_dir, vm_lima_dir, vm_setup_dir +from openagent_api.paths import data_dir, deps_dir, vm_lima_dir, vm_setup_dir logger = logging.getLogger(__name__) @@ -156,6 +156,107 @@ def _lima_status() -> dict[str, object]: return {"installed": False, "path": None, "managed": False} +# --------------------------------------------------------------------------- +# WSL (Windows) +# --------------------------------------------------------------------------- + +_WSL_INSTANCE = "openagent" +_WSL_EXPORT_SOURCE = "Ubuntu" + + +def _wsl_cmd() -> str | None: + return shutil.which("wsl.exe") or shutil.which("wsl") + + +def _decode_wsl_output(raw: bytes) -> str: + if raw[:2] == b"\xff\xfe" or b"\x00" in raw: + return raw.decode("utf-16-le", errors="replace").replace("\x00", "") + return raw.decode("utf-8", errors="replace") + + +def _parse_wsl_list(text: str) -> list[dict[str, str]]: + entries: list[dict[str, str]] = [] + for line in text.splitlines(): + stripped = line.strip() + if not stripped: + continue + if "NAME" in stripped.upper() and "STATE" in stripped.upper(): + continue + if stripped.startswith("*"): + stripped = stripped[1:].strip() + parts = stripped.split() + if len(parts) < 3: + continue + entries.append({"name": parts[0], "state": parts[1], "version": parts[2]}) + return entries + + +async def _wsl_list() -> list[dict[str, str]]: + if not _wsl_cmd(): + return [] + proc = await asyncio.create_subprocess_exec( + "wsl.exe", + "--list", + "--verbose", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + out_b, _ = await proc.communicate() + if proc.returncode != 0: + return [] + return _parse_wsl_list(_decode_wsl_output(out_b or b"")) + + +async def _wsl_instance_status() -> str | None: + entries = await _wsl_list() + for entry in entries: + if entry["name"].lower() == _WSL_INSTANCE.lower(): + return entry["state"] + return None + + +def _wsl_status() -> dict[str, object]: + wsl = _wsl_cmd() + return {"installed": bool(wsl), "path": wsl, "managed": False} + + +def _win_path_to_wsl(path: Path | str) -> str: + s = str(path).replace("\\", "/") + m = re.match(r"^([A-Za-z]):(.*)$", s) + if not m: + raise ValueError(f"Unsupported Windows path for WSL conversion: {s}") + drive = m.group(1).lower() + rest = m.group(2) + if not rest.startswith("/"): + rest = "/" + rest + return f"/mnt/{drive}{rest}" + + +async def _wsl_shell(cmd: str, *, timeout: float = 60) -> tuple[int, str, str]: + proc = await asyncio.create_subprocess_exec( + "wsl.exe", + "-d", + _WSL_INSTANCE, + "--", + "bash", + "-lc", + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + try: + stdout_b, stderr_b = await asyncio.wait_for(proc.communicate(), timeout=timeout) + except asyncio.TimeoutError: + proc.kill() + await proc.wait() + return 1, "", "Timed out" + return ( + proc.returncode or 0, + (stdout_b or b"").decode("utf-8", errors="replace"), + (stderr_b or b"").decode("utf-8", errors="replace"), + ) + + async def _install_lima_stream(): """SSE generator that downloads and installs Lima.""" def sse(event: str, data: dict[str, object]) -> str: @@ -219,6 +320,35 @@ def _extract() -> None: shutil.rmtree(tmp_dir, ignore_errors=True) +async def _install_wsl_stream(): + """SSE generator that enables WSL on Windows.""" + def sse(event: str, data: dict[str, object]) -> str: + return f"event: {event}\ndata: {json.dumps(data)}\n\n" + + wsl = _wsl_cmd() + if wsl: + yield sse("done", {"message": f"WSL already installed ({wsl})"}) + return + + yield sse("progress", {"step": "installing", "message": "Installing WSL components..."}) + proc = await asyncio.create_subprocess_exec( + "wsl.exe", + "--install", + "--no-distribution", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + out_b, err_b = await proc.communicate() + out = _decode_wsl_output(out_b or b"").strip() + err = _decode_wsl_output(err_b or b"").strip() + if proc.returncode != 0: + msg = err or out or "Failed to install WSL. Please run 'wsl --install' as Administrator." + yield sse("error", {"message": msg}) + return + + yield sse("done", {"message": "WSL installed. A reboot may be required before continuing."}) + + # --------------------------------------------------------------------------- # Platform dispatch # --------------------------------------------------------------------------- @@ -247,8 +377,7 @@ def _vm_status() -> dict[str, object]: if sys.platform == "darwin": return {"supported": True, "backend": "lima", **_lima_status()} if sys.platform == "win32": - # Placeholder for WSL support - return {"supported": True, "backend": "wsl", "installed": False} + return {"supported": True, "backend": "wsl", **_wsl_status()} return {"supported": False, "backend": None, "installed": False, "reason": f"No VM backend for {sys.platform}"} @@ -272,7 +401,10 @@ async def get_vm_status() -> dict[str, object]: instance_status: str | None = None if result.get("installed"): try: - instance_status = await _lima_instance_status() + if result.get("backend") == "lima": + instance_status = await _lima_instance_status() + elif result.get("backend") == "wsl": + instance_status = await _wsl_instance_status() vm_ready = instance_status == "Running" except Exception: pass @@ -298,8 +430,9 @@ async def install_vm_backend() -> StreamingResponse: backend = status["backend"] if backend == "lima": return StreamingResponse(_install_lima_stream(), media_type="text/event-stream") + if backend == "wsl": + return StreamingResponse(_install_wsl_stream(), media_type="text/event-stream") - # Future: WSL installation raise HTTPException(status_code=501, detail=f"Auto-install not yet supported for {backend}") @@ -450,6 +583,12 @@ class _BuildManager(_ProcessManager): """Manages ``limactl start`` to create or boot the VM.""" async def _run(self, **kwargs: object) -> None: + if sys.platform == "win32": + await self._run_wsl() + return + await self._run_lima() + + async def _run_lima(self) -> None: instance_status = await _lima_instance_status() if instance_status == "Running": @@ -501,6 +640,114 @@ async def _run(self, **kwargs: object) -> None: self._status = "error" self._error = f"exit {proc.returncode}" + async def _run_wsl(self) -> None: + if not _wsl_cmd(): + self._emit("error", {"message": "WSL is not installed. Install it first in Phase 1."}) + self._status = "error" + self._error = "WSL missing" + return + + status = await _wsl_instance_status() + if status == "Running": + self._emit("done", {"message": "WSL distro is already running"}) + self._status = "done" + return + + if status == "Stopped": + self._emit("progress", {"step": "starting", "message": "Starting existing WSL distro..."}) + proc = await asyncio.create_subprocess_exec( + "wsl.exe", "-d", _WSL_INSTANCE, "--", "echo", "ok", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + self._process = proc + _, stderr_b = await proc.communicate() + if proc.returncode == 0: + self._emit("done", {"message": "WSL distro started successfully"}) + self._status = "done" + else: + err = _decode_wsl_output(stderr_b or b"").strip() + self._emit("error", {"message": err or f"WSL start failed (exit {proc.returncode})"}) + self._status = "error" + self._error = f"exit {proc.returncode}" + return + + # Distro does not exist: bootstrap from Ubuntu export. + self._emit("progress", {"step": "creating", "message": "Preparing source distro (Ubuntu)..."}) + entries = await _wsl_list() + has_source = any(e["name"].lower() == _WSL_EXPORT_SOURCE.lower() for e in entries) + if not has_source: + proc = await asyncio.create_subprocess_exec( + "wsl.exe", "--install", "-d", _WSL_EXPORT_SOURCE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + self._process = proc + out_b, err_b = await proc.communicate() + if proc.returncode != 0: + err = (_decode_wsl_output(err_b or b"") or _decode_wsl_output(out_b or b"")).strip() + self._emit( + "error", + {"message": err or "Failed to install Ubuntu distro. Try running `wsl --install -d Ubuntu` manually once."}, + ) + self._status = "error" + self._error = f"exit {proc.returncode}" + return + self._emit("progress", {"step": "creating", "message": "Ubuntu installed. Continuing..."}) + + export_root = deps_dir() / "wsl" + export_root.mkdir(parents=True, exist_ok=True) + export_tar = export_root / f"{_WSL_EXPORT_SOURCE.lower()}-seed.tar" + import_dir = data_dir() / "wsl" / _WSL_INSTANCE / "disk" + import_dir.mkdir(parents=True, exist_ok=True) + + self._emit("progress", {"step": "creating", "message": "Exporting Ubuntu rootfs..."}) + proc_export = await asyncio.create_subprocess_exec( + "wsl.exe", "--export", _WSL_EXPORT_SOURCE, str(export_tar), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + self._process = proc_export + _, err_b = await proc_export.communicate() + if proc_export.returncode != 0: + err = _decode_wsl_output(err_b or b"").strip() + self._emit("error", {"message": err or f"WSL export failed (exit {proc_export.returncode})"}) + self._status = "error" + self._error = f"exit {proc_export.returncode}" + return + + self._emit("progress", {"step": "creating", "message": "Importing OpenAgent WSL distro..."}) + proc_import = await asyncio.create_subprocess_exec( + "wsl.exe", "--import", _WSL_INSTANCE, str(import_dir), str(export_tar), "--version", "2", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + self._process = proc_import + _, err_b = await proc_import.communicate() + if proc_import.returncode != 0: + err = _decode_wsl_output(err_b or b"").strip() + self._emit("error", {"message": err or f"WSL import failed (exit {proc_import.returncode})"}) + self._status = "error" + self._error = f"exit {proc_import.returncode}" + return + + self._emit("progress", {"step": "starting", "message": "Starting OpenAgent WSL distro..."}) + proc_start = await asyncio.create_subprocess_exec( + "wsl.exe", "-d", _WSL_INSTANCE, "--", "echo", "ok", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + self._process = proc_start + _, err_b = await proc_start.communicate() + if proc_start.returncode == 0: + self._emit("done", {"message": "WSL distro created and started successfully"}) + self._status = "done" + else: + err = _decode_wsl_output(err_b or b"").strip() + self._emit("error", {"message": err or f"WSL start failed (exit {proc_start.returncode})"}) + self._status = "error" + self._error = f"exit {proc_start.returncode}" + async def _stream_stderr(self, proc: asyncio.subprocess.Process) -> None: """Read limactl stderr line-by-line and emit progress events.""" assert proc.stderr is not None @@ -547,6 +794,12 @@ class _ProvisionManager(_ProcessManager): """Manages setup.sh execution inside the Lima VM.""" async def _run(self, **kwargs: object) -> None: + if sys.platform == "win32": + await self._run_wsl(**kwargs) + return + await self._run_lima(**kwargs) + + async def _run_lima(self, **kwargs: object) -> None: force = bool(kwargs.get("force", False)) # 1. Verify VM is running @@ -633,6 +886,65 @@ async def _run(self, **kwargs: object) -> None: self._status = "error" self._error = f"exit {proc.returncode}" + async def _run_wsl(self, **kwargs: object) -> None: + force = bool(kwargs.get("force", False)) + instance_status = await _wsl_instance_status() + if instance_status != "Running": + self._emit("error", {"message": f"WSL distro is not running (status: {instance_status})"}) + self._status = "error" + self._error = "WSL distro not running" + return + + self._emit("progress", {"step": "copying", "message": "Preparing setup files in WSL..."}) + setup_dir = vm_setup_dir() + if not setup_dir.is_dir(): + self._emit("error", {"message": f"Setup directory not found: {setup_dir}"}) + self._status = "error" + self._error = "Setup dir not found" + return + + setup_wsl = _win_path_to_wsl(setup_dir) + rc, _, err = await _wsl_shell( + f"sudo rm -rf {_SETUP_VM_DIR} && sudo mkdir -p {_SETUP_VM_DIR} && " + f"sudo cp -r {setup_wsl}/. {_SETUP_VM_DIR}/", + timeout=60, + ) + if rc != 0: + self._emit("error", {"message": f"Failed to stage setup files in WSL: {err}"}) + self._status = "error" + self._error = "Stage failed" + return + + self._emit("progress", {"step": "starting", "message": "Starting provisioning..."}) + cmd = f"sudo bash {_SETUP_VM_DIR}/setup.sh" + if force: + cmd += " --force" + + proc = await asyncio.create_subprocess_exec( + "wsl.exe", "-d", _WSL_INSTANCE, "--", "bash", "-lc", cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + self._process = proc + + assert proc.stdout is not None + while True: + line = await proc.stdout.readline() + if not line: + break + text = line.decode("utf-8", errors="replace").strip() + if text.startswith("@@SETUP:"): + self._handle_setup_line(text) + + await proc.wait() + if proc.returncode == 0: + self._emit("done", {"message": "Provisioning complete"}) + self._status = "done" + else: + self._emit("error", {"message": f"Provisioning failed (exit {proc.returncode})"}) + self._status = "error" + self._error = f"exit {proc.returncode}" + def _handle_setup_line(self, line: str) -> None: """Parse @@SETUP:step_id:status:message and emit typed SSE events.""" # Format: @@SETUP::: @@ -656,11 +968,16 @@ def _handle_setup_line(self, line: str) -> None: async def check_markers(self) -> dict[str, object]: """Read VM-side marker files to determine provision state.""" - instance_status = await _lima_instance_status() + if sys.platform == "win32": + instance_status = await _wsl_instance_status() + shell = _wsl_shell + else: + instance_status = await _lima_instance_status() + shell = _lima_shell if instance_status != "Running": return {"provisioned": False, "steps_done": [], "total_steps": len(_PROVISION_STEPS)} - rc, stdout, _ = await _lima_shell(f"ls {_SETUP_MARKER_DIR}/*.done 2>/dev/null || true") + rc, stdout, _ = await shell(f"ls {_SETUP_MARKER_DIR}/*.done 2>/dev/null || true") if rc != 0 or not stdout.strip(): return {"provisioned": False, "steps_done": [], "total_steps": len(_PROVISION_STEPS)} @@ -679,7 +996,8 @@ async def check_markers(self) -> dict[str, object]: async def get_log(self) -> str: """Fetch the latest setup log from the VM.""" - rc, stdout, _ = await _lima_shell( + shell = _wsl_shell if sys.platform == "win32" else _lima_shell + rc, stdout, _ = await shell( f"ls -t {_SETUP_LOG_DIR}/setup-*.log 2>/dev/null | head -1 | xargs cat 2>/dev/null | tail -500", timeout=15, ) @@ -718,7 +1036,8 @@ async def build_vm() -> StreamingResponse: """Create or start the VM. Streams SSE progress events.""" status = _vm_status() if not status.get("installed"): - raise HTTPException(status_code=422, detail="VM backend (Lima) is not installed") + backend = status.get("backend") or "vm backend" + raise HTTPException(status_code=422, detail=f"VM backend ({backend}) is not installed") mgr = _get_build_manager() if mgr._status != "running": @@ -732,7 +1051,10 @@ async def get_build_status() -> dict[str, object]: mgr = _get_build_manager() result = dict(mgr.status_dict()) if mgr._status in ("idle", "done", "error"): - result["vm_state"] = await _lima_instance_status() + if sys.platform == "win32": + result["vm_state"] = await _wsl_instance_status() + else: + result["vm_state"] = await _lima_instance_status() return result diff --git a/libs/openagent_demo/electron/package-lock.json b/libs/openagent_demo/electron/package-lock.json index 86887901..5000cb64 100644 --- a/libs/openagent_demo/electron/package-lock.json +++ b/libs/openagent_demo/electron/package-lock.json @@ -11,7 +11,7 @@ "tree-kill": "^1.2.2" }, "devDependencies": { - "electron": "^33.0.0", + "electron": "^33.4.11", "electron-builder": "^25.1.8" } }, @@ -84,7 +84,7 @@ }, "node_modules/@electron/get": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "resolved": "https://registry.npmmirror.com/@electron/get/-/get-2.0.3.tgz", "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", "dev": true, "license": "MIT", @@ -777,7 +777,7 @@ }, "node_modules/@types/yauzl": { "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, "license": "MIT", @@ -1232,7 +1232,7 @@ }, "node_modules/boolean": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "resolved": "https://registry.npmmirror.com/boolean/-/boolean-3.2.0.tgz", "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true, @@ -1951,7 +1951,7 @@ }, "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", @@ -1970,7 +1970,7 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "license": "MIT", @@ -2016,7 +2016,7 @@ }, "node_modules/detect-node": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "resolved": "https://registry.npmmirror.com/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true, "license": "MIT", @@ -2216,7 +2216,7 @@ }, "node_modules/electron": { "version": "33.4.11", - "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz", + "resolved": "https://registry.npmmirror.com/electron/-/electron-33.4.11.tgz", "integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==", "dev": true, "hasInstallScript": true, @@ -2502,7 +2502,7 @@ }, "node_modules/es6-error": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "resolved": "https://registry.npmmirror.com/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true, "license": "MIT", @@ -2520,7 +2520,7 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", @@ -2541,7 +2541,7 @@ }, "node_modules/extract-zip": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "resolved": "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "license": "BSD-2-Clause", @@ -2587,7 +2587,7 @@ }, "node_modules/fd-slicer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, "license": "MIT", @@ -2692,7 +2692,7 @@ }, "node_modules/fs-extra": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-8.1.0.tgz", "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", @@ -2876,7 +2876,7 @@ }, "node_modules/global-agent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "resolved": "https://registry.npmmirror.com/global-agent/-/global-agent-3.0.0.tgz", "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "dev": true, "license": "BSD-3-Clause", @@ -2895,7 +2895,7 @@ }, "node_modules/global-agent/node_modules/semver": { "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", @@ -2909,7 +2909,7 @@ }, "node_modules/globalthis": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", @@ -2983,7 +2983,7 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", @@ -3368,7 +3368,7 @@ }, "node_modules/json-stringify-safe": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "resolved": "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true, "license": "ISC", @@ -3389,7 +3389,7 @@ }, "node_modules/jsonfile": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", @@ -3633,7 +3633,7 @@ }, "node_modules/matcher": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "resolved": "https://registry.npmmirror.com/matcher/-/matcher-3.0.0.tgz", "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", "dev": true, "license": "MIT", @@ -4019,7 +4019,7 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "license": "MIT", @@ -4198,7 +4198,7 @@ }, "node_modules/pend": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true, "license": "MIT" @@ -4235,7 +4235,7 @@ }, "node_modules/progress": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, "license": "MIT", @@ -4461,7 +4461,7 @@ }, "node_modules/roarr": { "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "resolved": "https://registry.npmmirror.com/roarr/-/roarr-2.15.4.tgz", "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", "dev": true, "license": "BSD-3-Clause", @@ -4528,7 +4528,7 @@ }, "node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", @@ -4538,7 +4538,7 @@ }, "node_modules/semver-compare": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "resolved": "https://registry.npmmirror.com/semver-compare/-/semver-compare-1.0.0.tgz", "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "dev": true, "license": "MIT", @@ -4546,7 +4546,7 @@ }, "node_modules/serialize-error": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "resolved": "https://registry.npmmirror.com/serialize-error/-/serialize-error-7.0.1.tgz", "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", "dev": true, "license": "MIT", @@ -4717,7 +4717,7 @@ }, "node_modules/sprintf-js": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true, "license": "BSD-3-Clause", @@ -4816,7 +4816,7 @@ }, "node_modules/sumchecker": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "resolved": "https://registry.npmmirror.com/sumchecker/-/sumchecker-3.0.1.tgz", "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", "dev": true, "license": "Apache-2.0", @@ -4977,7 +4977,7 @@ }, "node_modules/type-fest": { "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.13.1.tgz", "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true, "license": "(MIT OR CC0-1.0)", @@ -5038,7 +5038,7 @@ }, "node_modules/universalify": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", @@ -5224,7 +5224,7 @@ }, "node_modules/yauzl": { "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "license": "MIT", diff --git a/libs/openagent_demo/electron/package.json b/libs/openagent_demo/electron/package.json index ce766bfe..6dbd8113 100644 --- a/libs/openagent_demo/electron/package.json +++ b/libs/openagent_demo/electron/package.json @@ -17,7 +17,7 @@ "tree-kill": "^1.2.2" }, "devDependencies": { - "electron": "^33.0.0", + "electron": "^33.4.11", "electron-builder": "^25.1.8" }, "build": { @@ -51,7 +51,10 @@ "target": [ { "target": "dmg", - "arch": ["arm64", "x64"] + "arch": [ + "arm64", + "x64" + ] } ], "icon": "resources/icon.icns", @@ -64,7 +67,9 @@ "target": [ { "target": "nsis", - "arch": ["x64"] + "arch": [ + "x64" + ] } ], "icon": "resources/icon.ico", diff --git a/libs/openagent_demo/frontend/src/components/SettingsModal.tsx b/libs/openagent_demo/frontend/src/components/SettingsModal.tsx index f9a63aeb..c815e1c2 100644 --- a/libs/openagent_demo/frontend/src/components/SettingsModal.tsx +++ b/libs/openagent_demo/frontend/src/components/SettingsModal.tsx @@ -1684,7 +1684,7 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { .catch(() => {}); }, [dispatch]); - // Phase 1: Lima + // Phase 1: VM backend (Lima on macOS / WSL on Windows) const [vmStatus, setVmStatus] = useState(null); const [phase1, setPhase1] = useState("checking"); const [phase1Msg, setPhase1Msg] = useState(""); @@ -1694,6 +1694,7 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { const [phase2, setPhase2] = useState("pending"); const [phase2Msg, setPhase2Msg] = useState(""); const [phase2Error, setPhase2Error] = useState(""); + const [vmInstanceStopped, setVmInstanceStopped] = useState(false); // Phase 3: Provision const [phase3, setPhase3] = useState("pending"); @@ -1738,7 +1739,7 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { } catch { /* best effort — step defs come from backend constants */ } // Phase 1 - let limaInstalled = false; + let vmBackendInstalled = false; try { const vs = await getVMStatus(); if (cancelled) return; @@ -1746,7 +1747,7 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { if (!vs.supported) { setPhase1("error"); setPhase1Error("Not supported on this platform"); return; } if (vs.installed) { setPhase1("done"); - limaInstalled = true; + vmBackendInstalled = true; } else { setPhase1("pending"); } @@ -1757,7 +1758,7 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { return; } - if (!limaInstalled) return; + if (!vmBackendInstalled) return; // Phase 2 let vmReady = false; @@ -1766,15 +1767,19 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { if (cancelled) return; if (bs.status === "running") { setPhase2("running"); + setVmInstanceStopped(false); attachBuild(); } else if (bs.vm_state === "Running") { setPhase2("done"); + setVmInstanceStopped(false); vmReady = true; } else if (bs.vm_state === "Stopped") { setPhase2("pending"); + setVmInstanceStopped(true); setPhase2Msg("VM exists but is stopped"); } else { setPhase2("pending"); + setVmInstanceStopped(false); } } catch { if (cancelled) return; @@ -1815,8 +1820,8 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { return () => { cancelled = true; }; }, []); // eslint-disable-line react-hooks/exhaustive-deps - // ── Phase 1: Install Lima ── - const handleInstallLima = async () => { + // ── Phase 1: Install VM backend ── + const handleInstallVmBackend = async () => { setPhase1("running"); setPhase1Error(""); setPhase1Msg("Starting installation..."); @@ -1846,6 +1851,7 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { (_step, message) => setPhase2Msg(message), () => { setPhase2("done"); + setVmInstanceStopped(false); setPhase2Msg(""); refreshAppVmStatus(); // Unblocks cowork mode in the app }, @@ -1957,6 +1963,12 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { } }; + const inferredBackend = navigator.platform.toUpperCase().includes("WIN") ? "wsl" : "lima"; + const vmBackend = vmStatus?.backend ?? inferredBackend; + const vmEngineLabel = vmBackend === "wsl" ? "WSL Engine" : "Lima Engine"; + const vmBackendName = vmBackend === "wsl" ? "WSL" : "Lima"; + const vmPlatformLabel = vmBackend === "wsl" ? "Windows only" : "macOS only"; + // Determine overall VM status for the card header // Cowork mode is usable once Lima is installed and VM is running (phases 1+2). // Phase 3 (dependency installation) is optional and can run in the background. @@ -2027,7 +2039,7 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) {
Virtual Machine Cowork mode - macOS only + {vmPlatformLabel}
Local VM sandbox — runs agent code securely on your machine @@ -2056,23 +2068,26 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { {/* ── Setup in progress or not yet started ── */} {vmStatus && vmStatus.supported && !allDone && (
- {/* Phase 1: Lima */} + {/* Phase 1: VM backend */}
{phaseIcon(phase1)} - VM Engine + {vmEngineLabel} {phase1 === "done" && Installed} {phase1 === "running" && phase1Msg && {phase1Msg}} {phase1 === "pending" && ( - + )} {phase1 === "error" && ( - + )}
{phase1 === "error" && phase1Error && (

{phase1Error}

)} + {phase1 === "done" && ( +

{vmBackendName} is installed and available.

+ )}
{/* Phase 2: VM Build */} @@ -2083,7 +2098,9 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { {phase2 === "done" && Ready} {phase2 === "running" && phase2Msg && {phase2Msg}} {phase2 === "pending" && phase1 === "done" && ( - + )} {phase2 === "error" && ( From a269b8c7afcb84d3c86b22ba7d6c6d6f1c24e7ec Mon Sep 17 00:00:00 2001 From: aone Date: Tue, 24 Mar 2026 10:43:42 +0800 Subject: [PATCH 03/34] fix(demo): remove duplicate VM backend declarations in settings Resolve duplicate const declarations introduced during conflict resolution so the sandbox settings panel compiles cleanly and preserves feat/agent-loop behavior. Made-with: Cursor --- .../openagent_demo/frontend/src/components/SettingsModal.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libs/openagent_demo/frontend/src/components/SettingsModal.tsx b/libs/openagent_demo/frontend/src/components/SettingsModal.tsx index c523997d..31e4fb22 100644 --- a/libs/openagent_demo/frontend/src/components/SettingsModal.tsx +++ b/libs/openagent_demo/frontend/src/components/SettingsModal.tsx @@ -1733,11 +1733,6 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { } }; - const inferredBackend = navigator.platform.toUpperCase().includes("WIN") ? "wsl" : "lima"; - const vmBackend = vmStatus?.backend ?? inferredBackend; - const vmEngineLabel = vmBackend === "wsl" ? "WSL Engine" : "Lima Engine"; - const vmBackendName = vmBackend === "wsl" ? "WSL" : "Lima"; - const vmPlatformLabel = vmBackend === "wsl" ? "Windows only" : "macOS only"; // Cowork mode is usable once Lima is installed and VM is running (phases 1+2). // Phase 3 (dependency installation) can run in the background. const vmUsable = phase1 === "done" && phase2 === "done"; From 107e4a5831f8ffe99683ec35a83145161ef58009 Mon Sep 17 00:00:00 2001 From: aone Date: Tue, 24 Mar 2026 17:16:35 +0800 Subject: [PATCH 04/34] =?UTF-8?q?mod=EF=BC=9Afix=20windows=20sandbox=20pro?= =?UTF-8?q?blem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../openagent/computer/local/_wsl.py | 55 ++++++++++-- .../openagent/langchain/middleware.py | 23 +++++ .../backend/openagent_api/agent_manager.py | 61 ++++++++++---- .../backend/openagent_api/routes/setup.py | 83 +++++++++++++++---- libs/openagent_demo/frontend/src/api.ts | 6 +- .../frontend/src/components/WelcomeScreen.tsx | 30 +++++-- libs/openagent_demo/frontend/src/vmSetup.tsx | 17 +++- 7 files changed, 223 insertions(+), 52 deletions(-) diff --git a/libs/openagent/openagent/computer/local/_wsl.py b/libs/openagent/openagent/computer/local/_wsl.py index 5724ac0d..1a57931b 100644 --- a/libs/openagent/openagent/computer/local/_wsl.py +++ b/libs/openagent/openagent/computer/local/_wsl.py @@ -45,6 +45,42 @@ _PLATFORM = sys.platform +def _resolve_wsl_exe() -> str | None: + """Return a usable ``wsl.exe`` path. + + Some hosts (notably Electron-spawned backends) omit ``System32`` from + ``PATH``, so ``shutil.which`` fails even though WSL is installed. + """ + w = shutil.which("wsl.exe") or shutil.which("wsl") + if w: + return w + system_root = os.environ.get("SystemRoot") or os.environ.get("WINDIR") + if not system_root: + system_root = r"C:\Windows" + candidate = Path(system_root) / "System32" / "wsl.exe" + if candidate.is_file(): + return str(candidate) + return None + + +def _stable_host_cwd() -> str: + """Return a safe Windows cwd for launching ``wsl.exe``. + + WSL tries to translate the parent process cwd into Linux path on every + invocation. If that cwd is a stale UNC/session path, startup prints + ``CreateProcessCommon: ... chdir(...) failed`` and commands may run in an + unexpected context. Force a stable host cwd to avoid inheriting stale + per-session paths. + """ + system_root = os.environ.get("SystemRoot") or os.environ.get("WINDIR") or r"C:\Windows" + # ``wsl.exe`` exists under System32 on supported hosts; use that directory + # as a stable cwd if available, otherwise fall back to the process cwd. + safe_dir = Path(system_root) / "System32" + if safe_dir.is_dir(): + return str(safe_dir) + return os.getcwd() + + def _ensure_proactor_event_loop() -> None: """Switch to ``ProactorEventLoop`` if not already active. @@ -81,7 +117,7 @@ def _check_wsl_prerequisites() -> None: if _PLATFORM != "win32": msg = f"WSL is a Windows subsystem — it cannot run on {_PLATFORM}" raise UnsupportedPlatformError(msg) - if not shutil.which("wsl") and not shutil.which("wsl.exe"): + if _resolve_wsl_exe() is None: msg = "wsl.exe not found. Install WSL2: https://learn.microsoft.com/windows/wsl/install" raise MissingDependencyError(msg) @@ -99,6 +135,9 @@ class WslVM: def __init__(self, instance: str) -> None: _check_wsl_prerequisites() + wsl_exe = _resolve_wsl_exe() + assert wsl_exe is not None + self._wsl_exe = wsl_exe self._instance = instance self._unc_prefix: str | None = None # cached after first successful probe @@ -133,7 +172,7 @@ async def status(self) -> str | None: WslError: If the distribution exists but is WSL version 1. """ proc = await asyncio.create_subprocess_exec( - "wsl.exe", + self._wsl_exe, "--list", "--verbose", stdout=asyncio.subprocess.PIPE, @@ -241,7 +280,7 @@ async def build(self, tarball_path: Path | str) -> None: disk_dir.mkdir(parents=True, exist_ok=True) await self._run_wsl( - "wsl.exe", + self._wsl_exe, "--import", self._instance, str(disk_dir), @@ -274,7 +313,7 @@ async def start(self) -> None: if current != "Running": # Trigger start by running a trivial command. await self._run_wsl( - "wsl.exe", + self._wsl_exe, "-d", self._instance, "--", @@ -323,7 +362,7 @@ async def stop(self) -> None: return await self._run_wsl( - "wsl.exe", + self._wsl_exe, "--terminate", self._instance, timeout=60, @@ -333,7 +372,7 @@ async def delete(self) -> None: """Unregister the WSL distribution and clean up config (best-effort).""" with contextlib.suppress(WslError): await self._run_wsl( - "wsl.exe", + self._wsl_exe, "--unregister", self._instance, ) @@ -370,7 +409,7 @@ async def shell( """ inner = f"cd {shlex.quote(cwd)} && {command}" if cwd is not None else command - exec_args: list[str] = ["wsl.exe", "-d", self._instance] + exec_args: list[str] = [self._wsl_exe, "-d", self._instance] if user is not None: exec_args += ["-u", user] exec_args += ["--", "bash"] @@ -386,6 +425,7 @@ async def shell( stdin=asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + cwd=_stable_host_cwd(), ) try: @@ -462,6 +502,7 @@ async def _run_wsl(self, *cmd: str, timeout: float = 300) -> str: # noqa: ASYNC *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + cwd=_stable_host_cwd(), ) try: stdout_bytes, stderr_bytes = await asyncio.wait_for( diff --git a/libs/openagent/openagent/langchain/middleware.py b/libs/openagent/openagent/langchain/middleware.py index e81d0853..9eeaada0 100644 --- a/libs/openagent/openagent/langchain/middleware.py +++ b/libs/openagent/openagent/langchain/middleware.py @@ -381,6 +381,26 @@ async def abefore_model( Group 3: Annotators (system reminders). """ messages: list[BaseMessage] = list(state["messages"]) + env_prompt_updated = False + + # Workspace path can change after agent creation (e.g. cowork warm-up + # creates the agent, then PATCH mounts the user's folder and sets + # default_cwd). Tools see the new cwd, but the system prompt still + # lists the old ``pwd`` from EnvironmentResolver — follow-up turns may + # run size/list commands against the wrong directory. Refresh when pwd + # drifts (same pattern as compaction rebuild). + if self._environment_resolver is not None and self._prompt_profile is not None: + fresh_env = await self._environment_resolver.resolve() + old_wd = self._context.environment.working_dir if self._context.environment else None + if fresh_env.working_dir != old_wd: + self._context = replace(self._context, environment=fresh_env) + new_content = compose(self._prompt_profile, self._context) + if self._custom_prompt: + new_content = f"{self._custom_prompt}\n\n{new_content}" + if messages and isinstance(messages[0], SystemMessage): + messages[0] = SystemMessage(content=new_content) + self._system_prompt = new_content + env_prompt_updated = True # --- GROUP 1: Intercepts (compaction phases) --- phase = CompactionPhase(state.get("compaction_phase", CompactionPhase.NONE)) @@ -459,6 +479,9 @@ async def abefore_model( if images_extracted: return {"messages": LangGraphOverwrite(messages)} + if env_prompt_updated: + return {"messages": LangGraphOverwrite(messages)} + return None @hook_config(can_jump_to=["model"]) diff --git a/libs/openagent_demo/backend/openagent_api/agent_manager.py b/libs/openagent_demo/backend/openagent_api/agent_manager.py index 053e3e14..23490c2d 100644 --- a/libs/openagent_demo/backend/openagent_api/agent_manager.py +++ b/libs/openagent_demo/backend/openagent_api/agent_manager.py @@ -20,6 +20,7 @@ import json import logging import os +import shlex import sys from typing import Any @@ -65,6 +66,33 @@ def _set_computer_default_cwd(computer: Any, cwd: str | None) -> None: if callable(setter): setter(cwd) + async def _verify_session_dir_writable(self, session_name: str, guest_dir: str) -> None: + """Ensure the cowork session user can write to the selected working dir.""" + if self._vm_manager is None: + return + vm = getattr(self._vm_manager, "_vm", None) + if vm is None: + return + + probe = f"{guest_dir.rstrip('/')}/.openagent_write_probe_{os.getpid()}" + cmd = ( + f"test -d {shlex.quote(guest_dir)} && " + f"test -w {shlex.quote(guest_dir)} && " + f"touch {shlex.quote(probe)} && " + f"rm -f {shlex.quote(probe)}" + ) + result = await vm.shell(cmd, user=session_name) + if result.exit_code == 0: + return + + detail = (result.stderr or result.stdout or "").strip() or "unknown error" + raise RuntimeError( + "Selected working directory is mounted but not writable for the cowork session user. " + f"guest_dir={guest_dir} session={session_name} detail={detail}. " + "Please choose a writable local folder (recommended: D:\\code\\...), " + "or adjust Windows folder ACL/WSL mount permissions." + ) + # ── Computer management ── async def _ensure_computer( @@ -123,11 +151,12 @@ async def _ensure_computer( if self._vm_manager is None: import shutil - vm_backend_ready = ( - bool(shutil.which("wsl.exe") or shutil.which("wsl")) - if sys.platform == "win32" - else bool(shutil.which("limactl")) - ) + if sys.platform == "win32": + from openagent.computer.local._wsl import _resolve_wsl_exe + + vm_backend_ready = _resolve_wsl_exe() is not None + else: + vm_backend_ready = bool(shutil.which("limactl")) if not vm_backend_ready: raise RuntimeError( "Cowork mode requires VM setup. " @@ -158,10 +187,9 @@ async def _ensure_computer( logger.info("Creating new session (mounts=%s)...", session_mounts) computer = await self._vm_manager.computer(mounts=session_mounts) if default_cwd is not None: - self._set_computer_default_cwd( - computer, - default_cwd.format(session=computer.session_name), - ) + resolved_cwd = default_cwd.format(session=computer.session_name) + self._set_computer_default_cwd(computer, resolved_cwd) + await self._verify_session_dir_writable(computer.session_name, resolved_cwd) except FileNotFoundError: raise RuntimeError( "Cowork mode requires VM setup. " @@ -433,11 +461,12 @@ def _schedule_vm_warmup(self) -> None: import shutil - vm_backend_available = ( - bool(shutil.which("wsl.exe") or shutil.which("wsl")) - if sys.platform == "win32" - else bool(shutil.which("limactl")) - ) + if sys.platform == "win32": + from openagent.computer.local._wsl import _resolve_wsl_exe + + vm_backend_available = _resolve_wsl_exe() is not None + else: + vm_backend_available = bool(shutil.which("limactl")) if not vm_backend_available: return @@ -581,7 +610,9 @@ async def mount_working_dir(self, session_name: str, working_dir: str) -> None: await self._vm_manager.mount([mount], session=session_name) self._session_working_dirs[session_name] = (working_dir, new_target) computer = self._computers.get(session_name) - self._set_computer_default_cwd(computer, f"/sessions/{session_name}/mnt/{new_target}") + resolved_cwd = f"/sessions/{session_name}/mnt/{new_target}" + self._set_computer_default_cwd(computer, resolved_cwd) + await self._verify_session_dir_writable(session_name, resolved_cwd) logger.info("Mounted working dir %s for session %s", working_dir, session_name) # Remove the stale mount-point directory left behind after unmount. diff --git a/libs/openagent_demo/backend/openagent_api/routes/setup.py b/libs/openagent_demo/backend/openagent_api/routes/setup.py index 41472442..d1b0d82a 100644 --- a/libs/openagent_demo/backend/openagent_api/routes/setup.py +++ b/libs/openagent_demo/backend/openagent_api/routes/setup.py @@ -182,7 +182,23 @@ def _lima_status() -> dict[str, object]: def _wsl_cmd() -> str | None: - return shutil.which("wsl.exe") or shutil.which("wsl") + """Resolve path to ``wsl.exe``. + + Electron / minimal service environments sometimes omit ``System32`` from + ``PATH``, which makes ``shutil.which`` fail even though WSL is installed. + Fall back to the well-known location so ``/api/setup/vm`` reports + ``installed: true`` and subprocess launches succeed. + """ + w = shutil.which("wsl.exe") or shutil.which("wsl") + if w: + return w + system_root = os.environ.get("SystemRoot") or os.environ.get("WINDIR") + if not system_root: + system_root = r"C:\Windows" + candidate = Path(system_root) / "System32" / "wsl.exe" + if candidate.is_file(): + return str(candidate) + return None def _decode_wsl_output(raw: bytes) -> str: @@ -209,10 +225,11 @@ def _parse_wsl_list(text: str) -> list[dict[str, str]]: async def _wsl_list() -> list[dict[str, str]]: - if not _wsl_cmd(): + wsl_exe = _wsl_cmd() + if not wsl_exe: return [] proc = await asyncio.create_subprocess_exec( - "wsl.exe", + wsl_exe, "--list", "--verbose", stdout=asyncio.subprocess.PIPE, @@ -232,6 +249,24 @@ async def _wsl_instance_status() -> str | None: return None +# ``wsl -l -v`` uses the Windows display language for the STATE column. +# Cowork only needs the ``openagent`` distro to exist; WSL starts it on demand. +_WSL_COWORK_READY_STATES = frozenset( + { + "Running", + "Stopped", + "正在运行", + "已停止", + } +) + + +def _wsl_distro_ready_for_cowork(state: str | None) -> bool: + if state is None: + return False + return state.strip() in _WSL_COWORK_READY_STATES + + def _wsl_status() -> dict[str, object]: wsl = _wsl_cmd() return {"installed": bool(wsl), "path": wsl, "managed": False} @@ -250,8 +285,11 @@ def _win_path_to_wsl(path: Path | str) -> str: async def _wsl_shell(cmd: str, *, timeout: float = 60) -> tuple[int, str, str]: + wsl_exe = _wsl_cmd() + if not wsl_exe: + return 1, "", "wsl.exe not found" proc = await asyncio.create_subprocess_exec( - "wsl.exe", + wsl_exe, "-d", _WSL_INSTANCE, "--", @@ -348,8 +386,10 @@ def sse(event: str, data: dict[str, object]) -> str: return yield sse("progress", {"step": "installing", "message": "Installing WSL components..."}) + # ``wsl.exe`` may exist in System32 even when not on PATH + wsl_for_install = _wsl_cmd() or str(Path(os.environ.get("SystemRoot", r"C:\Windows")) / "System32" / "wsl.exe") proc = await asyncio.create_subprocess_exec( - "wsl.exe", + wsl_for_install, "--install", "--no-distribution", stdout=asyncio.subprocess.PIPE, @@ -407,22 +447,23 @@ def _vm_status() -> dict[str, object]: async def get_vm_status() -> dict[str, object]: """Check whether the VM backend is available. - Returns ``vm_ready: true`` only when the backend is installed AND - the VM instance is running — i.e. cowork mode can start sessions - immediately. + Returns ``vm_ready: true`` when cowork can start: Lima needs the instance + **Running**; WSL accepts **Running** or **Stopped** (distro exists — WSL + starts it on first ``wsl -d``). Localized ``wsl -l -v`` state strings are + recognized where known. """ result = _vm_status() - # Quick readiness check: installed + instance running? vm_ready = False instance_status: str | None = None if result.get("installed"): try: if result.get("backend") == "lima": instance_status = await _lima_instance_status() + vm_ready = instance_status == "Running" elif result.get("backend") == "wsl": instance_status = await _wsl_instance_status() - vm_ready = instance_status == "Running" + vm_ready = _wsl_distro_ready_for_cowork(instance_status) except Exception: pass @@ -660,7 +701,8 @@ async def _run_lima(self) -> None: self._error = f"exit {proc.returncode}" async def _run_wsl(self) -> None: - if not _wsl_cmd(): + wsl_exe = _wsl_cmd() + if not wsl_exe: self._emit("error", {"message": "WSL is not installed. Install it first in Phase 1."}) self._status = "error" self._error = "WSL missing" @@ -675,7 +717,7 @@ async def _run_wsl(self) -> None: if status == "Stopped": self._emit("progress", {"step": "starting", "message": "Starting existing WSL distro..."}) proc = await asyncio.create_subprocess_exec( - "wsl.exe", "-d", _WSL_INSTANCE, "--", "echo", "ok", + wsl_exe, "-d", _WSL_INSTANCE, "--", "echo", "ok", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) @@ -697,7 +739,7 @@ async def _run_wsl(self) -> None: has_source = any(e["name"].lower() == _WSL_EXPORT_SOURCE.lower() for e in entries) if not has_source: proc = await asyncio.create_subprocess_exec( - "wsl.exe", "--install", "-d", _WSL_EXPORT_SOURCE, + wsl_exe, "--install", "-d", _WSL_EXPORT_SOURCE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) @@ -722,7 +764,7 @@ async def _run_wsl(self) -> None: self._emit("progress", {"step": "creating", "message": "Exporting Ubuntu rootfs..."}) proc_export = await asyncio.create_subprocess_exec( - "wsl.exe", "--export", _WSL_EXPORT_SOURCE, str(export_tar), + wsl_exe, "--export", _WSL_EXPORT_SOURCE, str(export_tar), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) @@ -737,7 +779,7 @@ async def _run_wsl(self) -> None: self._emit("progress", {"step": "creating", "message": "Importing OpenAgent WSL distro..."}) proc_import = await asyncio.create_subprocess_exec( - "wsl.exe", "--import", _WSL_INSTANCE, str(import_dir), str(export_tar), "--version", "2", + wsl_exe, "--import", _WSL_INSTANCE, str(import_dir), str(export_tar), "--version", "2", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) @@ -752,7 +794,7 @@ async def _run_wsl(self) -> None: self._emit("progress", {"step": "starting", "message": "Starting OpenAgent WSL distro..."}) proc_start = await asyncio.create_subprocess_exec( - "wsl.exe", "-d", _WSL_INSTANCE, "--", "echo", "ok", + wsl_exe, "-d", _WSL_INSTANCE, "--", "echo", "ok", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) @@ -945,8 +987,15 @@ async def _run_wsl(self, **kwargs: object) -> None: if force: cmd += " --force" + wsl_exe = _wsl_cmd() + if not wsl_exe: + self._emit("error", {"message": "wsl.exe not found — cannot provision"}) + self._status = "error" + self._error = "WSL missing" + return + proc = await asyncio.create_subprocess_exec( - "wsl.exe", "-d", _WSL_INSTANCE, "--", "bash", "-lc", cmd, + wsl_exe, "-d", _WSL_INSTANCE, "--", "bash", "-lc", cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) diff --git a/libs/openagent_demo/frontend/src/api.ts b/libs/openagent_demo/frontend/src/api.ts index c9a6abc1..204ad387 100644 --- a/libs/openagent_demo/frontend/src/api.ts +++ b/libs/openagent_demo/frontend/src/api.ts @@ -63,7 +63,11 @@ export async function updateWarmSession(sessionId: string, updates: { working_di headers: { "Content-Type": "application/json" }, body: JSON.stringify(updates), }); - if (!res.ok) throw new Error(`Failed to update session: ${res.statusText}`); + if (!res.ok) { + const detail = await res.json().catch(() => null); + const msg = detail?.detail || res.statusText || `HTTP ${res.status}`; + throw new Error(`Failed to update session (${res.status}): ${msg}`); + } return res.json(); } diff --git a/libs/openagent_demo/frontend/src/components/WelcomeScreen.tsx b/libs/openagent_demo/frontend/src/components/WelcomeScreen.tsx index d26757a0..f7c60c68 100644 --- a/libs/openagent_demo/frontend/src/components/WelcomeScreen.tsx +++ b/libs/openagent_demo/frontend/src/components/WelcomeScreen.tsx @@ -50,6 +50,7 @@ export default function WelcomeScreen({ onSubmit, mode, onOpenSettings }: Welcom const [current, setCurrent] = useState(0); const [prev, setPrev] = useState(null); const [selectedFolder, setSelectedFolder] = useState(""); + const [mountingFolder, setMountingFolder] = useState(false); const [pendingFiles, setPendingFiles] = useState([]); const textareaRef = useRef(null); const fileRef = useRef(null); @@ -116,7 +117,7 @@ export default function WelcomeScreen({ onSubmit, mode, onOpenSettings }: Welcom if (sandboxBlocked) { flashE2bHint(); return; } const trimmed = value.trim(); const hasContent = trimmed || doneFiles.length > 0; - if (!hasContent || anyUploading) return; + if (!hasContent || anyUploading || mountingFolder) return; const opts: { workingDir?: string; attachments?: Attachment[] } = {}; if (selectedFolder) opts.workingDir = selectedFolder; if (doneFiles.length > 0) { @@ -125,7 +126,7 @@ export default function WelcomeScreen({ onSubmit, mode, onOpenSettings }: Welcom onSubmit(trimmed, Object.keys(opts).length > 0 ? opts : undefined); setValue(""); setPendingFiles([]); - }, [value, onSubmit, selectedFolder, doneFiles, anyUploading, sandboxBlocked, flashE2bHint]); + }, [value, onSubmit, selectedFolder, doneFiles, anyUploading, mountingFolder, sandboxBlocked, flashE2bHint]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { @@ -196,9 +197,15 @@ export default function WelcomeScreen({ onSubmit, mode, onOpenSettings }: Welcom setSelectedFolder(folder); if (warmSessionId && folder) { // Mount the folder in the warm session - updateWarmSession(warmSessionId, { working_dir: folder }).catch(() => { - dispatch({ type: "SHOW_NOTIFICATION", payload: { message: "Failed to mount folder", type: "error" } }); - }); + setMountingFolder(true); + updateWarmSession(warmSessionId, { working_dir: folder }) + .catch((err: unknown) => { + const msg = err instanceof Error ? err.message : String(err); + // Benign race: warm session may be claimed by conversation creation. + if (msg.includes("(404)") && msg.includes("Session was claimed")) return; + dispatch({ type: "SHOW_NOTIFICATION", payload: { message: "Failed to mount folder", type: "error" } }); + }) + .finally(() => setMountingFolder(false)); } // If warmSessionId isn't ready yet, the effect below will flush when it arrives }, [warmSessionId, dispatch, vmNotReady, flashE2bHint]); @@ -206,9 +213,14 @@ export default function WelcomeScreen({ onSubmit, mode, onOpenSettings }: Welcom // Flush pending folder mount when warm session becomes available useEffect(() => { if (warmSessionId && selectedFolder) { - updateWarmSession(warmSessionId, { working_dir: selectedFolder }).catch(() => { - dispatch({ type: "SHOW_NOTIFICATION", payload: { message: "Failed to mount folder", type: "error" } }); - }); + setMountingFolder(true); + updateWarmSession(warmSessionId, { working_dir: selectedFolder }) + .catch((err: unknown) => { + const msg = err instanceof Error ? err.message : String(err); + if (msg.includes("(404)") && msg.includes("Session was claimed")) return; + dispatch({ type: "SHOW_NOTIFICATION", payload: { message: "Failed to mount folder", type: "error" } }); + }) + .finally(() => setMountingFolder(false)); } // Only trigger when warmSessionId changes (not on every folder change — // handleFolderChange already handles that when the session is ready) @@ -354,7 +366,7 @@ export default function WelcomeScreen({ onSubmit, mode, onOpenSettings }: Welcom + {isPreparingRequest && ( +
+ + Preparing request... +
+ )} {missingE2bKey && (
E2B API key required —{" "} diff --git a/libs/openagent_demo/frontend/src/components/WelcomeScreen.tsx b/libs/openagent_demo/frontend/src/components/WelcomeScreen.tsx index f7c60c68..5785f74a 100644 --- a/libs/openagent_demo/frontend/src/components/WelcomeScreen.tsx +++ b/libs/openagent_demo/frontend/src/components/WelcomeScreen.tsx @@ -25,6 +25,7 @@ interface WelcomeScreenProps { export default function WelcomeScreen({ onSubmit, mode, onOpenSettings }: WelcomeScreenProps) { const { state, dispatch } = useAppContext(); const warmSessionId = state.warmSessionId; + const isPreparingRequest = state.isRequestPending; const isCowork = mode === "cowork"; const noModels = !state.serverConfig?.models?.length; const missingE2bKey = !isCowork && !state.serverConfig?.sandbox?.e2b_api_key; @@ -112,12 +113,22 @@ export default function WelcomeScreen({ onSubmit, mode, onOpenSettings }: Welcom const doneFiles = pendingFiles.filter((f) => f.status === "done"); const anyUploading = pendingFiles.some((f) => f.status === "uploading"); + const sendDisabled = (!value.trim() && doneFiles.length === 0) || anyUploading || mountingFolder || isPreparingRequest || noModels || sandboxBlocked; + const sendTitle = noModels + ? "Configure a model in Settings first" + : mountingFolder + ? "Mounting folder..." + : isPreparingRequest + ? "Preparing request..." + : sandboxBlocked + ? "Sandbox setup required" + : "Send message"; const handleSubmit = useCallback(() => { if (sandboxBlocked) { flashE2bHint(); return; } const trimmed = value.trim(); const hasContent = trimmed || doneFiles.length > 0; - if (!hasContent || anyUploading || mountingFolder) return; + if (!hasContent || anyUploading || mountingFolder || isPreparingRequest) return; const opts: { workingDir?: string; attachments?: Attachment[] } = {}; if (selectedFolder) opts.workingDir = selectedFolder; if (doneFiles.length > 0) { @@ -126,7 +137,7 @@ export default function WelcomeScreen({ onSubmit, mode, onOpenSettings }: Welcom onSubmit(trimmed, Object.keys(opts).length > 0 ? opts : undefined); setValue(""); setPendingFiles([]); - }, [value, onSubmit, selectedFolder, doneFiles, anyUploading, mountingFolder, sandboxBlocked, flashE2bHint]); + }, [value, onSubmit, selectedFolder, doneFiles, anyUploading, mountingFolder, isPreparingRequest, sandboxBlocked, flashE2bHint]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { @@ -366,11 +377,23 @@ export default function WelcomeScreen({ onSubmit, mode, onOpenSettings }: Welcom + {mountingFolder && ( +
+ + 正在挂载目录... +
+ )} + {!mountingFolder && isPreparingRequest && ( +
+ + 正在准备请求... +
+ )} {sandboxBlocked && (
{missingE2bKey ? "E2B API key required" : "VM setup required"} —{" "} diff --git a/libs/openagent_demo/frontend/src/store.ts b/libs/openagent_demo/frontend/src/store.ts index 27f6b414..80bbe71b 100644 --- a/libs/openagent_demo/frontend/src/store.ts +++ b/libs/openagent_demo/frontend/src/store.ts @@ -20,6 +20,8 @@ export interface AppState { activeConversationId: string | null; /** Pre-conversation warm session ID (exists before first message). */ warmSessionId: string | null; + /** True after user submits until SSE stream starts (or request fails). */ + isRequestPending: boolean; isStreaming: boolean; streamingBlocks: ContentBlock[]; streamingMessageId: string | null; @@ -47,6 +49,7 @@ export const initialState: AppState = { conversations: [], activeConversationId: null, warmSessionId: null, + isRequestPending: false, isStreaming: false, streamingBlocks: [], streamingMessageId: null, @@ -74,6 +77,8 @@ export type Action = | { type: "DELETE_CONVERSATION"; payload: string } | { type: "SET_ACTIVE_CONVERSATION"; payload: string | null } | { type: "SET_WARM_SESSION"; payload: string | null } + | { type: "REQUEST_START" } + | { type: "REQUEST_END" } | { type: "ADD_USER_MESSAGE"; payload: { conversationId: string; message: Message } } | { type: "STREAM_START"; payload: { messageId: string } } | { type: "STREAM_TEXT_DELTA"; payload: string } @@ -506,6 +511,12 @@ export function reducer(state: AppState, action: Action): AppState { case "SET_WARM_SESSION": return { ...state, warmSessionId: action.payload }; + case "REQUEST_START": + return { ...state, isRequestPending: true }; + + case "REQUEST_END": + return { ...state, isRequestPending: false }; + case "ADD_USER_MESSAGE": return { ...state, @@ -525,6 +536,7 @@ export function reducer(state: AppState, action: Action): AppState { }; return { ...state, + isRequestPending: false, isStreaming: true, streamingBlocks: [], streamingMessageId: action.payload.messageId, @@ -632,6 +644,7 @@ export function reducer(state: AppState, action: Action): AppState { const streamConvId = state.streamingConversationId; return { ...state, + isRequestPending: false, isStreaming: false, streamingBlocks: [], streamingMessageId: null, @@ -655,6 +668,7 @@ export function reducer(state: AppState, action: Action): AppState { case "STREAM_ERROR": return { ...state, + isRequestPending: false, isStreaming: false, streamingConversationId: null, streamingBlocks: appendTextDelta( diff --git a/libs/openagent_demo/start-dev.ps1 b/libs/openagent_demo/start-dev.ps1 new file mode 100644 index 00000000..350a7964 --- /dev/null +++ b/libs/openagent_demo/start-dev.ps1 @@ -0,0 +1,71 @@ +param( + [switch]$CurrentWindow +) + +$ErrorActionPreference = "Stop" + +function Test-CommandExists { + param([Parameter(Mandatory = $true)][string]$Name) + return [bool](Get-Command $Name -ErrorAction SilentlyContinue) +} + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$BackendDir = Join-Path $ScriptDir "backend" +$FrontendDir = Join-Path $ScriptDir "frontend" + +if (-not (Test-Path $BackendDir)) { + throw "Backend directory not found: $BackendDir" +} +if (-not (Test-Path $FrontendDir)) { + throw "Frontend directory not found: $FrontendDir" +} + +if (-not (Test-CommandExists -Name "uv")) { + throw "Command 'uv' not found. Please install uv first." +} +if (-not (Test-CommandExists -Name "npm")) { + throw "Command 'npm' not found. Please install Node.js/npm first." +} + +$backendCmd = @" +Set-Location '$BackendDir' +if (-not (Test-Path '.venv')) { + Write-Host '[backend] .venv not found, running uv sync...' -ForegroundColor Yellow + uv sync +} +Write-Host '[backend] starting on http://127.0.0.1:8000' -ForegroundColor Cyan +uv run uvicorn openagent_api.main:app --host 127.0.0.1 --port 8000 +"@ + +$frontendCmd = @" +Set-Location '$FrontendDir' +if (-not (Test-Path 'node_modules')) { + Write-Host '[frontend] node_modules not found, running npm install...' -ForegroundColor Yellow + npm install +} +Write-Host '[frontend] starting on http://localhost:3000' -ForegroundColor Cyan +npm run dev +"@ + +if ($CurrentWindow) { + Write-Host "Starting backend in a background job (CurrentWindow mode)..." -ForegroundColor Green + Start-Job -Name "openagent-backend" -ScriptBlock { + param($cmd) + powershell -NoProfile -NoExit -Command $cmd + } -ArgumentList $backendCmd | Out-Null + + Write-Host "Starting frontend in current window..." -ForegroundColor Green + Invoke-Expression $frontendCmd + exit 0 +} + +Write-Host "Launching backend and frontend in new PowerShell windows..." -ForegroundColor Green +Start-Process powershell -ArgumentList "-NoProfile", "-NoExit", "-ExecutionPolicy", "Bypass", "-Command", $backendCmd | Out-Null +Start-Process powershell -ArgumentList "-NoProfile", "-NoExit", "-ExecutionPolicy", "Bypass", "-Command", $frontendCmd | Out-Null + +Write-Host "" +Write-Host "OpenAgent dev services launched." -ForegroundColor Green +Write-Host "Frontend: http://localhost:3000" +Write-Host "Backend : http://127.0.0.1:8000/health" +Write-Host "" +Write-Host "Tip: run with -CurrentWindow if you want frontend in the current shell." From 44684e4a5a358d976d8bd0713631cb9d6333988c Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Wed, 25 Mar 2026 21:25:39 +0800 Subject: [PATCH 07/34] fix(vm): harden WSL setup flow and rebuild prebuilt pipeline --- .gitattributes | 2 + libs/openagent/sandbox/vm/setup/setup.sh | 143 ++++++-- .../sandbox/vm/setup/steps/03_apt.sh | 68 +--- .../sandbox/vm/setup/steps/04_npm.sh | 34 +- .../sandbox/vm/setup/steps/05_pip.sh | 64 +--- .../sandbox/vm/setup/steps/06_playwright.sh | 38 +- .../backend/openagent_api/routes/setup.py | 327 ++++++++++++++++-- libs/openagent_demo/electron/main.js | 66 ++++ libs/openagent_demo/electron/preload.js | 1 + .../electron/scripts/build-all.ps1 | 9 +- .../electron/scripts/build-backend.ps1 | 66 ++-- .../electron/scripts/prepare-wsl-prebuilt.ps1 | 37 ++ .../frontend/src/components/SettingsModal.tsx | 15 +- .../openagent_demo/frontend/src/electron.d.ts | 10 + libs/openagent_demo/frontend/src/vmSetup.tsx | 98 ++++++ 15 files changed, 754 insertions(+), 224 deletions(-) create mode 100644 .gitattributes create mode 100644 libs/openagent_demo/electron/scripts/prepare-wsl-prebuilt.ps1 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..0793bb27 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +libs/openagent/sandbox/vm/setup/*.sh text eol=lf +libs/openagent/sandbox/vm/setup/steps/*.sh text eol=lf diff --git a/libs/openagent/sandbox/vm/setup/setup.sh b/libs/openagent/sandbox/vm/setup/setup.sh index 7d62c09f..18454caa 100755 --- a/libs/openagent/sandbox/vm/setup/setup.sh +++ b/libs/openagent/sandbox/vm/setup/setup.sh @@ -1,6 +1,6 @@ #!/bin/bash # ============================================================================= -# OpenAgent VM Setup — Orchestrator +# OpenAgent VM Setup -Orchestrator # ============================================================================= # Discovers and runs step scripts in order with progress reporting, # resumability (marker files), concurrency protection (flock), and @@ -21,7 +21,7 @@ set -uo pipefail # No -e: we handle errors per-step. -# ── Constants ──────────────────────────────────────────────────────────────── +# ------ Constants ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" STEPS_DIR="${SCRIPT_DIR}/steps" MARKER_DIR="/var/lib/openagent/setup" @@ -29,18 +29,31 @@ LOG_DIR="/var/log/openagent/setup" LOCK_FILE="/var/run/openagent-setup.lock" LOCK_FD=9 -# ── Environment (inherited by steps) ──────────────────────────────────────── +# ------ Environment (inherited by steps) ------------------------------------------------------------------------------------------------------------------------ export DEBIAN_FRONTEND=noninteractive export PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers export MARKER_DIR LOG_DIR - -# ── CLI defaults ───────────────────────────────────────────────────────────── +if grep -qi microsoft /proc/version 2>/dev/null; then + _OPENAGENT_DEFAULT_CN_MIRRORS=1 +else + _OPENAGENT_DEFAULT_CN_MIRRORS=0 +fi +OPENAGENT_USE_CN_MIRRORS="${OPENAGENT_USE_CN_MIRRORS:-${_OPENAGENT_DEFAULT_CN_MIRRORS}}" +OPENAGENT_APT_MIRROR="${OPENAGENT_APT_MIRROR:-https://mirrors.ustc.edu.cn/ubuntu}" +OPENAGENT_APT_PORTS_MIRROR="${OPENAGENT_APT_PORTS_MIRROR:-https://mirrors.ustc.edu.cn/ubuntu-ports}" +OPENAGENT_PIP_INDEX_URL="${OPENAGENT_PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}" +OPENAGENT_NPM_REGISTRY="${OPENAGENT_NPM_REGISTRY:-https://registry.npmmirror.com}" +OPENAGENT_PLAYWRIGHT_DOWNLOAD_HOST="${OPENAGENT_PLAYWRIGHT_DOWNLOAD_HOST:-https://npmmirror.com/mirrors/playwright}" +export OPENAGENT_USE_CN_MIRRORS OPENAGENT_APT_MIRROR OPENAGENT_APT_PORTS_MIRROR +export OPENAGENT_PIP_INDEX_URL OPENAGENT_NPM_REGISTRY OPENAGENT_PLAYWRIGHT_DOWNLOAD_HOST + +# ------ CLI defaults --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- FORCE=false SINGLE_STEP="" LIST_ONLY=false RESET=false -# ── CLI parsing ────────────────────────────────────────────────────────────── +# ------ CLI parsing ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ usage() { echo "Usage: sudo bash setup.sh [OPTIONS]" echo "" @@ -63,16 +76,16 @@ while [[ $# -gt 0 ]]; do esac done -# ── Root check ─────────────────────────────────────────────────────────────── +# ------ Root check --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- if [[ "$(id -u)" -ne 0 ]]; then echo "ERROR: Must run as root (sudo)." >&2 exit 1 fi -# ── Directory setup ────────────────────────────────────────────────────────── +# ------ Directory setup ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ mkdir -p "$MARKER_DIR" "$LOG_DIR" -# ── emit() — progress protocol ────────────────────────────────────────────── +# ------ emit() -progress protocol ------------------------------------------------------------------------------------------------------------------------------------------ # Writes to fd 3 which points to the original stdout (what the backend reads). # All other output (package managers) goes to the log file. emit() { @@ -82,7 +95,7 @@ emit() { } export -f emit -# ── apt_install — retry wrapper ────────────────────────────────────────────── +# ------ apt_install -retry wrapper ------------------------------------------------------------------------------------------------------------------------------------------ apt_install() { local max_attempts=5 local delay=3 @@ -131,22 +144,34 @@ apt_install() { } export -f apt_install -# ── pip_install — retry wrapper ────────────────────────────────────────────── +# ------ pip_install -retry wrapper ------------------------------------------------------------------------------------------------------------------------------------------ pip_install() { local max_attempts=5 local delay=5 local attempt=1 + local use_cn_mirrors="${OPENAGENT_USE_CN_MIRRORS:-0}" local pip_opts=( - --break-system-packages --timeout 120 --retries 3 + --no-cache-dir ) + if pip3 help install 2>/dev/null | grep -q -- "--break-system-packages"; then + pip_opts+=(--break-system-packages) + fi while [[ $attempt -le $max_attempts ]]; do echo ">>> pip install attempt $attempt/$max_attempts (${#} packages)" if pip3 install "${pip_opts[@]}" "$@"; then return 0 fi + if [[ "$use_cn_mirrors" == "1" ]]; then + echo ">>> Mirror install failed, retrying with official PyPI..." + if PIP_INDEX_URL="https://pypi.org/simple" \ + PIP_EXTRA_INDEX_URL="" \ + pip3 install "${pip_opts[@]}" "$@"; then + return 0 + fi + fi echo ">>> Attempt $attempt failed. Retrying in ${delay}s..." sleep $delay delay=$((delay * 2)) @@ -164,6 +189,14 @@ pip_install() { pkg_ok=true break fi + if [[ "$use_cn_mirrors" == "1" ]]; then + if PIP_INDEX_URL="https://pypi.org/simple" \ + PIP_EXTRA_INDEX_URL="" \ + pip3 install "${pip_opts[@]}" "$pkg" 2>&1; then + pkg_ok=true + break + fi + fi echo ">>> Failed: $pkg (attempt $pkg_attempt/3)" sleep $((pkg_attempt * 3)) pkg_attempt=$((pkg_attempt + 1)) @@ -182,11 +215,61 @@ pip_install() { } export -f pip_install -# ── Marker helpers ─────────────────────────────────────────────────────────── +configure_cn_mirrors() { + if [[ "${OPENAGENT_USE_CN_MIRRORS}" != "1" ]]; then + return 0 + fi + + emit _meta progress "Applying China mirrors (APT/PIP/NPM/Playwright)" + + # sed replacement escapes for arbitrary mirror strings (e.g. containing '&' or '|') + local apt_mirror_esc apt_ports_mirror_esc + apt_mirror_esc="${OPENAGENT_APT_MIRROR//\\/\\\\}" + apt_mirror_esc="${apt_mirror_esc//&/\\&}" + apt_mirror_esc="${apt_mirror_esc//|/\\|}" + apt_ports_mirror_esc="${OPENAGENT_APT_PORTS_MIRROR//\\/\\\\}" + apt_ports_mirror_esc="${apt_ports_mirror_esc//&/\\&}" + apt_ports_mirror_esc="${apt_ports_mirror_esc//|/\\|}" + + if [[ -f /etc/apt/sources.list ]]; then + cp -n /etc/apt/sources.list /etc/apt/sources.list.openagent.bak 2>/dev/null || true + sed -Ei \ + -e "s|https?://(archive|security)\.ubuntu\.com/ubuntu|${apt_mirror_esc}|g" \ + -e "s|https?://ports\.ubuntu\.com/ubuntu-ports|${apt_ports_mirror_esc}|g" \ + /etc/apt/sources.list 2>/dev/null || true + fi + + if [[ -f /etc/apt/sources.list.d/ubuntu.sources ]]; then + cp -n /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.openagent.bak 2>/dev/null || true + sed -Ei \ + -e "s|^URIs:[[:space:]]*https?://(archive|security)\.ubuntu\.com/ubuntu/?$|URIs: ${apt_mirror_esc}|g" \ + -e "s|^URIs:[[:space:]]*https?://ports\.ubuntu\.com/ubuntu-ports/?$|URIs: ${apt_ports_mirror_esc}|g" \ + /etc/apt/sources.list.d/ubuntu.sources 2>/dev/null || true + fi + + cat >/etc/pip.conf </etc/profile.d/openagent-mirrors.sh < "${MARKER_DIR}/$1.done"; } -# ── Step discovery ─────────────────────────────────────────────────────────── +# ------ Step discovery --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- discover_steps() { for f in "${STEPS_DIR}"/*.sh; do [[ -f "$f" ]] || continue @@ -199,41 +282,41 @@ step_desc() { sed -n '2s/^# *//p' "$1" } -# ── --reset ────────────────────────────────────────────────────────────────── +# ------ --reset ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ if [[ "$RESET" == true ]]; then rm -f "${MARKER_DIR}"/*.done echo "All markers cleared." exit 0 fi -# ── --list ─────────────────────────────────────────────────────────────────── +# ------ --list --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- if [[ "$LIST_ONLY" == true ]]; then while IFS= read -r step_file; do step_id="$(basename "$step_file" .sh)" if step_done "$step_id"; then echo "[done] $step_id ($(cat "${MARKER_DIR}/${step_id}.done"))" else - echo "[pending] $step_id — $(step_desc "$step_file")" + echo "[pending] $step_id -$(step_desc "$step_file")" fi done < <(discover_steps) exit 0 fi -# ── Concurrency lock ───────────────────────────────────────────────────────── +# ------ Concurrency lock --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- exec 9>"$LOCK_FILE" if ! flock -n $LOCK_FD; then echo "ERROR: Another setup instance is running (lockfile: $LOCK_FILE)" >&2 exit 1 fi -# ── fd redirection ─────────────────────────────────────────────────────────── -# fd 3 = original stdout → backend reads @@SETUP: lines from here -# stdout + stderr → log file (all package manager noise) +# ------ fd redirection --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +# fd 3 = original stdout -backend reads @@SETUP: lines from here +# stdout + stderr -log file (all package manager noise) LOGFILE="${LOG_DIR}/setup-$(date +%Y%m%d-%H%M%S).log" exec 3>&1 exec 1>>"$LOGFILE" 2>&1 -# ── Signal handling ────────────────────────────────────────────────────────── +# ------ Signal handling ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ HEARTBEAT_PID="" cleanup() { @@ -249,7 +332,7 @@ cancelled() { } trap cancelled SIGTERM SIGINT -# ── Heartbeat ──────────────────────────────────────────────────────────────── +# ------ Heartbeat ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ start_heartbeat() { local step_id="$1" ( @@ -269,7 +352,7 @@ stop_heartbeat() { fi } -# ── Preflight ──────────────────────────────────────────────────────────────── +# ------ Preflight ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ preflight() { emit _meta start "Preflight checks" @@ -281,7 +364,9 @@ preflight() { esac export ARCH - # Disk space (require ≥10 GB free on /) + configure_cn_mirrors + + # Disk space (require >= 10 GB free on /) local free_kb free_kb=$(df / --output=avail | tail -1 | tr -d ' ') if (( free_kb < 10485760 )); then @@ -292,7 +377,7 @@ preflight() { emit _meta done "Preflight OK (arch=$ARCH, free=$((free_kb / 1024))MB)" } -# ── run_step ───────────────────────────────────────────────────────────────── +# ------ run_step --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- run_step() { local step_file="$1" local step_id @@ -325,12 +410,12 @@ run_step() { mark_done "$step_id" emit "$step_id" done "Completed in ${elapsed}s" else - emit "$step_id" error "Failed (exit $rc) after ${elapsed}s — see $LOGFILE" + emit "$step_id" error "Failed (exit $rc) after ${elapsed}s - see $LOGFILE" return $rc fi } -# ── Main ───────────────────────────────────────────────────────────────────── +# ------ Main --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- main() { preflight @@ -362,7 +447,7 @@ main() { done < <(discover_steps) if [[ $failed -gt 0 ]]; then - emit _meta error "Setup failed — re-run to resume from failed step" + emit _meta error "Setup failed - re-run to resume from failed step" exit 1 fi diff --git a/libs/openagent/sandbox/vm/setup/steps/03_apt.sh b/libs/openagent/sandbox/vm/setup/steps/03_apt.sh index 8e1929a0..82bec2e1 100755 --- a/libs/openagent/sandbox/vm/setup/steps/03_apt.sh +++ b/libs/openagent/sandbox/vm/setup/steps/03_apt.sh @@ -1,71 +1,31 @@ #!/bin/bash -# System packages (core utils, Python, Java, PDF, LaTeX, fonts, etc.) +# System packages (minimal baseline; install extras on demand) set -uo pipefail # No -e: apt_install handles its own errors with retries. apt-get update -emit 03_apt progress "group=core_utils (17 packages)" +emit 03_apt progress "group=core_utils" apt_install \ - bash coreutils wget curl git zip unzip bzip2 xz-utils \ - file findutils patch perl jq tree sqlite3 ripgrep \ - netcat-openbsd apt-transport-https software-properties-common + bash coreutils wget curl git zip unzip jq tree ripgrep \ + file findutils patch sqlite3 || exit 1 -emit 03_apt progress "group=build_tools (2 packages)" -apt_install build-essential pkg-config +emit 03_apt progress "group=build_tools" +apt_install build-essential pkg-config || exit 1 -emit 03_apt progress "group=python (5 packages)" -apt_install python3 python3-dev python3-pip python3-venv pipx +emit 03_apt progress "group=python" +apt_install python3 python3-dev python3-pip python3-venv pipx || exit 1 -emit 03_apt progress "group=java (1 package)" -apt_install default-jre-headless +emit 03_apt progress "group=media" +apt_install imagemagick graphviz || exit 1 -emit 03_apt progress "group=pdf_tools (5 packages)" -apt_install poppler-utils qpdf pdftk-java wkhtmltopdf ghostscript - -emit 03_apt progress "group=pandoc (1 package)" -apt_install pandoc - -emit 03_apt progress "group=libreoffice (5 packages)" -apt_install \ - libreoffice-writer libreoffice-calc libreoffice-impress \ - libreoffice-common libreoffice-java-common - -emit 03_apt progress "group=media (3 packages)" -apt_install imagemagick graphviz ffmpeg - -emit 03_apt progress "group=ocr (2 packages)" -apt_install tesseract-ocr tesseract-ocr-eng - -emit 03_apt progress "group=latex (9 packages)" -apt_install \ - texlive-base texlive-latex-base texlive-latex-recommended \ - texlive-latex-extra texlive-fonts-recommended texlive-xetex \ - texlive-science texlive-pictures latexmk - -emit 03_apt progress "group=fonts (12 packages)" -apt_install \ - fonts-liberation2 fonts-dejavu fonts-freefont-ttf \ - fonts-noto-cjk fonts-noto-color-emoji \ - fonts-crosextra-caladea fonts-crosextra-carlito \ - fonts-lmodern fonts-texgyre fonts-opensymbol \ - fonts-wqy-zenhei fonts-ipafont-gothic - -emit 03_apt progress "group=x11_display (5 packages)" -apt_install \ - xvfb x11-xkb-utils xfonts-scalable xfonts-cyrillic xfonts-utils - -emit 03_apt progress "group=browser_libs (17 packages)" +emit 03_apt progress "group=fonts" apt_install \ - libnss3 libnss3-tools libatk1.0-0t64 libatk-bridge2.0-0t64 \ - libcups2t64 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \ - libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 \ - libcairo2 libatspi2.0-0t64 libgtk-3-0t64 libgtk-4-1 + fonts-liberation2 fonts-dejavu || exit 1 -emit 03_apt progress "group=dev_libs (7 packages)" +emit 03_apt progress "group=dev_libs" apt_install \ - libffi-dev zlib1g-dev libpng-dev libfreetype-dev libcairo2-dev \ - libglib2.0-dev libbz2-dev + libffi-dev zlib1g-dev libpng-dev libfreetype-dev libbz2-dev || exit 1 # Cleanup emit 03_apt progress "Cleaning apt cache" diff --git a/libs/openagent/sandbox/vm/setup/steps/04_npm.sh b/libs/openagent/sandbox/vm/setup/steps/04_npm.sh index 2d6fd675..014e24f0 100755 --- a/libs/openagent/sandbox/vm/setup/steps/04_npm.sh +++ b/libs/openagent/sandbox/vm/setup/steps/04_npm.sh @@ -1,31 +1,11 @@ #!/bin/bash -# NPM global packages +# NPM global packages (minimal baseline; install extras on demand) set -euo pipefail -emit 04_npm progress "Installing npm global packages" -npm install -g \ - docx@9 \ - pptxgenjs@4.0.1 \ - pdf-lib@1.17.1 \ - pdfjs-dist \ - marked \ - markdown-toc \ - markdownlint-cli \ - markdownlint-cli2 \ - remark-cli \ - remark-preset-lint-recommended \ - @mermaid-js/mermaid-cli \ - graphviz \ - react \ - react-dom \ - react-icons \ - typescript \ - ts-node \ - tsx \ - sharp \ - playwright - -if [[ "$ARCH" == "x86_64" ]]; then - emit 04_npm progress "Installing markdown-pdf (x86_64 only)" - npm install -g markdown-pdf +if [[ "${OPENAGENT_USE_CN_MIRRORS:-0}" == "1" ]]; then + emit 04_npm progress "Configuring npm registry mirror" + npm config set registry "${OPENAGENT_NPM_REGISTRY}" >/dev/null 2>&1 || true fi + +emit 04_npm progress "Installing npm global packages" +npm install -g typescript tsx playwright diff --git a/libs/openagent/sandbox/vm/setup/steps/05_pip.sh b/libs/openagent/sandbox/vm/setup/steps/05_pip.sh index e0236d87..1960556a 100755 --- a/libs/openagent/sandbox/vm/setup/steps/05_pip.sh +++ b/libs/openagent/sandbox/vm/setup/steps/05_pip.sh @@ -1,64 +1,16 @@ #!/bin/bash -# Python packages (11 batches) -set -uo pipefail -# No -e: pip_install handles its own retries. +# Python packages (minimal baseline; install extras on demand) +set -euo pipefail -# Preflight: fix blinker conflict with system Flask -emit 05_pip progress "Preflight: fixing blinker" -pip3 install --break-system-packages --timeout 120 --ignore-installed blinker +emit 05_pip progress "Core data & visualization" +pip_install numpy pandas matplotlib pillow -emit 05_pip progress "Batch 1/11 — Core numeric (4 packages)" -pip_install numpy pandas scipy sympy +emit 05_pip progress "Web & HTTP" +pip_install requests beautifulsoup4 lxml -emit 05_pip progress "Batch 2/11 — ML / CV (3 packages)" -pip_install scikit-learn scikit-image onnxruntime - -emit 05_pip progress "Batch 3/11 — ML / CV OpenCV (3 packages)" -pip_install opencv-python opencv-contrib-python opencv-python-headless - -emit 05_pip progress "Batch 4/11 — Visualization (3 packages)" -pip_install matplotlib seaborn networkx - -emit 05_pip progress "Batch 5/11 — Image / media (5 packages)" -pip_install pillow imageio imageio-ffmpeg Wand pytesseract - -emit 05_pip progress "Batch 6/11 — PDF tools (11 packages)" -pip_install \ - pdfplumber pdfminer.six pypdf pikepdf pdf2image pdfkit \ - img2pdf camelot-py tabula-py reportlab pypdfium2 pymupdf - -emit 05_pip progress "Batch 7/11 — Office documents (5 packages)" -pip_install python-docx python-pptx openpyxl xlsxwriter odfpy - -emit 05_pip progress "Batch 8/11 — Markdown / docs (11 packages)" +emit 05_pip progress "Utilities" pip_install \ - markitdown markdownify markdown grip mistune markdown-it-py \ - marko mkdocs mkdocs-material mkdocs-material-extensions \ - mkdocs-get-deps pymdown-extensions - -emit 05_pip progress "Batch 9/11 — Web / HTTP (5 packages)" -pip_install requests beautifulsoup4 lxml Flask httplib2 - -emit 05_pip progress "Batch 10/11 — Automation / browser (3 packages)" -pip_install playwright unoserver pyoo - -emit 05_pip progress "Batch 11/11 — System utilities (14 packages)" -pip_install \ - uv magika click colorama coloredlogs humanfriendly tabulate \ - python-dotenv psutil watchdog sounddevice pycairo graphviz freetype-py - -# Foundational (many already installed as transitive deps — pip will no-op) -emit 05_pip progress "Foundational / low-level (17 packages)" -pip_install \ - attrs bcrypt jsonschema python-magic livereload tornado PyYAML \ - certifi charset-normalizer cryptography defusedxml idna joblib \ - packaging protobuf python-dateutil pytz typing_extensions urllib3 - -# Platform-specific -if [[ "$ARCH" == "x86_64" ]]; then - emit 05_pip progress "Platform-specific: mediapipe (x86_64 only)" - pip_install "mediapipe>=0.10.32" -fi + uv click pyyaml python-dotenv tabulate # Cleanup emit 05_pip progress "Cleaning pip cache" diff --git a/libs/openagent/sandbox/vm/setup/steps/06_playwright.sh b/libs/openagent/sandbox/vm/setup/steps/06_playwright.sh index f017ffbf..be4d13ac 100755 --- a/libs/openagent/sandbox/vm/setup/steps/06_playwright.sh +++ b/libs/openagent/sandbox/vm/setup/steps/06_playwright.sh @@ -6,11 +6,13 @@ set -euo pipefail dpkg --configure -a || true apt-get install -y -f || true -# Install Playwright OS deps (apt) — retry-wrapped +# Install Playwright OS deps (apt) retry-wrapped max_attempts=5 +deps_ok=0 for ((attempt = 1; attempt <= max_attempts; attempt++)); do emit 06_playwright progress "install-deps attempt $attempt/$max_attempts" if npx playwright install-deps chromium; then + deps_ok=1 break fi echo ">>> Retrying in 5s..." @@ -20,9 +22,41 @@ for ((attempt = 1; attempt <= max_attempts; attempt++)); do apt-get update || true done +if [[ $deps_ok -ne 1 ]]; then + emit 06_playwright error "Failed to install Playwright system dependencies" + exit 1 +fi + # Download Chromium binary emit 06_playwright progress "Downloading Chromium binary" -PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers npx playwright install chromium +mirror_host="${OPENAGENT_PLAYWRIGHT_DOWNLOAD_HOST:-}" +browser_ok=0 +for ((attempt = 1; attempt <= max_attempts; attempt++)); do + if [[ "${OPENAGENT_USE_CN_MIRRORS:-0}" == "1" && -n "$mirror_host" ]]; then + emit 06_playwright progress "browser install attempt $attempt/$max_attempts (mirror)" + if PLAYWRIGHT_DOWNLOAD_HOST="$mirror_host" \ + PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers \ + npx playwright install chromium; then + browser_ok=1 + break + fi + emit 06_playwright progress "Mirror unavailable, retrying with official host" + else + emit 06_playwright progress "browser install attempt $attempt/$max_attempts" + fi + + if PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers npx playwright install chromium; then + browser_ok=1 + break + fi + echo ">>> Browser download attempt $attempt failed. Retrying in 5s..." + sleep 5 +done + +if [[ $browser_ok -ne 1 ]]; then + emit 06_playwright error "Failed to download Chromium browser" + exit 1 +fi # Allow PDF operations in ImageMagick (if restricted) if [[ -f /etc/ImageMagick-6/policy.xml ]] && \ diff --git a/libs/openagent_demo/backend/openagent_api/routes/setup.py b/libs/openagent_demo/backend/openagent_api/routes/setup.py index 2ce9d62e..6d754e7a 100644 --- a/libs/openagent_demo/backend/openagent_api/routes/setup.py +++ b/libs/openagent_demo/backend/openagent_api/routes/setup.py @@ -14,6 +14,7 @@ import os import platform import re +import shlex import shutil import subprocess as _sp import sys @@ -180,6 +181,10 @@ def _lima_status() -> dict[str, object]: _WSL_INSTANCE = "openagent" _WSL_EXPORT_SOURCE = "Ubuntu" +_WSL_PREBUILT_CANDIDATES = ( + "openagent-prebuilt.tar", + "openagent.tar", +) def _wsl_cmd() -> str | None: @@ -208,6 +213,68 @@ def _decode_wsl_output(raw: bytes) -> str: return raw.decode("utf-8", errors="replace") +def _combine_wsl_output(stdout_b: bytes | None, stderr_b: bytes | None) -> str: + """Decode and combine WSL stdout/stderr, preferring non-empty stderr first.""" + err = _decode_wsl_output(stderr_b or b"").strip() + out = _decode_wsl_output(stdout_b or b"").strip() + if err and out: + return f"{err}\n{out}" + return err or out + + +def _looks_like_missing_wsl_disk(msg: str) -> bool: + text = msg.lower() + return ( + "error_path_not_found" in text + or "mountdisk" in text + or "ext4.vhdx" in text + ) + + +def _wsl2_blocker_reason(text: str) -> str | None: + """Return a friendly reason when host cannot run WSL2.""" + t = (text or "").lower() + blockers = ( + "does not support wsl2", + "not support wsl2", + "wsl2", + "enablevirtualization", + "virtual machine platform", + "bios", + "当前计算机配置不支持 wsl2", + "虚拟机平台", + ) + if any(k in t for k in blockers): + return ( + "WSL2 is not available on this PC yet. Please enable " + "'Virtual Machine Platform', ensure virtualization is enabled in BIOS, " + "then reboot Windows and retry." + ) + return None + + +def _probe_wsl2_readiness() -> tuple[bool, str | None]: + """Check whether host is ready for WSL2-based distro import/start.""" + wsl = _wsl_cmd() + if not wsl: + return False, "wsl.exe not found" + try: + proc = _sp.run( + [wsl, "--status"], + stdout=_sp.PIPE, + stderr=_sp.PIPE, + timeout=8, + ) + except Exception as exc: # pragma: no cover - defensive + return False, str(exc) + + combined = _combine_wsl_output(proc.stdout, proc.stderr).strip() + reason = _wsl2_blocker_reason(combined) + if reason: + return False, reason + return True, None + + def _parse_wsl_list(text: str) -> list[dict[str, str]]: entries: list[dict[str, str]] = [] for line in text.splitlines(): @@ -219,9 +286,13 @@ def _parse_wsl_list(text: str) -> list[dict[str, str]]: if stripped.startswith("*"): stripped = stripped[1:].strip() parts = stripped.split() - if len(parts) < 3: + if len(parts) >= 3: + entries.append({"name": parts[0], "state": parts[1], "version": parts[2]}) continue - entries.append({"name": parts[0], "state": parts[1], "version": parts[2]}) + # Older WSL builds may only return distro names with `wsl --list`. + # Keep them with a synthetic state so downstream logic can still detect existence. + if len(parts) == 1 and parts[0].lower() not in {"windows", "subsystem", "linux"}: + entries.append({"name": parts[0], "state": "Unknown", "version": ""}) return entries @@ -229,17 +300,27 @@ async def _wsl_list() -> list[dict[str, str]]: wsl_exe = _wsl_cmd() if not wsl_exe: return [] - proc = await asyncio.create_subprocess_exec( - wsl_exe, - "--list", - "--verbose", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + # Newer WSL supports `--list --verbose`, while older builds support `-l -v` + # or only plain `--list`. Try all variants for best compatibility. + variants = ( + ("--list", "--verbose"), + ("-l", "-v"), + ("--list",), ) - out_b, _ = await proc.communicate() - if proc.returncode != 0: - return [] - return _parse_wsl_list(_decode_wsl_output(out_b or b"")) + for args in variants: + proc = await asyncio.create_subprocess_exec( + wsl_exe, + *args, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + out_b, _err_b = await proc.communicate() + if proc.returncode != 0: + continue + parsed = _parse_wsl_list(_decode_wsl_output(out_b or b"")) + if parsed: + return parsed + return [] async def _wsl_instance_status() -> str | None: @@ -250,6 +331,46 @@ async def _wsl_instance_status() -> str | None: return None +def _wsl_prebuilt_tar_path() -> Path | None: + """Return bundled prebuilt WSL rootfs tar if present.""" + prebuilt_dir = vm_setup_dir().parent / "wsl" / "prebuilt" + for name in _WSL_PREBUILT_CANDIDATES: + candidate = prebuilt_dir / name + if candidate.is_file(): + return candidate + return None + + +async def _wsl_probe_start() -> tuple[bool, str]: + """Best-effort probe that distro can actually start. + + This catches cases where `wsl -l -v` still lists the distro (Stopped), + but its backing VHDX path is missing/corrupted. + """ + wsl_exe = _wsl_cmd() + if not wsl_exe: + return False, "wsl.exe not found" + proc = await asyncio.create_subprocess_exec( + wsl_exe, + "-d", + _WSL_INSTANCE, + "--", + "echo", + "ok", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + try: + stdout_b, stderr_b = await asyncio.wait_for(proc.communicate(), timeout=20) + except asyncio.TimeoutError: + proc.kill() + await proc.wait() + return False, "WSL start probe timed out" + if (proc.returncode or 0) == 0: + return True, "" + return False, _combine_wsl_output(stdout_b, stderr_b) + + # ``wsl -l -v`` uses the Windows display language for the STATE column. # Cowork only needs the ``openagent`` distro to exist; WSL starts it on demand. _WSL_COWORK_READY_STATES = frozenset( @@ -305,7 +426,49 @@ def _pick_wsl_source_distro(entries: list[dict[str, str]]) -> str | None: def _wsl_status() -> dict[str, object]: wsl = _wsl_cmd() - return {"installed": bool(wsl), "path": wsl, "managed": False} + if not wsl: + return {"installed": False, "path": None, "managed": False} + + ready, reason = _probe_wsl2_readiness() + if not ready: + return { + "installed": False, + "path": wsl, + "managed": False, + "reason": reason or "WSL runtime is not available", + } + + # `wsl.exe` may exist even when WSL optional components are not enabled. + # Probe command success instead of relying on binary presence. + probe_variants = ( + ("--status",), + ("--list", "--verbose"), + ("-l", "-v"), + ("--list",), + ) + last_err = "" + for args in probe_variants: + try: + proc = _sp.run( + [wsl, *args], + stdout=_sp.PIPE, + stderr=_sp.PIPE, + timeout=8, + ) + except Exception as exc: # pragma: no cover - defensive + last_err = str(exc) + continue + + if proc.returncode == 0: + return {"installed": True, "path": wsl, "managed": False} + last_err = _combine_wsl_output(proc.stdout, proc.stderr).strip() or last_err + + return { + "installed": False, + "path": wsl, + "managed": False, + "reason": last_err or "WSL runtime is not available", + } def _win_path_to_wsl(path: Path | str) -> str: @@ -492,6 +655,7 @@ async def get_vm_status() -> dict[str, object]: vm_ready = False instance_status: str | None = None + instance_error: str | None = None if result.get("installed"): try: if result.get("backend") == "lima": @@ -500,10 +664,17 @@ async def get_vm_status() -> dict[str, object]: elif result.get("backend") == "wsl": instance_status = await _wsl_instance_status() vm_ready = _wsl_distro_ready_for_cowork(instance_status) + if vm_ready: + ok, err = await _wsl_probe_start() + if not ok: + vm_ready = False + instance_error = err or "WSL distro exists but failed to start" except Exception: pass result["instance_status"] = instance_status + if instance_error: + result["instance_error"] = instance_error result["vm_ready"] = vm_ready return result @@ -772,6 +943,13 @@ async def _run_wsl(self) -> None: self._error = "WSL missing" return + ready, reason = _probe_wsl2_readiness() + if not ready: + self._emit("error", {"message": reason or "WSL2 runtime is not ready"}) + self._status = "error" + self._error = "WSL2 not ready" + return + status = await _wsl_instance_status() if _wsl_state_equals(status, _WSL_RUNNING_STATES): self._emit("done", {"message": "WSL distro is already running"}) @@ -786,7 +964,7 @@ async def _run_wsl(self) -> None: stderr=asyncio.subprocess.PIPE, ) self._process = proc - _, stderr_b = await self._communicate_with_heartbeat( + stdout_b, stderr_b = await self._communicate_with_heartbeat( proc, step="starting", message="Starting existing WSL distro...", @@ -794,14 +972,87 @@ async def _run_wsl(self) -> None: if proc.returncode == 0: self._emit("done", {"message": "WSL distro started successfully"}) self._status = "done" + return else: - err = _decode_wsl_output(stderr_b or b"").strip() - self._emit("error", {"message": err or f"WSL start failed (exit {proc.returncode})"}) + err = _combine_wsl_output(stdout_b, stderr_b) + if _looks_like_missing_wsl_disk(err): + self._emit("progress", {"step": "creating", "message": "Detected broken WSL distro disk. Recreating OpenAgent distro..."}) + proc_unreg = await asyncio.create_subprocess_exec( + wsl_exe, "--unregister", _WSL_INSTANCE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + self._process = proc_unreg + u_out_b, u_err_b = await self._communicate_with_heartbeat( + proc_unreg, + step="creating", + message="Removing broken OpenAgent WSL distro...", + ) + if proc_unreg.returncode != 0: + u_err = _combine_wsl_output(u_out_b, u_err_b) + self._emit("error", {"message": u_err or f"WSL unregister failed (exit {proc_unreg.returncode})"}) + self._status = "error" + self._error = f"exit {proc_unreg.returncode}" + return + # Continue with fresh-create flow below. + else: + self._emit("error", {"message": err or f"WSL start failed (exit {proc.returncode})"}) + self._status = "error" + self._error = f"exit {proc.returncode}" + return + + prebuilt_tar = _wsl_prebuilt_tar_path() + import_dir = data_dir() / "wsl" / _WSL_INSTANCE / "disk" + + # Distro does not exist: prefer bundled prebuilt OpenAgent rootfs. + if prebuilt_tar is not None: + self._emit("progress", {"step": "creating", "message": "Importing bundled OpenAgent VM image..."}) + if import_dir.exists(): + shutil.rmtree(import_dir, ignore_errors=True) + import_dir.mkdir(parents=True, exist_ok=True) + + proc_import = await asyncio.create_subprocess_exec( + wsl_exe, "--import", _WSL_INSTANCE, str(import_dir), str(prebuilt_tar), "--version", "2", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + self._process = proc_import + _, err_b = await self._communicate_with_heartbeat( + proc_import, + step="creating", + message="Importing bundled OpenAgent VM image...", + progress_info=lambda: f"(image ~{(prebuilt_tar.stat().st_size / (1024 * 1024)):.1f} MB)", + ) + if proc_import.returncode != 0: + err = _decode_wsl_output(err_b or b"").strip() + self._emit("error", {"message": err or f"Bundled image import failed (exit {proc_import.returncode})"}) self._status = "error" - self._error = f"exit {proc.returncode}" + self._error = f"exit {proc_import.returncode}" + return + + self._emit("progress", {"step": "starting", "message": "Starting imported OpenAgent WSL distro..."}) + proc_start = await asyncio.create_subprocess_exec( + wsl_exe, "-d", _WSL_INSTANCE, "--", "echo", "ok", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + self._process = proc_start + out_b, err_b = await self._communicate_with_heartbeat( + proc_start, + step="starting", + message="Starting imported OpenAgent WSL distro...", + ) + if proc_start.returncode == 0: + self._emit("done", {"message": "WSL distro imported from bundled image and started successfully"}) + self._status = "done" + else: + err = _combine_wsl_output(out_b, err_b) + self._emit("error", {"message": err or f"WSL start failed (exit {proc_start.returncode})"}) + self._status = "error" + self._error = f"exit {proc_start.returncode}" return - # Distro does not exist: bootstrap from Ubuntu export. + # Fallback: bootstrap from Ubuntu export. self._emit("progress", {"step": "creating", "message": "Preparing source distro (Ubuntu)..."}) entries = await _wsl_list() source_distro = _pick_wsl_source_distro(entries) @@ -841,7 +1092,6 @@ async def _run_wsl(self) -> None: export_root = deps_dir() / "wsl" export_root.mkdir(parents=True, exist_ok=True) export_tar = export_root / f"{source_distro.lower()}-seed.tar" - import_dir = data_dir() / "wsl" / _WSL_INSTANCE / "disk" import_dir.mkdir(parents=True, exist_ok=True) self._emit("progress", {"step": "creating", "message": "Exporting Ubuntu rootfs..."}) @@ -890,7 +1140,7 @@ async def _run_wsl(self) -> None: stderr=asyncio.subprocess.PIPE, ) self._process = proc_start - _, err_b = await self._communicate_with_heartbeat( + out_b, err_b = await self._communicate_with_heartbeat( proc_start, step="starting", message="Starting OpenAgent WSL distro...", @@ -899,7 +1149,7 @@ async def _run_wsl(self) -> None: self._emit("done", {"message": "WSL distro created and started successfully"}) self._status = "done" else: - err = _decode_wsl_output(err_b or b"").strip() + err = _combine_wsl_output(out_b, err_b) self._emit("error", {"message": err or f"WSL start failed (exit {proc_start.returncode})"}) self._status = "error" self._error = f"exit {proc_start.returncode}" @@ -1051,11 +1301,34 @@ async def _run_lima(self, **kwargs: object) -> None: async def _run_wsl(self, **kwargs: object) -> None: force = bool(kwargs.get("force", False)) instance_status = await _wsl_instance_status() - if instance_status != "Running": - self._emit("error", {"message": f"WSL distro is not running (status: {instance_status})"}) + # Keep cowork behavior consistent with /api/setup/vm: + # distro may be Stopped but still ready; start it on-demand. + if not _wsl_distro_ready_for_cowork(instance_status): + self._emit("error", {"message": f"WSL distro is not available (status: {instance_status})"}) self._status = "error" - self._error = "WSL distro not running" + self._error = "WSL distro unavailable" return + if not _wsl_state_equals(instance_status, _WSL_RUNNING_STATES): + self._emit("progress", {"step": "starting", "message": "Starting WSL distro for provisioning..."}) + wsl_exe = _wsl_cmd() + if not wsl_exe: + self._emit("error", {"message": "wsl.exe not found - cannot provision"}) + self._status = "error" + self._error = "WSL missing" + return + proc_start = await asyncio.create_subprocess_exec( + wsl_exe, "-d", _WSL_INSTANCE, "--", "echo", "ok", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + self._process = proc_start + stdout_b, stderr_b = await proc_start.communicate() + if proc_start.returncode != 0: + err = _combine_wsl_output(stdout_b, stderr_b) + self._emit("error", {"message": err or f"WSL start failed (exit {proc_start.returncode})"}) + self._status = "error" + self._error = f"exit {proc_start.returncode}" + return self._emit("progress", {"step": "copying", "message": "Preparing setup files in WSL..."}) setup_dir = vm_setup_dir() @@ -1066,9 +1339,11 @@ async def _run_wsl(self, **kwargs: object) -> None: return setup_wsl = _win_path_to_wsl(setup_dir) + setup_wsl_quoted = shlex.quote(setup_wsl) + setup_vm_dir_quoted = shlex.quote(_SETUP_VM_DIR) rc, _, err = await _wsl_shell( - f"sudo rm -rf {_SETUP_VM_DIR} && sudo mkdir -p {_SETUP_VM_DIR} && " - f"sudo cp -r {setup_wsl}/. {_SETUP_VM_DIR}/", + f"sudo rm -rf {setup_vm_dir_quoted} && sudo mkdir -p {setup_vm_dir_quoted} && " + f"sudo cp -r {setup_wsl_quoted}/. {setup_vm_dir_quoted}/", timeout=60, ) if rc != 0: diff --git a/libs/openagent_demo/electron/main.js b/libs/openagent_demo/electron/main.js index 7591777d..2addbd09 100644 --- a/libs/openagent_demo/electron/main.js +++ b/libs/openagent_demo/electron/main.js @@ -195,12 +195,78 @@ function killBackend() { } } +function runCommand(cmd, args) { + return new Promise((resolve) => { + const p = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] }); + let stdout = ""; + let stderr = ""; + p.stdout?.on("data", (d) => { stdout += d.toString(); }); + p.stderr?.on("data", (d) => { stderr += d.toString(); }); + p.on("error", (err) => { + resolve({ code: 1, stdout, stderr: `${stderr}\n${err.message}`.trim() }); + }); + p.on("close", (code) => { + resolve({ code: code ?? 1, stdout, stderr }); + }); + }); +} + // ── IPC ────────────────────────────────────────────────────────────────────── ipcMain.on("get-backend-port", (event) => { event.returnValue = backendPort; }); +ipcMain.handle("install-wsl-runtime", async () => { + if (process.platform !== "win32") { + return { ok: false, message: "This action is only available on Windows." }; + } + + // Launch WSL installation with UAC elevation so non-technical users can + // complete prerequisites in-app with one click. + const psScript = ` +$ErrorActionPreference = 'Stop' +$proc = Start-Process -FilePath "wsl.exe" -ArgumentList "--install","--no-distribution" -Verb RunAs -Wait -PassThru +if ($null -eq $proc) { exit 1 } +exit $proc.ExitCode +`.trim(); + + const res = await runCommand("powershell.exe", [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + psScript, + ]); + + // WSL optional features often need a reboot before WSL2 import/start works. + // We gate follow-up VM instance setup on reboot to avoid first-run import failures. + const success = res.code === 0 || res.code === 3010; + const rebootRequired = success; + if (success) { + return { + ok: true, + rebootRequired, + exitCode: res.code, + message: "Runtime installation completed. Please restart Windows before continuing VM setup.", + stdout: res.stdout, + stderr: res.stderr, + }; + } + + const combined = `${res.stderr || ""}\n${res.stdout || ""}`.trim(); + const cancelled = /canceled|cancelled|拒绝|已取消|denied/i.test(combined); + if (cancelled) { + return { ok: false, exitCode: res.code, message: "Installation was cancelled." }; + } + + return { + ok: false, + exitCode: res.code, + message: combined || `Runtime installation failed (exit ${res.code}).`, + }; +}); + // ── Window ─────────────────────────────────────────────────────────────────── function createWindow() { diff --git a/libs/openagent_demo/electron/preload.js b/libs/openagent_demo/electron/preload.js index 8e1a6251..cb15a785 100644 --- a/libs/openagent_demo/electron/preload.js +++ b/libs/openagent_demo/electron/preload.js @@ -5,4 +5,5 @@ contextBridge.exposeInMainWorld("electronAPI", { backendPort: ipcRenderer.sendSync("get-backend-port"), isElectron: true, platform: process.platform, + installWslRuntime: () => ipcRenderer.invoke("install-wsl-runtime"), }); diff --git a/libs/openagent_demo/electron/scripts/build-all.ps1 b/libs/openagent_demo/electron/scripts/build-all.ps1 index 2fd33da8..032fc487 100644 --- a/libs/openagent_demo/electron/scripts/build-all.ps1 +++ b/libs/openagent_demo/electron/scripts/build-all.ps1 @@ -3,6 +3,7 @@ $ErrorActionPreference = 'Stop' $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $ElectronDir = Resolve-Path "$ScriptDir\.." $Target = if ($args.Count -gt 0) { $args[0] } else { 'win' } +$EmbedWslPrebuilt = ($env:OPENAGENT_EMBED_WSL_PREBUILT -eq "1") Write-Host '=========================================' Write-Host ' OpenAgent Desktop - Build ('$Target')' @@ -24,6 +25,12 @@ Write-Host '' Write-Host '[2/3] Skipping electron dependencies (already installed)...' Set-Location $ElectronDir +if ($Target -eq 'win' -and $EmbedWslPrebuilt) { + Write-Host '' + Write-Host '[2.2/3] Exporting prebuilt WSL VM image for offline-ready package...' + & "$ScriptDir\prepare-wsl-prebuilt.ps1" +} + Write-Host '' Write-Host '[2.5/3] Building backend...' # Call PowerShell version of backend build script @@ -44,4 +51,4 @@ Write-Host ' Build complete! Output in dist/' Write-Host '=========================================' # List build artifacts -Get-ChildItem "$ElectronDir\dist\*.exe", "$ElectronDir\dist\*.blockmap" | Format-Table -AutoSize \ No newline at end of file +Get-ChildItem "$ElectronDir\dist\*.exe", "$ElectronDir\dist\*.blockmap" | Format-Table -AutoSize diff --git a/libs/openagent_demo/electron/scripts/build-backend.ps1 b/libs/openagent_demo/electron/scripts/build-backend.ps1 index c4e64c03..d86f3a56 100644 --- a/libs/openagent_demo/electron/scripts/build-backend.ps1 +++ b/libs/openagent_demo/electron/scripts/build-backend.ps1 @@ -6,34 +6,46 @@ $BackendDir = Resolve-Path "$ElectronDir\..\backend" Write-Host "==> Installing PyInstaller..." Set-Location $BackendDir -uv pip install pyinstaller +$pyinstallerArgs = @( + "--name", "openagent_api_server", + "--onedir", + "--noconfirm", + "--hidden-import", "uvicorn.logging", + "--hidden-import", "uvicorn.loops", + "--hidden-import", "uvicorn.loops.auto", + "--hidden-import", "uvicorn.loops.asyncio", + "--hidden-import", "uvicorn.protocols", + "--hidden-import", "uvicorn.protocols.http", + "--hidden-import", "uvicorn.protocols.http.auto", + "--hidden-import", "uvicorn.protocols.http.h11_impl", + "--hidden-import", "uvicorn.protocols.http.httptools_impl", + "--hidden-import", "uvicorn.protocols.websockets", + "--hidden-import", "uvicorn.protocols.websockets.auto", + "--hidden-import", "uvicorn.protocols.websockets.wsproto_impl", + "--hidden-import", "uvicorn.protocols.websockets.websockets_impl", + "--hidden-import", "uvicorn.lifespan", + "--hidden-import", "uvicorn.lifespan.on", + "--hidden-import", "uvicorn.lifespan.off", + "--collect-submodules", "openagent_api", + "--collect-submodules", "openagent", + "--collect-data", "openagent", + "--add-data", "../../openagent/sandbox/vm;sandbox/vm", + "--add-data", "skills;skills", + "openagent_api/server.py" +) -Write-Host "==> Building backend with PyInstaller..." -uv run pyinstaller ` - --name openagent_api_server ` - --onedir ` - --noconfirm ` - --hidden-import uvicorn.logging ` - --hidden-import uvicorn.loops ` - --hidden-import uvicorn.loops.auto ` - --hidden-import uvicorn.loops.asyncio ` - --hidden-import uvicorn.protocols ` - --hidden-import uvicorn.protocols.http ` - --hidden-import uvicorn.protocols.http.auto ` - --hidden-import uvicorn.protocols.http.h11_impl ` - --hidden-import uvicorn.protocols.http.httptools_impl ` - --hidden-import uvicorn.protocols.websockets ` - --hidden-import uvicorn.protocols.websockets.auto ` - --hidden-import uvicorn.protocols.websockets.wsproto_impl ` - --hidden-import uvicorn.protocols.websockets.websockets_impl ` - --hidden-import uvicorn.lifespan ` - --hidden-import uvicorn.lifespan.on ` - --hidden-import uvicorn.lifespan.off ` - --collect-submodules openagent_api ` - --collect-submodules openagent ` - --collect-data openagent ` - --add-data "skills;skills" ` - openagent_api/server.py +if (Get-Command uv -ErrorAction SilentlyContinue) { + uv pip install pyinstaller + Write-Host "==> Building backend with PyInstaller (uv)..." + uv run pyinstaller @pyinstallerArgs +} else { + $venvPython = Join-Path $BackendDir ".venv\Scripts\python.exe" + if (-not (Test-Path $venvPython)) { + throw "uv not found and backend venv python missing: $venvPython" + } + Write-Host "==> uv not found, using backend venv python fallback..." + & $venvPython -m PyInstaller @pyinstallerArgs +} Write-Host "==> Copying dist to electron/backend_dist..." if (Test-Path "$ElectronDir\backend_dist") { diff --git a/libs/openagent_demo/electron/scripts/prepare-wsl-prebuilt.ps1 b/libs/openagent_demo/electron/scripts/prepare-wsl-prebuilt.ps1 new file mode 100644 index 00000000..2f551f94 --- /dev/null +++ b/libs/openagent_demo/electron/scripts/prepare-wsl-prebuilt.ps1 @@ -0,0 +1,37 @@ +$ErrorActionPreference = "Stop" + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$OpenagentRoot = Resolve-Path "$ScriptDir\..\..\.." +$PrebuiltDir = Join-Path $OpenagentRoot "openagent\sandbox\vm\wsl\prebuilt" +$PrebuiltTar = Join-Path $PrebuiltDir "openagent-prebuilt.tar" +$DistroName = "openagent" + +if ($env:OS -ne "Windows_NT") { + Write-Host "Skipping WSL prebuilt export: non-Windows environment." + exit 0 +} + +if (-not (Get-Command wsl -ErrorAction SilentlyContinue)) { + throw "wsl command not found. Install WSL first." +} + +Write-Host "==> Ensuring distro '$DistroName' can start..." +& wsl -d $DistroName -- echo ok | Out-Null +if ($LASTEXITCODE -ne 0) { + throw "WSL distro '$DistroName' is not available/runnable. Please initialize VM Instance first." +} + +New-Item -ItemType Directory -Force -Path $PrebuiltDir | Out-Null +if (Test-Path $PrebuiltTar) { + Remove-Item -Force $PrebuiltTar +} + +Write-Host "==> Exporting '$DistroName' to $PrebuiltTar (this can take several minutes)..." +wsl --export $DistroName $PrebuiltTar + +if (-not (Test-Path $PrebuiltTar)) { + throw "WSL export completed but output tar was not found: $PrebuiltTar" +} + +$sizeMb = [math]::Round(((Get-Item $PrebuiltTar).Length / 1MB), 1) +Write-Host "==> WSL prebuilt image ready: $PrebuiltTar (${sizeMb} MB)" diff --git a/libs/openagent_demo/frontend/src/components/SettingsModal.tsx b/libs/openagent_demo/frontend/src/components/SettingsModal.tsx index 31e4fb22..ec6e9d63 100644 --- a/libs/openagent_demo/frontend/src/components/SettingsModal.tsx +++ b/libs/openagent_demo/frontend/src/components/SettingsModal.tsx @@ -1739,6 +1739,7 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { const allDone = vmUsable && phase3 === "done"; const anyRunning = phase1 === "running" || phase2 === "running" || phase3 === "running"; const coreError = phase1 === "error" || phase2 === "error"; + const phase1NeedsRestart = /restart windows|重启.*windows|重启.*电脑|reboot/i.test(phase1Error || ""); return (
@@ -1837,10 +1838,20 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { {phase1 === "done" && Installed} {phase1 === "running" && phase1Msg && {phase1Msg}} {phase1 === "pending" && ( - + )} {phase1 === "error" && ( - + phase1NeedsRestart ? ( + + ) : ( + + ) )}
{phase1 === "error" && phase1Error && ( diff --git a/libs/openagent_demo/frontend/src/electron.d.ts b/libs/openagent_demo/frontend/src/electron.d.ts index 2b7220e4..61b88d74 100644 --- a/libs/openagent_demo/frontend/src/electron.d.ts +++ b/libs/openagent_demo/frontend/src/electron.d.ts @@ -4,6 +4,16 @@ declare global { interface Window { electronAPI?: { backendPort?: number; + isElectron?: boolean; + platform?: string; + installWslRuntime?: () => Promise<{ + ok: boolean; + rebootRequired?: boolean; + exitCode?: number; + message?: string; + stdout?: string; + stderr?: string; + }>; }; } } diff --git a/libs/openagent_demo/frontend/src/vmSetup.tsx b/libs/openagent_demo/frontend/src/vmSetup.tsx index a91e5860..b4b3c7f9 100644 --- a/libs/openagent_demo/frontend/src/vmSetup.tsx +++ b/libs/openagent_demo/frontend/src/vmSetup.tsx @@ -50,6 +50,7 @@ export interface VMSetupContextValue { provLog: string | null; installLima: () => void; + recheckVmEngine: () => void; buildVMInstance: () => void; startProvision: (force?: boolean) => void; stopProvision: () => void; @@ -107,6 +108,42 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { dispatch({ type: "SHOW_NOTIFICATION", payload: { message, type } }); }; + const doRecheckVmEngine = async () => { + setPhase1("checking"); + setPhase1Msg("Re-checking runtime status..."); + setPhase1Error(""); + try { + const vs = await getVMStatus(); + setVmStatus(vs); + dispatch({ type: "SET_VM_STATUS", payload: vs }); + if (!vs.supported) { + setPhase1("error"); + setPhase1Msg(""); + setPhase1Error("Not supported on this platform"); + return; + } + if (vs.installed) { + setPhase1("done"); + setPhase1Msg(""); + setPhase1Error(""); + setPhase2("pending"); + attachBuild(); + } else if (vs.reason) { + setPhase1("error"); + setPhase1Msg(""); + setPhase1Error(vs.reason); + } else { + setPhase1("pending"); + setPhase1Msg(""); + setPhase1Error(""); + } + } catch { + setPhase1("error"); + setPhase1Msg(""); + setPhase1Error("Could not check VM status"); + } + }; + // ── Phase 3: Provision ── const attachProvision = (force = false) => { @@ -185,6 +222,63 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { // ── Phase 1: Install Lima ── const doInstallLima = () => { + const inferredBackend = navigator.platform.toUpperCase().includes("WIN") ? "wsl" : "lima"; + const currentBackend = vmStatus?.backend ?? inferredBackend; + const canAssistWslInstall = + currentBackend === "wsl" && + typeof window !== "undefined" && + typeof window.electronAPI?.installWslRuntime === "function"; + + if (canAssistWslInstall) { + setPhase1("running"); + setPhase1Error(""); + setPhase1Msg("Installing required Windows runtime (admin permission needed)..."); + + window.electronAPI! + .installWslRuntime!() + .then(async (res) => { + if (!res.ok) { + const msg = res.message || "Runtime installation failed."; + setPhase1("error"); + setPhase1Error(msg); + notify(msg, "error"); + return; + } + + if (res.rebootRequired) { + const msg = res.message || "Runtime installed. Please restart Windows before continuing VM setup."; + setPhase1("error"); + setPhase1Msg(""); + setPhase1Error(msg); + notify(msg, "info"); + return; + } + + const vs = await getVMStatus(); + setVmStatus(vs); + dispatch({ type: "SET_VM_STATUS", payload: vs }); + + if (vs.installed) { + setPhase1("done"); + setPhase1Msg(""); + notify("Runtime installed", "success"); + setPhase2("pending"); + attachBuild(); + } else { + setPhase1("pending"); + setPhase1Msg("Runtime install command completed. Click Retry if needed."); + notify("Runtime install command completed", "info"); + } + }) + .catch((err: unknown) => { + const msg = err instanceof Error ? err.message : String(err); + setPhase1("error"); + setPhase1Error(msg); + notify(`Runtime installation failed: ${msg}`, "error"); + }); + return; + } + setPhase1("running"); setPhase1Error(""); setPhase1Msg("Starting installation..."); @@ -266,6 +360,9 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { if (vs.installed) { setPhase1("done"); limaInstalled = true; + } else if (vs.reason) { + setPhase1("error"); + setPhase1Error(vs.reason); } else { setPhase1("pending"); } @@ -351,6 +448,7 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { phase3, phase3Error, provSteps, provStepStatus, provStepMsg, provLog, installLima: doInstallLima, + recheckVmEngine: doRecheckVmEngine, buildVMInstance: attachBuild, startProvision: doStartProvision, stopProvision: doStopProvision, From 47501c9e108807ce1a2a9cd2be93dde3e7074750 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Thu, 26 Mar 2026 19:02:47 +0800 Subject: [PATCH 08/34] fix(vm-win): harden WSL startup/user creation and add regressions --- .../openagent/computer/local/_wsl.py | 30 ++++++++---- .../openagent/computer/local/vm_win.py | 20 +++++++- .../sandbox/vm/setup/steps/06_playwright.sh | 13 ++++- .../unit_tests/computer/test_local_vm_win.py | 30 ++++++++++-- .../tests/unit_tests/computer/test_wsl.py | 47 +++++++++++++++++++ 5 files changed, 123 insertions(+), 17 deletions(-) diff --git a/libs/openagent/openagent/computer/local/_wsl.py b/libs/openagent/openagent/computer/local/_wsl.py index 5708c81d..483886a1 100644 --- a/libs/openagent/openagent/computer/local/_wsl.py +++ b/libs/openagent/openagent/computer/local/_wsl.py @@ -312,15 +312,27 @@ async def start(self) -> None: if current != "Running": # Trigger start by running a trivial command. - await self._run_wsl( - self._wsl_exe, - "-d", - self._instance, - "--", - "echo", - "ok", - timeout=120, - ) + # Some Windows hosts occasionally return a transient -1/4294967295 + # from wsl.exe during startup even though a subsequent attempt works. + for attempt in range(2): + try: + await self._run_wsl( + self._wsl_exe, + "-d", + self._instance, + "--", + "echo", + "ok", + timeout=120, + ) + break + except WslError as exc: + text = str(exc).lower() + transient = "exit 4294967295" in text or "exit -1" in text + if transient and attempt == 0: + await asyncio.sleep(0.5) + continue + raise await self._apply_bind_mounts() diff --git a/libs/openagent/openagent/computer/local/vm_win.py b/libs/openagent/openagent/computer/local/vm_win.py index dd15b5e2..cc0485fa 100644 --- a/libs/openagent/openagent/computer/local/vm_win.py +++ b/libs/openagent/openagent/computer/local/vm_win.py @@ -602,14 +602,30 @@ async def _create_user(self, name: str) -> None: qname = shlex.quote(name) home = f"/sessions/{name}" qhome = shlex.quote(home) - result = await self._vm.shell(f"sudo useradd -m -d {qhome} -s /bin/bash --no-log-init -K SUB_UID_COUNT=0 -K SUB_GID_COUNT=0 {qname}") + sudo_probe = await self._vm.shell("command -v sudo >/dev/null 2>&1") + sudo_prefix = "sudo " if sudo_probe.exit_code == 0 else "" + + create_cmd = ( + f"{sudo_prefix}useradd -m -d {qhome} -s /bin/bash " + f"--no-log-init -K SUB_UID_COUNT=0 -K SUB_GID_COUNT=0 {qname}" + ) + result = await self._vm.shell(create_cmd) + if result.exit_code != 0: + # Some Ubuntu/WSL images reject useradd with SUB_UID/GID_COUNT=0. + # Retry without those overrides for compatibility. We retry + # regardless of locale-specific stderr text. + fallback_cmd = ( + f"{sudo_prefix}useradd -m -d {qhome} -s /bin/bash " + f"--no-log-init {qname}" + ) + result = await self._vm.shell(fallback_cmd) if result.exit_code != 0: msg = f"Failed to create session user '{name}': {result.stderr}" raise VMError(msg) all_dirs = " ".join(shlex.quote(f"{home}/{d}") for d in SESSION_DIRS) writable = f"{shlex.quote(f'{home}/{SESSION_TMP_DIR}')} {shlex.quote(f'{home}/{SESSION_OUTPUTS_DIR}')}" - result = await self._vm.shell(f"sudo mkdir -p {all_dirs} && sudo chown {qname} {writable}") + result = await self._vm.shell(f"{sudo_prefix}mkdir -p {all_dirs} && {sudo_prefix}chown {qname} {writable}") if result.exit_code != 0: msg = f"Failed to create session directories for '{name}': {result.stderr}" raise VMError(msg) diff --git a/libs/openagent/sandbox/vm/setup/steps/06_playwright.sh b/libs/openagent/sandbox/vm/setup/steps/06_playwright.sh index be4d13ac..c1b344b8 100755 --- a/libs/openagent/sandbox/vm/setup/steps/06_playwright.sh +++ b/libs/openagent/sandbox/vm/setup/steps/06_playwright.sh @@ -31,7 +31,18 @@ fi emit 06_playwright progress "Downloading Chromium binary" mirror_host="${OPENAGENT_PLAYWRIGHT_DOWNLOAD_HOST:-}" browser_ok=0 + +# If bundled browsers are already present in the VM image, skip network download. +if find /opt/pw-browsers -type f \( -name "chrome-headless-shell" -o -name "chrome" \) 2>/dev/null | grep -q .; then + emit 06_playwright progress "Bundled Playwright browser detected under /opt/pw-browsers, skipping download" + browser_ok=1 +fi + for ((attempt = 1; attempt <= max_attempts; attempt++)); do + if [[ $browser_ok -eq 1 ]]; then + break + fi + if [[ "${OPENAGENT_USE_CN_MIRRORS:-0}" == "1" && -n "$mirror_host" ]]; then emit 06_playwright progress "browser install attempt $attempt/$max_attempts (mirror)" if PLAYWRIGHT_DOWNLOAD_HOST="$mirror_host" \ @@ -45,7 +56,7 @@ for ((attempt = 1; attempt <= max_attempts; attempt++)); do emit 06_playwright progress "browser install attempt $attempt/$max_attempts" fi - if PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers npx playwright install chromium; then + if env -u PLAYWRIGHT_DOWNLOAD_HOST PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers npx playwright install chromium; then browser_ok=1 break fi diff --git a/libs/openagent/tests/unit_tests/computer/test_local_vm_win.py b/libs/openagent/tests/unit_tests/computer/test_local_vm_win.py index 2543c3c4..450cb050 100644 --- a/libs/openagent/tests/unit_tests/computer/test_local_vm_win.py +++ b/libs/openagent/tests/unit_tests/computer/test_local_vm_win.py @@ -293,7 +293,7 @@ class TestSession: async def test_creates_new_session(self) -> None: vm = _mock_vm() mgr = _make_manager(vm) - vm.shell = AsyncMock(side_effect=[_fail(), _ok(), _ok()]) + vm.shell = AsyncMock(side_effect=[_fail(), _ok(), _ok(), _ok()]) computer = await mgr.computer() @@ -327,7 +327,7 @@ async def test_rejects_resume_with_mounts(self) -> None: async def test_auto_starts_if_needed(self) -> None: vm = _mock_vm(status="Stopped") mgr = _make_manager(vm) - vm.shell = AsyncMock(side_effect=[_fail(), _ok(), _ok()]) + vm.shell = AsyncMock(side_effect=[_fail(), _ok(), _ok(), _ok()]) await mgr.computer() @@ -461,11 +461,11 @@ class TestCreateUser: async def test_creates_session_dirs(self) -> None: vm = _mock_vm() mgr = _make_manager(vm) - vm.shell = AsyncMock(side_effect=[_ok(), _ok()]) + vm.shell = AsyncMock(side_effect=[_ok(), _ok(), _ok()]) await mgr._create_user("test-user") - setup_call = vm.shell.call_args_list[1].args[0] + setup_call = vm.shell.call_args_list[2].args[0] home = "/sessions/test-user" for d in SESSION_DIRS: assert f"{home}/{d}" in setup_call @@ -479,6 +479,26 @@ async def test_useradd_failure_raises(self) -> None: with pytest.raises(VMError, match="Failed to create session user"): await mgr._create_user("test-user") + async def test_useradd_retries_without_k_flags(self) -> None: + vm = _mock_vm() + mgr = _make_manager(vm) + vm.shell = AsyncMock( + side_effect=[ + _ok(), # sudo probe + _fail(stderr="localized subordinate uid failure"), # useradd with -K... + _ok(), # fallback useradd without -K... + _ok(), # mkdir/chown + ] + ) + + await mgr._create_user("test-user") + + first_useradd = vm.shell.call_args_list[1].args[0] + fallback_useradd = vm.shell.call_args_list[2].args[0] + assert "-K SUB_UID_COUNT=0 -K SUB_GID_COUNT=0" in first_useradd + assert "-K SUB_UID_COUNT=0 -K SUB_GID_COUNT=0" not in fallback_useradd + assert "--no-log-init" in fallback_useradd + class TestNameGeneration: """Tests for _generate_unique_name.""" @@ -510,7 +530,7 @@ async def test_mount_failure_cleans_up_user(self, tmp_path: Any) -> None: mgr = _make_manager(vm) d = tmp_path / "nope" - vm.shell = AsyncMock(side_effect=[_fail(), _ok(), _ok(), _ok()]) + vm.shell = AsyncMock(side_effect=[_fail(), _ok(), _ok(), _ok(), _ok()]) with pytest.raises(ValueError, match="does not exist"): await mgr.computer(mounts=[Mount(source=str(d), target="proj")]) diff --git a/libs/openagent/tests/unit_tests/computer/test_wsl.py b/libs/openagent/tests/unit_tests/computer/test_wsl.py index 1da40b91..e039354d 100644 --- a/libs/openagent/tests/unit_tests/computer/test_wsl.py +++ b/libs/openagent/tests/unit_tests/computer/test_wsl.py @@ -372,6 +372,53 @@ def test_accepts_win32_with_wsl(self) -> None: assert vm.instance == "myvm" +# =========================================================================== +# WslVM start retry behavior +# =========================================================================== + + +class TestWslVMStart: + """Tests for WslVM.start().""" + + def _make_vm(self) -> WslVM: + with ( + patch("openagent.computer.local._wsl._PLATFORM", "win32"), + patch("shutil.which", return_value="C:\\Windows\\System32\\wsl.exe"), + ): + return WslVM(instance="test") + + async def test_start_retries_once_on_transient_minus_one_exit(self) -> None: + vm = self._make_vm() + + with ( + patch.object(vm, "status", new_callable=AsyncMock, return_value="Stopped"), + patch.object(vm, "_run_wsl", new_callable=AsyncMock) as mock_run_wsl, + patch.object(vm, "_apply_bind_mounts", new_callable=AsyncMock) as mock_apply, + ): + mock_run_wsl.side_effect = [ + WslError("wsl.exe failed (exit 4294967295): "), + "ok", + ] + + await vm.start() + + assert mock_run_wsl.await_count == 2 + mock_apply.assert_awaited_once() + + async def test_start_does_not_retry_on_non_transient_failure(self) -> None: + vm = self._make_vm() + + with ( + patch.object(vm, "status", new_callable=AsyncMock, return_value="Stopped"), + patch.object(vm, "_run_wsl", new_callable=AsyncMock, side_effect=WslError("wsl.exe failed (exit 1): boom")), + patch.object(vm, "_apply_bind_mounts", new_callable=AsyncMock) as mock_apply, + pytest.raises(WslError, match="exit 1"), + ): + await vm.start() + + mock_apply.assert_not_awaited() + + # =========================================================================== # Status output parsing # =========================================================================== From efeaeadcd4ec8ed0d801839ce4f798af71ca2eb4 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Thu, 26 Mar 2026 19:02:56 +0800 Subject: [PATCH 09/34] fix(backend): preserve actionable cowork errors and improve WSL setup execution --- .../backend/openagent_api/agent_manager.py | 15 +++++--- .../backend/openagent_api/main.py | 11 ++++++ .../backend/openagent_api/routes/setup.py | 36 ++++++++++--------- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/libs/openagent_demo/backend/openagent_api/agent_manager.py b/libs/openagent_demo/backend/openagent_api/agent_manager.py index 87b35cc6..f0a24d96 100644 --- a/libs/openagent_demo/backend/openagent_api/agent_manager.py +++ b/libs/openagent_demo/backend/openagent_api/agent_manager.py @@ -196,14 +196,21 @@ async def _ensure_computer( "Please install and configure it in Settings \u2192 Sandbox." ) from None except Exception as exc: - # Convert VM infrastructure errors to user-friendly messages - msg = str(exc).lower() - if "not found" in msg or "does not exist" in msg or "not running" in msg: + # Preserve actionable details instead of collapsing all errors + # into "VM is not running", which can hide the real cause. + detail = str(exc).strip() or exc.__class__.__name__ + low = detail.lower() + if "does not exist" in low and "wsl distro" in low: + raise RuntimeError( + "Cowork mode requires VM setup. " + "Please install and configure it in Settings \u2192 Sandbox." + ) from None + if "not running" in low and "session" not in low: raise RuntimeError( "VM is not running. " "Please set it up in Settings \u2192 Sandbox." ) from None - raise + raise RuntimeError(f"Cowork session setup failed: {detail}") from None actual_name = computer.session_name self._computers[actual_name] = computer diff --git a/libs/openagent_demo/backend/openagent_api/main.py b/libs/openagent_demo/backend/openagent_api/main.py index fb9a366a..d41df59e 100644 --- a/libs/openagent_demo/backend/openagent_api/main.py +++ b/libs/openagent_demo/backend/openagent_api/main.py @@ -16,13 +16,24 @@ from fastapi.middleware.cors import CORSMiddleware from openagent_api.agent_manager import agent_manager +from openagent_api.paths import data_dir from openagent_api.routes import chat, config, conversations, sessions, setup, skills +_LOG_DIR = data_dir() / "logs" +_LOG_DIR.mkdir(parents=True, exist_ok=True) +_LOG_FILE = _LOG_DIR / "backend.log" + logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", + handlers=[ + logging.StreamHandler(), + logging.FileHandler(_LOG_FILE, encoding="utf-8"), + ], + force=True, ) logger = logging.getLogger(__name__) +logger.info("Backend log file: %s", _LOG_FILE) async def _cleanup_expired_sessions() -> None: diff --git a/libs/openagent_demo/backend/openagent_api/routes/setup.py b/libs/openagent_demo/backend/openagent_api/routes/setup.py index 6d754e7a..deb0350e 100644 --- a/libs/openagent_demo/backend/openagent_api/routes/setup.py +++ b/libs/openagent_demo/backend/openagent_api/routes/setup.py @@ -483,18 +483,21 @@ def _win_path_to_wsl(path: Path | str) -> str: return f"/mnt/{drive}{rest}" -async def _wsl_shell(cmd: str, *, timeout: float = 60) -> tuple[int, str, str]: +async def _wsl_shell( + cmd: str, + *, + timeout: float = 60, + user: str | None = None, +) -> tuple[int, str, str]: wsl_exe = _wsl_cmd() if not wsl_exe: return 1, "", "wsl.exe not found" + exec_args: list[str] = [wsl_exe, "-d", _WSL_INSTANCE] + if user: + exec_args.extend(["-u", user]) + exec_args.extend(["--", "bash", "-lc", cmd]) proc = await asyncio.create_subprocess_exec( - wsl_exe, - "-d", - _WSL_INSTANCE, - "--", - "bash", - "-lc", - cmd, + *exec_args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) @@ -506,8 +509,8 @@ async def _wsl_shell(cmd: str, *, timeout: float = 60) -> tuple[int, str, str]: return 1, "", "Timed out" return ( proc.returncode or 0, - (stdout_b or b"").decode("utf-8", errors="replace"), - (stderr_b or b"").decode("utf-8", errors="replace"), + _decode_wsl_output(stdout_b or b""), + _decode_wsl_output(stderr_b or b""), ) @@ -1342,9 +1345,10 @@ async def _run_wsl(self, **kwargs: object) -> None: setup_wsl_quoted = shlex.quote(setup_wsl) setup_vm_dir_quoted = shlex.quote(_SETUP_VM_DIR) rc, _, err = await _wsl_shell( - f"sudo rm -rf {setup_vm_dir_quoted} && sudo mkdir -p {setup_vm_dir_quoted} && " - f"sudo cp -r {setup_wsl_quoted}/. {setup_vm_dir_quoted}/", + f"rm -rf {setup_vm_dir_quoted} && mkdir -p {setup_vm_dir_quoted} && " + f"cp -r {setup_wsl_quoted}/. {setup_vm_dir_quoted}/", timeout=60, + user="root", ) if rc != 0: self._emit("error", {"message": f"Failed to stage setup files in WSL: {err}"}) @@ -1353,7 +1357,7 @@ async def _run_wsl(self, **kwargs: object) -> None: return self._emit("progress", {"step": "starting", "message": "Starting provisioning..."}) - cmd = f"sudo bash {_SETUP_VM_DIR}/setup.sh" + cmd = f"bash {_SETUP_VM_DIR}/setup.sh" if force: cmd += " --force" @@ -1365,7 +1369,7 @@ async def _run_wsl(self, **kwargs: object) -> None: return proc = await asyncio.create_subprocess_exec( - wsl_exe, "-d", _WSL_INSTANCE, "--", "bash", "-lc", cmd, + wsl_exe, "-d", _WSL_INSTANCE, "-u", "root", "--", "bash", "-lc", cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) @@ -1414,7 +1418,7 @@ async def check_markers(self) -> dict[str, object]: """Read VM-side marker files to determine provision state.""" if sys.platform == "win32": instance_status = await _wsl_instance_status() - shell = _wsl_shell + shell = lambda cmd: _wsl_shell(cmd, user="root") else: instance_status = await _lima_instance_status() shell = _lima_shell @@ -1440,7 +1444,7 @@ async def check_markers(self) -> dict[str, object]: async def get_log(self) -> str: """Fetch the latest setup log from the VM.""" - shell = _wsl_shell if sys.platform == "win32" else _lima_shell + shell = (lambda cmd, timeout=15: _wsl_shell(cmd, timeout=timeout, user="root")) if sys.platform == "win32" else _lima_shell rc, stdout, _ = await shell( f"ls -t {_SETUP_LOG_DIR}/setup-*.log 2>/dev/null | head -1 | xargs cat 2>/dev/null | tail -500", timeout=15, From cf2b5450763a84eb7436f92095426ed33cbbc596 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Thu, 26 Mar 2026 19:03:03 +0800 Subject: [PATCH 10/34] feat(electron): add WSL prerequisite precheck IPC and typed bridge --- libs/openagent_demo/electron/main.js | 151 +++++++++++++++++- libs/openagent_demo/electron/preload.js | 1 + .../openagent_demo/frontend/src/electron.d.ts | 14 ++ 3 files changed, 162 insertions(+), 4 deletions(-) diff --git a/libs/openagent_demo/electron/main.js b/libs/openagent_demo/electron/main.js index 2addbd09..3b06f145 100644 --- a/libs/openagent_demo/electron/main.js +++ b/libs/openagent_demo/electron/main.js @@ -211,24 +211,155 @@ function runCommand(cmd, args) { }); } +function tryParseJsonObject(text) { + const raw = (text || "").trim(); + if (!raw) return null; + const first = raw.indexOf("{"); + const last = raw.lastIndexOf("}"); + if (first < 0 || last <= first) return null; + try { + return JSON.parse(raw.slice(first, last + 1)); + } catch { + return null; + } +} + +async function checkWslPrerequisitesInternal() { + if (process.platform !== "win32") { + return { + ok: false, + code: "UNSUPPORTED_PLATFORM", + message: "This check is only available on Windows.", + }; + } + + const psScript = ` +$ErrorActionPreference = 'SilentlyContinue' +$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1 VMMonitorModeExtensions,SecondLevelAddressTranslationExtensions,VirtualizationFirmwareEnabled +$cs = Get-CimInstance Win32_ComputerSystem | Select-Object -First 1 HypervisorPresent +$vmp = (Get-WindowsOptionalFeature -Online -FeatureName 'VirtualMachinePlatform').State +$wsl = (Get-WindowsOptionalFeature -Online -FeatureName 'Microsoft-Windows-Subsystem-Linux').State +$hypervisorAuto = $false +try { + $line = (bcdedit /enum '{current}' | Select-String -Pattern 'hypervisorlaunchtype' -SimpleMatch | Select-Object -First 1).ToString() + if ($line -match 'Auto') { $hypervisorAuto = $true } +} catch {} +$rebootPending = (Test-Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending') -or (Test-Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\RebootRequired') +$vmMonitorRaw = $cpu.VMMonitorModeExtensions +$slatRaw = $cpu.SecondLevelAddressTranslationExtensions +$virtFirmwareRaw = $cpu.VirtualizationFirmwareEnabled +$vmMonitorKnown = $null -ne $vmMonitorRaw +$slatKnown = $null -ne $slatRaw +$virtFirmwareKnown = $null -ne $virtFirmwareRaw +$vmMonitor = [bool]$vmMonitorRaw +$slat = [bool]$slatRaw +$virtFirmware = [bool]$virtFirmwareRaw +$hypervisorPresent = [bool]$cs.HypervisorPresent +# Hypervisor already running => virtualization requirements are effectively met. +$virtualizationReady = $hypervisorPresent -or ($vmMonitor -and $slat -and $virtFirmware) +$vmpEnabled = ($vmp -eq 'Enabled') +$wslFeatureEnabled = ($wsl -eq 'Enabled') +$ok = $virtualizationReady -and $vmpEnabled -and $wslFeatureEnabled -and $hypervisorAuto +$code = 'OK' +$message = 'WSL prerequisites are ready.' +if ((-not $hypervisorPresent) -and (($vmMonitorKnown -and -not $vmMonitor) -or ($slatKnown -and -not $slat))) { + $ok = $false + $code = 'CPU_NOT_SUPPORTED' + $message = 'Your CPU does not meet WSL2 virtualization requirements (VM monitor mode + SLAT).' +} elseif (-not $vmpEnabled -or -not $wslFeatureEnabled) { + $ok = $false + $code = 'WINDOWS_FEATURES_DISABLED' + $message = 'Required Windows features are not enabled yet. Click Retry install to enable them automatically (admin permission), then restart Windows.' +} elseif ((-not $hypervisorPresent) -and $virtFirmwareKnown -and -not $virtFirmware) { + $ok = $false + $code = 'BIOS_VIRT_DISABLED' + $message = "Hardware virtualization is disabled in BIOS. Please enable Intel VT-x/AMD-V (SVM), save BIOS, then reboot Windows." +} elseif (-not $hypervisorAuto) { + $ok = $false + $code = 'HYPERVISOR_DISABLED' + $message = "Hypervisor launch is disabled. Click Retry install to fix it automatically, then restart Windows." +} +[pscustomobject]@{ + ok = $ok + code = $code + message = $message + virtualizationReady = $virtualizationReady + vmMonitorModeExtensions = $vmMonitor + slat = $slat + virtualizationFirmwareEnabled = $virtFirmware + hypervisorPresent = $hypervisorPresent + virtualMachinePlatformEnabled = $vmpEnabled + wslFeatureEnabled = $wslFeatureEnabled + hypervisorLaunchAuto = $hypervisorAuto + rebootPending = $rebootPending +} | ConvertTo-Json -Compress +`.trim(); + + const res = await runCommand("powershell.exe", [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + psScript, + ]); + + const parsed = tryParseJsonObject(`${res.stdout || ""}\n${res.stderr || ""}`); + if (parsed && typeof parsed === "object") { + return parsed; + } + return { + ok: false, + code: "CHECK_FAILED", + message: "Failed to check WSL prerequisites.", + }; +} + // ── IPC ────────────────────────────────────────────────────────────────────── ipcMain.on("get-backend-port", (event) => { event.returnValue = backendPort; }); +ipcMain.handle("check-wsl-prerequisites", async () => { + return checkWslPrerequisitesInternal(); +}); + ipcMain.handle("install-wsl-runtime", async () => { if (process.platform !== "win32") { return { ok: false, message: "This action is only available on Windows." }; } + const precheck = await checkWslPrerequisitesInternal(); + if (precheck?.code === "BIOS_VIRT_DISABLED" || precheck?.code === "CPU_NOT_SUPPORTED") { + return { + ok: false, + code: precheck.code, + message: precheck.message, + precheck, + }; + } + // Launch WSL installation with UAC elevation so non-technical users can // complete prerequisites in-app with one click. const psScript = ` $ErrorActionPreference = 'Stop' -$proc = Start-Process -FilePath "wsl.exe" -ArgumentList "--install","--no-distribution" -Verb RunAs -Wait -PassThru -if ($null -eq $proc) { exit 1 } -exit $proc.ExitCode +$wslPath = Join-Path $env:SystemRoot "System32\\wsl.exe" +if (-not (Test-Path $wslPath)) { + $wslPath = Join-Path $env:SystemRoot "Sysnative\\wsl.exe" +} +if (-not (Test-Path $wslPath)) { + throw "wsl.exe not found under %SystemRoot%." +} +try { + $proc = Start-Process -FilePath $wslPath -ArgumentList @("--install","--no-distribution") -Verb RunAs -Wait -PassThru + if ($null -eq $proc) { throw "Start-Process returned null process." } + exit $proc.ExitCode +} catch { + $msg = $_.Exception.Message + if ([string]::IsNullOrWhiteSpace($msg)) { $msg = "Unknown Start-Process failure." } + Write-Output ("INSTALL_ERR:" + $msg) + exit 1 +} `.trim(); const res = await runCommand("powershell.exe", [ @@ -255,15 +386,27 @@ exit $proc.ExitCode } const combined = `${res.stderr || ""}\n${res.stdout || ""}`.trim(); + const installErr = (combined.match(/INSTALL_ERR:(.*)/) || [null, ""])[1]?.trim(); const cancelled = /canceled|cancelled|拒绝|已取消|denied/i.test(combined); if (cancelled) { return { ok: false, exitCode: res.code, message: "Installation was cancelled." }; } + if (precheck?.code === "WINDOWS_FEATURES_DISABLED" || precheck?.code === "HYPERVISOR_DISABLED") { + return { + ok: false, + code: precheck.code, + exitCode: res.code, + message: + "WSL prerequisites are not fully enabled yet. Please allow the admin prompt, restart Windows, and retry VM setup.", + precheck, + }; + } + return { ok: false, exitCode: res.code, - message: combined || `Runtime installation failed (exit ${res.code}).`, + message: installErr || combined || `Runtime installation failed (exit ${res.code}).`, }; }); diff --git a/libs/openagent_demo/electron/preload.js b/libs/openagent_demo/electron/preload.js index cb15a785..279d4ef0 100644 --- a/libs/openagent_demo/electron/preload.js +++ b/libs/openagent_demo/electron/preload.js @@ -5,5 +5,6 @@ contextBridge.exposeInMainWorld("electronAPI", { backendPort: ipcRenderer.sendSync("get-backend-port"), isElectron: true, platform: process.platform, + checkWslPrerequisites: () => ipcRenderer.invoke("check-wsl-prerequisites"), installWslRuntime: () => ipcRenderer.invoke("install-wsl-runtime"), }); diff --git a/libs/openagent_demo/frontend/src/electron.d.ts b/libs/openagent_demo/frontend/src/electron.d.ts index 61b88d74..ed453916 100644 --- a/libs/openagent_demo/frontend/src/electron.d.ts +++ b/libs/openagent_demo/frontend/src/electron.d.ts @@ -6,8 +6,22 @@ declare global { backendPort?: number; isElectron?: boolean; platform?: string; + checkWslPrerequisites?: () => Promise<{ + ok: boolean; + code?: string; + message?: string; + virtualizationReady?: boolean; + vmMonitorModeExtensions?: boolean; + slat?: boolean; + virtualizationFirmwareEnabled?: boolean; + virtualMachinePlatformEnabled?: boolean; + wslFeatureEnabled?: boolean; + hypervisorLaunchAuto?: boolean; + rebootPending?: boolean; + }>; installWslRuntime?: () => Promise<{ ok: boolean; + code?: string; rebootRequired?: boolean; exitCode?: number; message?: string; From 163fda75fb9bda061e83d5b72a6d9bc6410a4f72 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Thu, 26 Mar 2026 19:03:12 +0800 Subject: [PATCH 11/34] feat(frontend): improve VM setup flow and add global reboot-required modal --- libs/openagent_demo/frontend/src/App.css | 108 ++++++++ libs/openagent_demo/frontend/src/App.tsx | 6 + libs/openagent_demo/frontend/src/api.ts | 1 + .../frontend/src/components/ChatArea.tsx | 11 +- .../src/components/OnboardingWizard.tsx | 11 +- .../src/components/RestartRequiredModal.tsx | 48 ++++ .../frontend/src/components/SettingsModal.tsx | 26 +- libs/openagent_demo/frontend/src/store.ts | 27 ++ libs/openagent_demo/frontend/src/vmSetup.tsx | 238 ++++++++++++------ 9 files changed, 401 insertions(+), 75 deletions(-) create mode 100644 libs/openagent_demo/frontend/src/components/RestartRequiredModal.tsx diff --git a/libs/openagent_demo/frontend/src/App.css b/libs/openagent_demo/frontend/src/App.css index da546366..1c30d7d8 100644 --- a/libs/openagent_demo/frontend/src/App.css +++ b/libs/openagent_demo/frontend/src/App.css @@ -5792,6 +5792,114 @@ } } +/* ===== Global Restart Required Modal ===== */ +.restart-required-overlay { + position: fixed; + inset: 0; + z-index: 12000; + background: rgba(0, 0, 0, 0.58); + backdrop-filter: blur(3px); + -webkit-backdrop-filter: blur(3px); + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.restart-required-modal { + width: min(560px, 94vw); + background: var(--bg-primary); + border: 1px solid rgba(248, 113, 113, 0.35); + border-radius: 14px; + padding: 20px 22px; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45); +} + +.restart-required-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; +} + +.restart-required-icon { + color: var(--danger, #ef4444); + flex-shrink: 0; +} + +.restart-required-title { + margin: 0; + font-size: 20px; + line-height: 1.25; + color: var(--text-primary); +} + +.restart-required-text { + margin: 0 0 8px; + font-size: 14px; + line-height: 1.55; + color: var(--text-primary); +} + +.restart-required-text--strong { + color: var(--danger, #ef4444); + font-weight: 600; +} + +.restart-required-detail { + margin: 4px 0 0; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--bg-secondary); + color: var(--text-secondary); + font-size: 12px; + line-height: 1.45; + word-break: break-word; +} + +.restart-required-actions { + margin-top: 16px; + display: flex; + gap: 8px; + justify-content: flex-end; + flex-wrap: wrap; +} + +.restart-required-btn { + display: inline-flex; + align-items: center; + gap: 6px; + border-radius: 10px; + border: 1px solid var(--border); + padding: 8px 12px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 120ms ease, border-color 120ms ease, color 120ms ease; +} + +.restart-required-btn--ghost { + background: transparent; + color: var(--text-primary); +} + +.restart-required-btn--ghost:hover { + background: var(--bg-hover); + border-color: var(--border-hover); +} + +.restart-required-btn--primary { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} + +.restart-required-btn--primary:hover { + background: var(--accent-hover); + border-color: var(--accent-hover); +} + /* ===== VM Setup Floating Indicator ===== */ .vm-floater { diff --git a/libs/openagent_demo/frontend/src/App.tsx b/libs/openagent_demo/frontend/src/App.tsx index 26d9dcf6..6028c720 100644 --- a/libs/openagent_demo/frontend/src/App.tsx +++ b/libs/openagent_demo/frontend/src/App.tsx @@ -10,6 +10,7 @@ import type { Tab as SettingsTab } from "./components/SettingsModal"; import OnboardingWizard from "./components/OnboardingWizard"; import SearchModal from "./components/SearchModal"; import Toast from "./components/Toast"; +import RestartRequiredModal from "./components/RestartRequiredModal"; import VMSetupFloater from "./components/VMSetupFloater"; import { VMSetupProvider } from "./vmSetup"; import type { Attachment, ConversationMode, Message } from "./types"; @@ -425,6 +426,11 @@ function App() { notifications={state.notifications} onDismiss={(id) => dispatch({ type: "DISMISS_NOTIFICATION", payload: id })} /> + openSettings("sandbox")} + /> ); diff --git a/libs/openagent_demo/frontend/src/api.ts b/libs/openagent_demo/frontend/src/api.ts index 204ad387..d58bb32f 100644 --- a/libs/openagent_demo/frontend/src/api.ts +++ b/libs/openagent_demo/frontend/src/api.ts @@ -419,6 +419,7 @@ export interface VMStatus { managed?: boolean; reason?: string; instance_status?: string | null; + instance_error?: string | null; vm_ready?: boolean; } diff --git a/libs/openagent_demo/frontend/src/components/ChatArea.tsx b/libs/openagent_demo/frontend/src/components/ChatArea.tsx index c655af24..8bf6a35d 100644 --- a/libs/openagent_demo/frontend/src/components/ChatArea.tsx +++ b/libs/openagent_demo/frontend/src/components/ChatArea.tsx @@ -1,6 +1,7 @@ import { useState, useCallback, useRef, useEffect, useMemo } from "react"; import { PanelRight } from "lucide-react"; import { useAppContext } from "../store"; +import { getVMStatus } from "../api"; import WelcomeScreen from "./WelcomeScreen"; import MessageList from "./MessageList"; import ChatInput from "./ChatInput"; @@ -72,7 +73,15 @@ export default function ChatArea({ conversation, onSendMessage, onOpenSettings, const isMac = navigator.platform.toUpperCase().includes("MAC"); const handleModeChange = useCallback( - (mode: ConversationMode) => { + async (mode: ConversationMode) => { + if (mode === "cowork") { + try { + const vs = await getVMStatus(); + dispatch({ type: "SET_VM_STATUS", payload: vs }); + } catch { + // Best-effort refresh; keep UX responsive. + } + } dispatch({ type: "SET_SELECTED_MODE", payload: mode }); }, [dispatch] diff --git a/libs/openagent_demo/frontend/src/components/OnboardingWizard.tsx b/libs/openagent_demo/frontend/src/components/OnboardingWizard.tsx index c5ba2249..8b9be968 100644 --- a/libs/openagent_demo/frontend/src/components/OnboardingWizard.tsx +++ b/libs/openagent_demo/frontend/src/components/OnboardingWizard.tsx @@ -139,6 +139,7 @@ export default function OnboardingWizard({ open, onComplete, settings, onSetting const vmPhase2Error = vm.phase2Error; const vmPhase3 = vm.phase3; const vmUsable = vmPhase1 === "done" && vmPhase2 === "done"; + const vmPhase1NeedsRestart = /restart windows|重启.*windows|重启.*电脑|reboot/i.test(vmPhase1Error || ""); // Load server config and reset name on open useEffect(() => { @@ -822,7 +823,15 @@ export default function OnboardingWizard({ open, onComplete, settings, onSetting )} {vmPhase1 === "error" && ( - + vmPhase1NeedsRestart ? ( + + ) : ( + + ) )}
{vmPhase1 === "error" && vmPhase1Error && ( diff --git a/libs/openagent_demo/frontend/src/components/RestartRequiredModal.tsx b/libs/openagent_demo/frontend/src/components/RestartRequiredModal.tsx new file mode 100644 index 00000000..15354fdd --- /dev/null +++ b/libs/openagent_demo/frontend/src/components/RestartRequiredModal.tsx @@ -0,0 +1,48 @@ +import { AlertTriangle, RefreshCw, Settings } from "lucide-react"; +import { useVMSetup } from "../vmSetup"; + +interface RestartRequiredModalProps { + open: boolean; + message: string; + onOpenSettings: () => void; +} + +export default function RestartRequiredModal({ + open, + message, + onOpenSettings, +}: RestartRequiredModalProps) { + const vm = useVMSetup(); + + if (!open) return null; + + return ( +
+
+
+ +

+ Restart Required +

+
+

+ WSL runtime installation is complete, but Windows must restart before OpenAgent can continue. +

+

+ Please restart your computer now, otherwise VM/Cowork features will not work. +

+ {message ?

{message}

: null} +
+ + +
+
+
+ ); +} diff --git a/libs/openagent_demo/frontend/src/components/SettingsModal.tsx b/libs/openagent_demo/frontend/src/components/SettingsModal.tsx index ec6e9d63..c2009b23 100644 --- a/libs/openagent_demo/frontend/src/components/SettingsModal.tsx +++ b/libs/openagent_demo/frontend/src/components/SettingsModal.tsx @@ -1733,9 +1733,9 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { } }; - // Cowork mode is usable once Lima is installed and VM is running (phases 1+2). - // Phase 3 (dependency installation) can run in the background. - const vmUsable = phase1 === "done" && phase2 === "done"; + // Cowork mode should follow backend truth from /api/setup/vm (vm_ready). + // Keep phase fallback only when status payload is unavailable. + const vmUsable = vmStatus?.vm_ready ?? (phase1 === "done" && phase2 === "done"); const allDone = vmUsable && phase3 === "done"; const anyRunning = phase1 === "running" || phase2 === "running" || phase3 === "running"; const coreError = phase1 === "error" || phase2 === "error"; @@ -1857,6 +1857,26 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { {phase1 === "error" && phase1Error && (

{phase1Error}

)} + {phase1 === "error" && /bios|firmware|vt-x|amd-v|svm|virtualization is disabled|BIOS_VIRT_DISABLED|virtualization/i.test(phase1Error || "") && ( +
+

+ We cannot enable BIOS virtualization remotely, but you can finish it in 3 steps: +

+
    +
  1. Restart and enter BIOS/UEFI setup.
  2. +
  3. Enable virtualization.
  4. +
  5. Save and reboot Windows, then click Retry.
  6. +
+

Common option names:

+
    +
  • Intel: Intel Virtualization Technology / VT-x
  • +
  • AMD: SVM Mode / AMD-V
  • +
+

+ Common BIOS keys: F2, Del, Esc, F10, F12 (varies by brand). +

+
+ )} {phase1 === "done" && (

{vmBackendName} is installed and available.

)} diff --git a/libs/openagent_demo/frontend/src/store.ts b/libs/openagent_demo/frontend/src/store.ts index 80bbe71b..456e98e3 100644 --- a/libs/openagent_demo/frontend/src/store.ts +++ b/libs/openagent_demo/frontend/src/store.ts @@ -15,6 +15,11 @@ export interface Notification { type: "error" | "info" | "success"; } +export interface RestartRequiredModalState { + open: boolean; + message: string; +} + export interface AppState { conversations: Conversation[]; activeConversationId: string | null; @@ -39,6 +44,7 @@ export interface AppState { /** Remembers the last active conversation (or null=welcome) per mode. */ lastActiveByMode: Record; notifications: Notification[]; + restartRequiredModal: RestartRequiredModalState; filePreview: { path: string; mimeType: string; conversationId: string } | null; filePreviewVisible: boolean; /** Saved right-panel state before file preview opened (for restore on close). */ @@ -66,6 +72,7 @@ export const initialState: AppState = { })(), lastActiveByMode: { chat: null, cowork: null }, notifications: [], + restartRequiredModal: { open: false, message: "" }, filePreview: null, filePreviewVisible: false, rightPanelBeforePreview: null, @@ -104,6 +111,8 @@ export type Action = | { type: "SET_SELECTED_MODE"; payload: ConversationMode } | { type: "SHOW_NOTIFICATION"; payload: { message: string; type: "error" | "info" | "success" } } | { type: "DISMISS_NOTIFICATION"; payload: string } + | { type: "SHOW_RESTART_REQUIRED_MODAL"; payload: { message: string } } + | { type: "HIDE_RESTART_REQUIRED_MODAL" } | { type: "SET_FILE_PREVIEW"; payload: { path: string; mimeType: string; conversationId: string } | null } | { type: "SET_FILE_PREVIEW_VISIBLE"; payload: boolean }; @@ -772,6 +781,24 @@ export function reducer(state: AppState, action: Action): AppState { notifications: state.notifications.filter((n) => n.id !== action.payload), }; + case "SHOW_RESTART_REQUIRED_MODAL": + return { + ...state, + restartRequiredModal: { + open: true, + message: action.payload.message, + }, + }; + + case "HIDE_RESTART_REQUIRED_MODAL": + return { + ...state, + restartRequiredModal: { + open: false, + message: "", + }, + }; + case "SET_FILE_PREVIEW": { const rpConvId = state.activeConversationId; const currentPanelVisible = rpConvId ? (state.rightPanelByConversation[rpConvId] ?? false) : false; diff --git a/libs/openagent_demo/frontend/src/vmSetup.tsx b/libs/openagent_demo/frontend/src/vmSetup.tsx index b4b3c7f9..742bac12 100644 --- a/libs/openagent_demo/frontend/src/vmSetup.tsx +++ b/libs/openagent_demo/frontend/src/vmSetup.tsx @@ -27,6 +27,23 @@ import { import type { VMStatus, ProvisionStepDef } from "./api"; import { useAppContext } from "./store"; +const WSL_MANUAL_INSTALL_CMD = "wsl --install --no-distribution"; +const IS_WINDOWS = navigator.platform.toUpperCase().includes("WIN"); +const RESTART_REQUIRED_PATTERN = /restart windows|restart your computer|reboot|重启|重新启动/i; + +function buildWslInstallRecoveryMessage(detail?: string): string { + const base = + `Automatic runtime installation failed. Run this in an Administrator PowerShell: ${WSL_MANUAL_INSTALL_CMD}. ` + + "Then restart Windows and click Retry. You can skip Local VM for now and use E2B Sandbox (chat mode)."; + const trimmed = (detail || "").trim(); + if (!trimmed) return base; + return `${base} Details: ${trimmed}`; +} + +function isRestartRequiredMessage(detail?: string | null): boolean { + return RESTART_REQUIRED_PATTERN.test((detail || "").trim()); +} + // ── Types ── export type PhaseStatus = "checking" | "pending" | "running" | "done" | "error"; @@ -90,6 +107,7 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { const [provStepStatus, setProvStepStatus] = useState>({}); const [provStepMsg, setProvStepMsg] = useState>({}); const [provLog, setProvLog] = useState(null); + const autoBootstrapTriggeredRef = useRef(false); // SSE abort controllers (kept alive across renders, never aborted on unmount) const installCtrl = useRef(null); @@ -108,7 +126,16 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { dispatch({ type: "SHOW_NOTIFICATION", payload: { message, type } }); }; + const showRestartRequiredModal = (message: string) => { + dispatch({ type: "SHOW_RESTART_REQUIRED_MODAL", payload: { message } }); + }; + + const hideRestartRequiredModal = () => { + dispatch({ type: "HIDE_RESTART_REQUIRED_MODAL" }); + }; + const doRecheckVmEngine = async () => { + hideRestartRequiredModal(); setPhase1("checking"); setPhase1Msg("Re-checking runtime status..."); setPhase1Error(""); @@ -123,6 +150,7 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { return; } if (vs.installed) { + hideRestartRequiredModal(); setPhase1("done"); setPhase1Msg(""); setPhase1Error(""); @@ -132,6 +160,7 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { setPhase1("error"); setPhase1Msg(""); setPhase1Error(vs.reason); + if (isRestartRequiredMessage(vs.reason)) showRestartRequiredModal(vs.reason); } else { setPhase1("pending"); setPhase1Msg(""); @@ -221,7 +250,8 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { // ── Phase 1: Install Lima ── - const doInstallLima = () => { + const doInstallLima = async () => { + hideRestartRequiredModal(); const inferredBackend = navigator.platform.toUpperCase().includes("WIN") ? "wsl" : "lima"; const currentBackend = vmStatus?.backend ?? inferredBackend; const canAssistWslInstall = @@ -230,52 +260,84 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { typeof window.electronAPI?.installWslRuntime === "function"; if (canAssistWslInstall) { - setPhase1("running"); + setPhase1("checking"); setPhase1Error(""); - setPhase1Msg("Installing required Windows runtime (admin permission needed)..."); + setPhase1Msg("Checking Windows virtualization prerequisites..."); - window.electronAPI! - .installWslRuntime!() - .then(async (res) => { - if (!res.ok) { - const msg = res.message || "Runtime installation failed."; + try { + if (typeof window.electronAPI?.checkWslPrerequisites === "function") { + const pre = await window.electronAPI.checkWslPrerequisites(); + const autoFixable = + pre?.code === "WINDOWS_FEATURES_DISABLED" || + pre?.code === "HYPERVISOR_DISABLED"; + if (!pre.ok && !autoFixable) { setPhase1("error"); - setPhase1Error(msg); - notify(msg, "error"); + setPhase1Msg(""); + setPhase1Error(pre.message || "WSL prerequisites are not ready."); + notify(pre.message || "WSL prerequisites are not ready.", "error"); return; } - - if (res.rebootRequired) { - const msg = res.message || "Runtime installed. Please restart Windows before continuing VM setup."; + if (!pre.ok && autoFixable) { + notify("Windows features are not enabled yet. OpenAgent will try to enable them with admin permission.", "info"); + } + if (pre.rebootPending) { + const msg = "Windows restart is pending. Please restart first, then continue VM setup."; setPhase1("error"); setPhase1Msg(""); setPhase1Error(msg); notify(msg, "info"); + showRestartRequiredModal(msg); return; } + } - const vs = await getVMStatus(); - setVmStatus(vs); - dispatch({ type: "SET_VM_STATUS", payload: vs }); + setPhase1("running"); + setPhase1Error(""); + setPhase1Msg("Installing required Windows runtime (admin permission needed)..."); - if (vs.installed) { - setPhase1("done"); - setPhase1Msg(""); - notify("Runtime installed", "success"); - setPhase2("pending"); - attachBuild(); - } else { - setPhase1("pending"); - setPhase1Msg("Runtime install command completed. Click Retry if needed."); - notify("Runtime install command completed", "info"); - } - }) - .catch((err: unknown) => { - const msg = err instanceof Error ? err.message : String(err); + const res = await window.electronAPI!.installWslRuntime!(); + if (!res.ok) { + const raw = res.message || "Runtime installation failed."; + const msg = buildWslInstallRecoveryMessage(raw); + setPhase1("error"); + setPhase1Error(msg); + notify("Automatic runtime installation failed. See VM Engine details for manual steps.", "error"); + return; + } + + if (res.rebootRequired) { + const msg = res.message || "Runtime installed. Please restart Windows before continuing VM setup."; setPhase1("error"); + setPhase1Msg(""); setPhase1Error(msg); - notify(`Runtime installation failed: ${msg}`, "error"); - }); + notify(msg, "info"); + showRestartRequiredModal(msg); + return; + } + + const vs = await getVMStatus(); + setVmStatus(vs); + dispatch({ type: "SET_VM_STATUS", payload: vs }); + + if (vs.installed) { + hideRestartRequiredModal(); + setPhase1("done"); + setPhase1Msg(""); + notify("Runtime installed", "success"); + setPhase2("pending"); + attachBuild(); + } else { + setPhase1("pending"); + setPhase1Msg("Runtime install command completed. Click Retry if needed."); + notify("Runtime install command completed", "info"); + } + } catch (err: unknown) { + const raw = err instanceof Error ? err.message : String(err); + const msg = buildWslInstallRecoveryMessage(raw); + setPhase1("error"); + setPhase1Error(msg); + notify("Automatic runtime installation failed. See VM Engine details for manual steps.", "error"); + } return; } @@ -352,20 +414,25 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { vmStatusSnapshot = vs; if (cancelled) return; setVmStatus(vs); - if (!vs.supported) { - setPhase1("error"); - setPhase1Error("Not supported on this platform"); - return; - } - if (vs.installed) { - setPhase1("done"); - limaInstalled = true; - } else if (vs.reason) { - setPhase1("error"); - setPhase1Error(vs.reason); - } else { - setPhase1("pending"); - } + dispatch({ type: "SET_VM_STATUS", payload: vs }); + if (!vs.supported) { + hideRestartRequiredModal(); + setPhase1("error"); + setPhase1Error("Not supported on this platform"); + return; + } + if (vs.installed) { + hideRestartRequiredModal(); + setPhase1("done"); + limaInstalled = true; + } else if (vs.reason) { + setPhase1("error"); + setPhase1Error(vs.reason); + if (isRestartRequiredMessage(vs.reason)) showRestartRequiredModal(vs.reason); + } else { + hideRestartRequiredModal(); + setPhase1("pending"); + } } catch { if (cancelled) return; setPhase1("error"); @@ -378,32 +445,39 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { // Phase 2: VM instance (Lima must be Running; WSL is on-demand — Stopped is OK) const runningLabels = ["Running", "正在运行"]; const wslStoppedLabels = ["Stopped", "已停止"]; - let vmReady = false; - try { - const bs = await getVMBuildStatus(); - if (cancelled) return; - if (bs.status === "running") { - // Re-attach to an in-progress build - attachBuild(); - } else if (bs.vm_state && runningLabels.includes(bs.vm_state)) { - setPhase2("done"); - vmReady = true; - } else if ( - vmStatusSnapshot?.backend === "wsl" && - bs.vm_state && - wslStoppedLabels.includes(bs.vm_state) - ) { - setPhase2("done"); - vmReady = true; - } else if (bs.vm_state === "Stopped") { - setPhase2("pending"); - setPhase2Msg("VM exists but is stopped"); - } else { + let vmReady = vmStatusSnapshot?.vm_ready === true; + if (vmReady) { + setPhase2("done"); + } else if (vmStatusSnapshot?.instance_error) { + setPhase2("error"); + setPhase2Error(vmStatusSnapshot.instance_error); + } else { + try { + const bs = await getVMBuildStatus(); + if (cancelled) return; + if (bs.status === "running") { + // Re-attach to an in-progress build + attachBuild(); + } else if (bs.vm_state && runningLabels.includes(bs.vm_state)) { + setPhase2("done"); + vmReady = true; + } else if ( + vmStatusSnapshot?.backend === "wsl" && + bs.vm_state && + wslStoppedLabels.includes(bs.vm_state) + ) { + setPhase2("done"); + vmReady = true; + } else if (bs.vm_state === "Stopped") { + setPhase2("pending"); + setPhase2Msg("VM exists but is stopped"); + } else { + setPhase2("pending"); + } + } catch { + if (cancelled) return; setPhase2("pending"); } - } catch { - if (cancelled) return; - setPhase2("pending"); } if (!vmReady) return; @@ -441,6 +515,30 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { // ── Context value ── + // Windows-only auto setup: + // automatically trigger VM Engine + VM Instance on startup so users + // don't need to click "Install" manually after app installation. + useEffect(() => { + if (!IS_WINDOWS) return; + if (autoBootstrapTriggeredRef.current) return; + if (!vmStatus?.supported) return; + if (phase1 === "checking") return; + if (phase1 === "running" || phase2 === "running") return; + + if (phase1 === "pending") { + autoBootstrapTriggeredRef.current = true; + notify("Detected first-time Windows setup. Starting VM runtime install automatically...", "info"); + void doInstallLima(); + return; + } + + if (phase1 === "done" && phase2 === "pending") { + autoBootstrapTriggeredRef.current = true; + notify("Runtime is ready. Starting VM instance setup automatically...", "info"); + attachBuild(); + } + }, [vmStatus, phase1, phase2]); // eslint-disable-line react-hooks/exhaustive-deps + const value: VMSetupContextValue = { vmStatus, phase1, phase1Msg, phase1Error, From 3b4c892319b898e489a5d0cd296e296005d935f1 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Thu, 26 Mar 2026 19:03:45 +0800 Subject: [PATCH 12/34] docs: add windows regression checklist and vm prebuilt inventory --- .../windows-installer-regression-checklist.md | 73 ++ .../apt-installed-new.txt | Bin 0 -> 3090 bytes .../apt-installed.txt | 964 ++++++++++++++++++ .../new_dpkg_status.txt | Bin 0 -> 769308 bytes .../npm-global-new.txt | Bin 0 -> 90 bytes .../npm-global.txt | 22 + .../pip-dist-info.txt | 83 ++ .../pip-freeze-new.txt | Bin 0 -> 1246 bytes .../rebuild-comparison.md | 52 + 9 files changed, 1194 insertions(+) create mode 100644 docs/windows-installer-regression-checklist.md create mode 100644 reports/vm-prebuilt-inventory-20260325/apt-installed-new.txt create mode 100644 reports/vm-prebuilt-inventory-20260325/apt-installed.txt create mode 100644 reports/vm-prebuilt-inventory-20260325/new_dpkg_status.txt create mode 100644 reports/vm-prebuilt-inventory-20260325/npm-global-new.txt create mode 100644 reports/vm-prebuilt-inventory-20260325/npm-global.txt create mode 100644 reports/vm-prebuilt-inventory-20260325/pip-dist-info.txt create mode 100644 reports/vm-prebuilt-inventory-20260325/pip-freeze-new.txt create mode 100644 reports/vm-prebuilt-inventory-20260325/rebuild-comparison.md diff --git a/docs/windows-installer-regression-checklist.md b/docs/windows-installer-regression-checklist.md new file mode 100644 index 00000000..c72c92a2 --- /dev/null +++ b/docs/windows-installer-regression-checklist.md @@ -0,0 +1,73 @@ +# Windows Installer Regression Checklist (No-WSL Machine) + +## 0. Test Environment +- Fresh Windows 10/11 x64 machine (no OpenAgent, no WSL distro). +- Verify: + - `wsl --status` shows not installed or unavailable. + - `wsl -l -v` has no `openagent` distro. + +## 1. Install App +- Run `OpenAgent-0.0.1-win-x64.exe`. +- Open OpenAgent -> Settings -> Sandbox. +- Expected: + - App launches normally. + - No crash on Sandbox page. + +## 2. Assisted WSL Install (First-time) +- Click VM setup / Retry on `VM Engine`. +- Expected: + - If WSL missing: clear guided message appears. + - App attempts assisted install (with clear status text). + - If system needs reboot, UI explicitly asks reboot. + +## 3. Reboot Required Path +- If prompted, reboot Windows. +- Re-open OpenAgent -> Sandbox. +- Click Retry again. +- Expected: + - `VM Instance` eventually becomes `Ready`. + - No `exit 4294967295` generic toast without guidance. + +## 4. VM Dependencies Install +- Click install for `VM System Dependencies`. +- Expected: + - Progress steps update continuously. + - Failure (if any) includes actionable reason. + - Success ends with green/ready state. + +## 5. Cowork Functional Smoke Test +- Set cowork folder to a writable path, e.g. `D:\code\agentTest`. +- In chat: + - Create folder `sport`. + - Then create `sport\basketball`. +- Expected: + - Both operations succeed. + - No false "current directory not writable" error. + +## 6. Non-Technical User UX Check +- Trigger BIOS/virtualization-disabled scenario (or validate copy via mocked error). +- Expected: + - Error includes clear next steps: + - Enable virtualization in BIOS (Intel VT-x / AMD SVM). + - Save BIOS and reboot. + - Retry in app. + - Language is understandable for non-technical users. + +## 7. Log Collection (On Failure) +- Collect: + - OpenAgent backend logs. + - Sandbox setup UI log panel output. + - `wsl --status` + - `wsl -l -v` +- Record exact toast error text and timestamp. + +## 8. Release Gate (Pass Criteria) +- Must pass: + - App install + launch. + - WSL guided install path. + - Reboot path. + - VM Instance ready. + - VM dependencies ready. + - Cowork nested directory creation. +- Block release if any of above fails. + diff --git a/reports/vm-prebuilt-inventory-20260325/apt-installed-new.txt b/reports/vm-prebuilt-inventory-20260325/apt-installed-new.txt new file mode 100644 index 0000000000000000000000000000000000000000..42f8bd6ff8c23dc3f52b2957a960fff46f4aa2c1 GIT binary patch literal 3090 ycmezW&z8ZKftP^`NRHB@;V_yGM)SdFIWSrdjFtnV<-lk;Fj@|bmIEWL8~^|WaonH) literal 0 HcmV?d00001 diff --git a/reports/vm-prebuilt-inventory-20260325/apt-installed.txt b/reports/vm-prebuilt-inventory-20260325/apt-installed.txt new file mode 100644 index 00000000..df481158 --- /dev/null +++ b/reports/vm-prebuilt-inventory-20260325/apt-installed.txt @@ -0,0 +1,964 @@ +adduser==3.137ubuntu1 +adwaita-icon-theme==46.0-1 +apparmor==4.0.1really4.0.1-0ubuntu0.24.04.5 +apport==2.28.1-0ubuntu3.8 +apport-core-dump-handler==2.28.1-0ubuntu3.8 +apport-symptoms==0.25 +appstream==1.0.2-1build6 +apt==2.8.3 +apt-transport-https==2.8.3 +apt-utils==2.8.3 +at-spi2-common==2.52.0-1build1 +at-spi2-core==2.52.0-1build1 +base-files==13ubuntu10.4 +base-passwd==3.6.3build1 +bash==5.2.21-2ubuntu4 +bash-completion==1:2.11-8 +bc==1.07.1-3ubuntu4 +binutils==2.42-4ubuntu2.8 +binutils-common==2.42-4ubuntu2.8 +binutils-x86-64-linux-gnu==2.42-4ubuntu2.8 +bsdextrautils==2.39.3-9ubuntu6.5 +bsdutils==1:2.39.3-9ubuntu6.5 +build-essential==12.10ubuntu1 +byobu==6.11-0ubuntu1 +bzip2==1.0.8-5.1build0.1 +ca-certificates==20240203 +ca-certificates-java==20240118 +cloud-guest-utils==0.33-1 +cloud-init==25.2-0ubuntu1~24.04.1 +command-not-found==23.04.0 +console-setup==1.226ubuntu1 +console-setup-linux==1.226ubuntu1 +coreutils==9.4-3ubuntu6.2 +cpp==4:13.2.0-7ubuntu1 +cpp-13==13.3.0-6ubuntu2~24.04.1 +cpp-13-x86-64-linux-gnu==13.3.0-6ubuntu2~24.04.1 +cpp-x86-64-linux-gnu==4:13.2.0-7ubuntu1 +cron==3.0pl1-184ubuntu2 +cron-daemon-common==3.0pl1-184ubuntu2 +curl==8.5.0-2ubuntu10.8 +dash==0.5.12-6ubuntu5 +dbus==1.14.10-4ubuntu4.1 +dbus-bin==1.14.10-4ubuntu4.1 +dbus-daemon==1.14.10-4ubuntu4.1 +dbus-session-bus-common==1.14.10-4ubuntu4.1 +dbus-system-bus-common==1.14.10-4ubuntu4.1 +dbus-user-session==1.14.10-4ubuntu4.1 +dbus-x11==1.14.10-4ubuntu4.1 +dconf-gsettings-backend==0.40.0-4ubuntu0.1 +dconf-service==0.40.0-4ubuntu0.1 +debconf==1.5.86ubuntu1 +debconf-i18n==1.5.86ubuntu1 +debianutils==5.17build1 +default-jre-headless==2:1.21-75+exp1 +dhcpcd-base==1:10.0.6-1ubuntu3.2 +diffutils==1:3.10-1build1 +dirmngr==2.4.4-2ubuntu17.4 +distro-info==1.7build1 +distro-info-data==0.60ubuntu0.5 +dmsetup==2:1.02.185-3ubuntu3.2 +dpkg==1.22.6ubuntu6.5 +dpkg-dev==1.22.6ubuntu6.5 +e2fsprogs==1.47.0-2.4~exp1ubuntu4.1 +e2fsprogs-l10n==1.47.0-2.4~exp1ubuntu4.1 +eatmydata==131-1ubuntu1 +ed==1.20.1-1 +eject==2.39.3-9ubuntu6.5 +ethtool==1:6.7-1build1 +fdisk==2.39.3-9ubuntu6.5 +ffmpeg==7:6.1.1-3ubuntu5 +file==1:5.45-3build1 +findutils==4.9.0-5build1 +fontconfig==2.15.0-1.1ubuntu2 +fontconfig-config==2.15.0-1.1ubuntu2 +fonts-crosextra-caladea==20200211-2 +fonts-crosextra-carlito==20230309-2 +fonts-dejavu==2.37-8 +fonts-dejavu-core==2.37-8 +fonts-dejavu-extra==2.37-8 +fonts-dejavu-mono==2.37-8 +fonts-freefont-ttf==20211204+svn4273-2 +fonts-gfs-baskerville==1.1-6 +fonts-gfs-porson==1.1-7 +fonts-ipafont-gothic==00303-21ubuntu1 +fonts-liberation==1:2.1.5-3 +fonts-liberation2==1:2.1.5-3 +fonts-lmodern==2.005-1 +fonts-noto-cjk==1:20230817+repack1-3 +fonts-noto-color-emoji==2.047-0ubuntu0.24.04.1 +fonts-opensymbol==4:102.12+LibO24.2.7-0ubuntu0.24.04.4 +fonts-texgyre==20180621-6 +fonts-ubuntu==0.869+git20240321-0ubuntu1 +fonts-urw-base35==20200910-8 +fonts-wqy-zenhei==0.9.45-8 +fuse3==3.14.0-5build1 +g++==4:13.2.0-7ubuntu1 +g++-13==13.3.0-6ubuntu2~24.04.1 +g++-13-x86-64-linux-gnu==13.3.0-6ubuntu2~24.04.1 +g++-x86-64-linux-gnu==4:13.2.0-7ubuntu1 +gawk==1:5.2.1-2build3 +gcc==4:13.2.0-7ubuntu1 +gcc-13==13.3.0-6ubuntu2~24.04.1 +gcc-13-base==13.3.0-6ubuntu2~24.04.1 +gcc-13-x86-64-linux-gnu==13.3.0-6ubuntu2~24.04.1 +gcc-14-base==14.2.0-4ubuntu2~24.04.1 +gcc-x86-64-linux-gnu==4:13.2.0-7ubuntu1 +gdisk==1.0.10-1build1 +gettext-base==0.21-14ubuntu2 +ghostscript==10.02.1~dfsg1-0ubuntu7.8 +gir1.2-girepository-2.0==1.80.1-1 +gir1.2-glib-2.0==2.80.0-6ubuntu3.8 +gir1.2-packagekitglib-1.0==1.2.8-2ubuntu1.4 +git==1:2.43.0-1ubuntu7.3 +git-man==1:2.43.0-1ubuntu7.3 +gnupg==2.4.4-2ubuntu17.4 +gnupg-l10n==2.4.4-2ubuntu17.4 +gnupg-utils==2.4.4-2ubuntu17.4 +gpg==2.4.4-2ubuntu17.4 +gpg-agent==2.4.4-2ubuntu17.4 +gpgconf==2.4.4-2ubuntu17.4 +gpgsm==2.4.4-2ubuntu17.4 +gpgv==2.4.4-2ubuntu17.4 +gpg-wks-client==2.4.4-2ubuntu17.4 +graphviz==2.42.2-9ubuntu0.1 +grep==3.11-4build1 +groff-base==1.23.0-3build2 +gsettings-desktop-schemas==46.1-0ubuntu1 +gtk-update-icon-cache==3.24.41-4ubuntu1.3 +gzip==1.12-1ubuntu3.1 +hicolor-icon-theme==0.17-2 +hostname==3.23+nmu2ubuntu2 +humanity-icon-theme==0.6.16 +imagemagick==8:6.9.12.98+dfsg1-5.2build2 +imagemagick-6.q16==8:6.9.12.98+dfsg1-5.2build2 +imagemagick-6-common==8:6.9.12.98+dfsg1-5.2build2 +info==7.1-3build2 +init==1.66ubuntu1 +init-system-helpers==1.66ubuntu1 +install-info==7.1-3build2 +iproute2==6.1.0-1ubuntu6.2 +iputils-ping==3:20240117-1ubuntu0.1 +iso-codes==4.16.0-1 +java-common==0.75+exp1 +jq==1.7.1-3ubuntu0.24.04.1 +kbd==2.6.4-2ubuntu2 +keyboard-configuration==1.226ubuntu1 +keyboxd==2.4.4-2ubuntu17.4 +kmod==31+20240202-2ubuntu7.1 +krb5-locales==1.20.1-6ubuntu2.6 +landscape-client==24.02-0ubuntu5.7 +landscape-common==24.02-0ubuntu5.7 +latexmk==1:4.83-1 +less==590-2ubuntu2.1 +libabsl20220623t64==20220623.1-3.1ubuntu3.2 +libabw-0.1-1==0.1.3-1build4 +libacl1==2.3.2-1build1.1 +libann0==1.1.2+doc-9build1 +libaom3==3.8.2-2ubuntu0.1 +libapache-pom-java==29-2 +libapparmor1==4.0.1really4.0.1-0ubuntu0.24.04.5 +libappstream5==1.0.2-1build6 +libapt-pkg6.0t64==2.8.3 +libarchive13t64==3.7.2-2ubuntu0.5 +libargon2-1==0~20190702+dfsg-4build1 +libasan8==14.2.0-4ubuntu2~24.04.1 +libasound2-data==1.2.11-1ubuntu0.2 +libasound2t64==1.2.11-1ubuntu0.2 +libass9==1:0.17.1-2build1 +libassuan0==2.5.6-1build1 +libasyncns0==0.8-6build4 +libatk1.0-0t64==2.52.0-1build1 +libatk-bridge2.0-0t64==2.52.0-1build1 +libatm1t64==1:2.5.1-5.1build1 +libatomic1==14.2.0-4ubuntu2~24.04.1 +libatspi2.0-0t64==2.52.0-1build1 +libattr1==1:2.5.2-1build1.1 +libaudit1==1:3.1.2-2.1build1.1 +libaudit-common==1:3.1.2-2.1build1.1 +libavahi-client3==0.8-13ubuntu6.1 +libavahi-common3==0.8-13ubuntu6.1 +libavahi-common-data==0.8-13ubuntu6.1 +libavc1394-0==0.5.4-5build3 +libavcodec60==7:6.1.1-3ubuntu5 +libavdevice60==7:6.1.1-3ubuntu5 +libavfilter9==7:6.1.1-3ubuntu5 +libavformat60==7:6.1.1-3ubuntu5 +libavutil58==7:6.1.1-3ubuntu5 +libbcprov-java==1.77-1 +libbinutils==2.42-4ubuntu2.8 +libblas3==3.12.0-3build1.1 +libblkid1==2.39.3-9ubuntu6.5 +libblkid-dev==2.39.3-9ubuntu6.5 +libbluray2==1:1.3.4-1build1 +libboost-iostreams1.83.0==1.83.0-2.1ubuntu3.2 +libboost-locale1.83.0==1.83.0-2.1ubuntu3.2 +libboost-thread1.83.0==1.83.0-2.1ubuntu3.2 +libbpf1==1:1.3.0-2build2 +libbrotli1==1.1.0-2build2 +libbrotli-dev==1.1.0-2build2 +libbs2b0==3.1.0+dfsg-7build1 +libbsd0==0.12.1-1build1.1 +libbz2-1.0==1.0.8-5.1build0.1 +libbz2-dev==1.0.8-5.1build0.1 +libc6==2.39-0ubuntu8.7 +libc6-dev==2.39-0ubuntu8.7 +libcaca0==0.99.beta20-4ubuntu0.1 +libcairo2==1.18.0-3build1 +libcairo2-dev==1.18.0-3build1 +libcairo-gobject2==1.18.0-3build1 +libcairo-script-interpreter2==1.18.0-3build1 +libcap2==1:2.66-5ubuntu2.2 +libcap2-bin==1:2.66-5ubuntu2.2 +libcap-ng0==0.8.4-2build2 +libc-bin==2.39-0ubuntu8.7 +libcbor0.10==0.10.2-1.2ubuntu2 +libcc1-0==14.2.0-4ubuntu2~24.04.1 +libc-dev-bin==2.39-0ubuntu8.7 +libcdio19t64==2.1.0-4.1ubuntu1.2 +libcdio-cdda2t64==10.2+2.0.1-1.1build2 +libcdio-paranoia2t64==10.2+2.0.1-1.1build2 +libcdr-0.1-1==0.1.7-1build2 +libcdt5==2.42.2-9ubuntu0.1 +libcgraph6==2.42.2-9ubuntu0.1 +libchromaprint1==1.5.1-5 +libcjson1==1.7.17-1 +libclucene-contribs1t64==2.3.3.4+dfsg-1.2ubuntu2 +libclucene-core1t64==2.3.3.4+dfsg-1.2ubuntu2 +libcodec2-1.2==1.2.0-2build1 +libcolamd3==1:7.6.1+dfsg-1build1 +libcolord2==1.4.7-1build2 +libcom-err2==1.47.0-2.4~exp1ubuntu4.1 +libcommons-lang3-java==3.14.0-1 +libcommons-logging-java==1.3.0-1ubuntu1 +libcommons-parent-java==56-1 +libcrypt1==1:4.4.36-4build1 +libcrypt-dev==1:4.4.36-4build1 +libcryptsetup12==2:2.7.0-1ubuntu4.2 +libctf0==2.42-4ubuntu2.8 +libctf-nobfd0==2.42-4ubuntu2.8 +libcups2t64==2.4.7-1.2ubuntu7.9 +libcurl3t64-gnutls==8.5.0-2ubuntu10.8 +libcurl4t64==8.5.0-2ubuntu10.8 +libdatrie1==0.2.13-3build1 +libdav1d7==1.4.1-1build1 +libdb5.3t64==5.3.28+dfsg2-7 +libdbus-1-3==1.14.10-4ubuntu4.1 +libdc1394-25==2.2.6-4build1 +libdconf1==0.40.0-4ubuntu0.1 +libde265-0==1.0.15-1build3 +libdebconfclient0==0.271ubuntu3 +libdecor-0-0==0.2.2-1build2 +libdeflate0==1.19-1build1.1 +libdevmapper1.02.1==2:1.02.185-3ubuntu3.2 +libdouble-conversion3==3.3.0-1build1 +libdpkg-perl==1.22.6ubuntu6.5 +libdrm2==2.4.125-1ubuntu0.1~24.04.1 +libdrm-amdgpu1==2.4.125-1ubuntu0.1~24.04.1 +libdrm-common==2.4.125-1ubuntu0.1~24.04.1 +libdrm-intel1==2.4.125-1ubuntu0.1~24.04.1 +libduktape207==2.7.0+tests-0ubuntu3 +libdw1t64==0.190-1.1ubuntu0.1 +libeatmydata1==131-1ubuntu1 +libe-book-0.1-1==0.1.3-2build6 +libedit2==3.1-20230828-1build1 +libegl1==1.7.0-1build1 +libegl-mesa0==25.2.8-0ubuntu0.24.04.1 +libelf1t64==0.190-1.1ubuntu0.1 +libeot0==0.01-5build3 +libepoxy0==1.5.10-1build1 +libepubgen-0.1-1==0.1.1-1ubuntu6 +liberror-perl==0.17029-2 +libestr0==0.1.11-1build1 +libetonyek-0.1-1==0.1.10-5build1 +libevdev2==1.13.1+dfsg-1build1 +libevent-core-2.1-7t64==2.1.12-stable-9ubuntu2 +libexpat1==2.6.1-2ubuntu0.4 +libexpat1-dev==2.6.1-2ubuntu0.4 +libext2fs2t64==1.47.0-2.4~exp1ubuntu4.1 +libexttextcat-2.0-0==3.4.7-1build1 +libexttextcat-data==3.4.7-1build1 +libfastjson4==1.2304.0-1build1 +libfdisk1==2.39.3-9ubuntu6.5 +libffi8==3.4.6-1build1 +libffi-dev==3.4.6-1build1 +libfftw3-double3==3.3.10-1ubuntu3 +libfido2-1==1.14.0-1build3 +libflac12t64==1.4.3+ds-2.1ubuntu2 +libflite1==2.2-6build3 +libfontbox-java==1:1.8.16-5 +libfontconfig1==2.15.0-1.1ubuntu2 +libfontconfig-dev==2.15.0-1.1ubuntu2 +libfontenc1==1:1.1.8-1build1 +libfreehand-0.1-1==0.1.2-3build3 +libfreetype6==2.13.2+dfsg-1ubuntu0.1 +libfreetype-dev==2.13.2+dfsg-1ubuntu0.1 +libfribidi0==1.0.13-3build1 +libfuse3-3==3.14.0-5build1 +libgbm1==25.2.8-0ubuntu0.24.04.1 +libgcc-13-dev==13.3.0-6ubuntu2~24.04.1 +libgcc-s1==14.2.0-4ubuntu2~24.04.1 +libgcrypt20==1.10.3-2build1 +libgd3==2.3.3-9ubuntu5 +libgdbm6t64==1.23-5.1build1 +libgdbm-compat4t64==1.23-5.1build1 +libgdk-pixbuf-2.0-0==2.42.10+dfsg-3ubuntu3.2 +libgdk-pixbuf2.0-bin==2.42.10+dfsg-3ubuntu3.2 +libgdk-pixbuf2.0-common==2.42.10+dfsg-3ubuntu3.2 +libgfortran5==14.2.0-4ubuntu2~24.04.1 +libgif7==5.2.2-1ubuntu1 +libgirepository-1.0-1==1.80.1-1 +libgirepository-2.0-0==2.80.0-6ubuntu3.8 +libgl1==1.7.0-1build1 +libgl1-mesa-dri==25.2.8-0ubuntu0.24.04.1 +libgles2==1.7.0-1build1 +libglib2.0-0t64==2.80.0-6ubuntu3.8 +libglib2.0-bin==2.80.0-6ubuntu3.8 +libglib2.0-data==2.80.0-6ubuntu3.8 +libglib2.0-dev==2.80.0-6ubuntu3.8 +libglib2.0-dev-bin==2.80.0-6ubuntu3.8 +libglvnd0==1.7.0-1build1 +libglx0==1.7.0-1build1 +libglx-mesa0==25.2.8-0ubuntu0.24.04.1 +libgme0==0.6.3-7build1 +libgmp10==2:6.3.0+dfsg-2ubuntu6.1 +libgnutls30t64==3.8.3-1.1ubuntu3.4 +libgomp1==14.2.0-4ubuntu2~24.04.1 +libgpg-error0==1.47-3build2.1 +libgpg-error-l10n==1.47-3build2.1 +libgpgme11t64==1.18.0-4.1ubuntu4 +libgpgmepp6t64==1.18.0-4.1ubuntu4 +libgpm2==1.20.7-11 +libgprofng0==2.42-4ubuntu2.8 +libgraphene-1.0-0==1.10.8-3build2 +libgraphite2-3==1.3.14-2build1 +libgs10==10.02.1~dfsg1-0ubuntu7.8 +libgs10-common==10.02.1~dfsg1-0ubuntu7.8 +libgs-common==10.02.1~dfsg1-0ubuntu7.8 +libgsm1==1.0.22-1build1 +libgssapi-krb5-2==1.20.1-6ubuntu2.6 +libgstreamer1.0-0==1.24.2-1ubuntu0.1 +libgstreamer-plugins-base1.0-0==1.24.2-1ubuntu0.3 +libgtk-3-0t64==3.24.41-4ubuntu1.3 +libgtk-3-bin==3.24.41-4ubuntu1.3 +libgtk-3-common==3.24.41-4ubuntu1.3 +libgtk-4-1==4.14.5+ds-0ubuntu0.9 +libgtk-4-common==4.14.5+ds-0ubuntu0.9 +libgts-0.7-5t64==0.7.6+darcs121130-5.2build1 +libgudev-1.0-0==1:238-5ubuntu1 +libgvc6==2.42.2-9ubuntu0.1 +libgvpr2==2.42.2-9ubuntu0.1 +libharfbuzz0b==8.3.0-2build2 +libharfbuzz-icu0==8.3.0-2build2 +libheif1==1.17.6-1ubuntu4.2 +libheif-plugin-aomdec==1.17.6-1ubuntu4.2 +libheif-plugin-libde265==1.17.6-1ubuntu4.2 +libhogweed6t64==3.9.1-2.2build1.1 +libhunspell-1.7-0==1.7.2+really1.7.2-10build3 +libhwasan0==14.2.0-4ubuntu2~24.04.1 +libhwy1t64==1.0.7-8.1build1 +libhyphen0==2.8.8-7build3 +libice6==2:1.0.10-1build3 +libice-dev==2:1.0.10-1build3 +libicu74==74.2-1ubuntu3.1 +libidn12==1.42-1build1 +libidn2-0==2.3.7-2build1.1 +libiec61883-0==1.2.0-6build1 +libijs-0.35==0.35-15.1build1 +libinput10==1.25.0-1ubuntu3.3 +libinput-bin==1.25.0-1ubuntu3.3 +libisl23==0.26-3build1.1 +libitm1==14.2.0-4ubuntu2~24.04.1 +libjack-jackd2-0==1.9.21~dfsg-3ubuntu3 +libjansson4==2.14-2build2 +libjbig0==2.1-6.1ubuntu2 +libjbig2dec0==0.20-1build3 +libjpeg8==8c-2ubuntu11 +libjpeg-turbo8==2.1.5-2ubuntu2 +libjq1==1.7.1-3ubuntu0.24.04.1 +libjs-jquery==3.6.1+dfsg+~3.5.14-1 +libjson-c5==0.17-1build1 +libjs-sphinxdoc==7.2.6-6 +libjs-underscore==1.13.4~dfsg+~1.11.4-3 +libjxl0.7==0.7.0-10.2ubuntu6.1 +libk5crypto3==1.20.1-6ubuntu2.6 +libkeyutils1==1.6.3-3build1 +libkmod2==31+20240202-2ubuntu7.1 +libkpathsea6==2023.20230311.66589-9build3 +libkrb5-3==1.20.1-6ubuntu2.6 +libkrb5support0==1.20.1-6ubuntu2.6 +libksba8==1.6.6-1build1 +liblab-gamut1==2.42.2-9ubuntu0.1 +liblangtag1==0.6.7-1build2 +liblangtag-common==0.6.7-1build2 +liblapack3==3.12.0-3build1.1 +liblcms2-2==2.14-2build1 +libldap2==2.6.10+dfsg-0ubuntu0.24.04.1 +libldap-common==2.6.10+dfsg-0ubuntu0.24.04.1 +liblept5==1.82.0-3build4 +liblerc4==4.0.0+ds-4ubuntu2 +liblibreoffice-java==4:24.2.7-0ubuntu0.24.04.4 +liblilv-0-0==0.24.22-1build1 +libllvm20==1:20.1.2-0ubuntu1~24.04.2 +liblocale-gettext-perl==1.07-6ubuntu5 +liblqr-1-0==0.4.2-2.1build2 +liblsan0==14.2.0-4ubuntu2~24.04.1 +libltdl7==2.4.7-7build1 +liblua5.4-0==5.4.6-3build2 +liblz4-1==1.9.4-1build1.1 +liblzma5==5.6.1+really5.4.5-1ubuntu0.2 +liblzo2-2==2.10-2build4 +libmagic1t64==1:5.45-3build1 +libmagickcore-6.q16-7t64==8:6.9.12.98+dfsg1-5.2build2 +libmagickwand-6.q16-7t64==8:6.9.12.98+dfsg1-5.2build2 +libmagic-mgc==1:5.45-3build1 +libmbedcrypto7t64==2.28.8-1 +libmd0==1.1.0-2build1.1 +libmd4c0==0.4.8-1build1 +libmhash2==0.9.9.9-9build3 +libmnl0==1.0.5-2build1 +libmount1==2.39.3-9ubuntu6.5 +libmount-dev==2.39.3-9ubuntu6.5 +libmp3lame0==3.100-6build1 +libmpc3==1.3.1-1build1.1 +libmpfr6==4.2.1-1build1.1 +libmpg123-0t64==1.32.5-1ubuntu1.1 +libmspub-0.1-1==0.1.4-3build7 +libmtdev1t64==1.1.6-1.1build1 +libmwaw-0.3-3==0.3.22-1build1 +libmysofa1==1.3.2+dfsg-2ubuntu2 +libmythes-1.2-0==2:1.2.5-1build1 +libncursesw6==6.4+20240113-1ubuntu2 +libnetplan1==1.1.2-8ubuntu1~24.04.1 +libnettle8t64==3.9.1-2.2build1.1 +libnewt0.52==0.52.24-2ubuntu2 +libnghttp2-14==1.59.0-1ubuntu0.2 +libnorm1t64==1.5.9+dfsg-3.1build1 +libnpth0t64==1.6-3.1build1 +libnspr4==2:4.35-1.1build1 +libnss3==2:3.98-1ubuntu0.1 +libnss3-tools==2:3.98-1ubuntu0.1 +libnss-systemd==255.4-1ubuntu8.12 +libnuma1==2.0.18-1ubuntu0.24.04.1 +libodfgen-0.1-1==0.1.8-2build3 +libogg0==1.3.5-3build1 +libonig5==6.9.9-1build1 +libopenal1==1:1.23.1-4build1 +libopenal-data==1:1.23.1-4build1 +libopenjp2-7==2.5.0-2ubuntu0.4 +libopenmpt0t64==0.7.3-1.1build3 +libopus0==1.4-1build1 +liborc-0.4-0t64==1:0.4.38-1ubuntu0.1 +liborcus-0.18-0==0.19.2-3build3 +liborcus-parser-0.18-0==0.19.2-3build3 +libp11-kit0==0.25.3-4ubuntu2.1 +libpackagekit-glib2-18==1.2.8-2ubuntu1.4 +libpagemaker-0.0-0==0.0.4-1build4 +libpam0g==1.5.3-5ubuntu5.5 +libpam-cap==1:2.66-5ubuntu2.2 +libpam-modules==1.5.3-5ubuntu5.5 +libpam-modules-bin==1.5.3-5ubuntu5.5 +libpam-runtime==1.5.3-5ubuntu5.5 +libpam-systemd==255.4-1ubuntu8.12 +libpango-1.0-0==1.52.1+ds-1build1 +libpangocairo-1.0-0==1.52.1+ds-1build1 +libpangoft2-1.0-0==1.52.1+ds-1build1 +libpaper1==1.1.29build1 +libpaper-utils==1.1.29build1 +libpathplan4==2.42.2-9ubuntu0.1 +libpciaccess0==0.17-3ubuntu0.24.04.2 +libpcre2-16-0==10.42-4ubuntu2.1 +libpcre2-32-0==10.42-4ubuntu2.1 +libpcre2-8-0==10.42-4ubuntu2.1 +libpcre2-dev==10.42-4ubuntu2.1 +libpcre2-posix3==10.42-4ubuntu2.1 +libpcsclite1==2.0.3-1build1 +libpdfbox-java==1:1.8.16-5 +libperl5.38t64==5.38.2-3.2ubuntu0.2 +libpgm-5.3-0t64==5.3.128~dfsg-2.1build1 +libpipeline1==1.5.7-2 +libpixman-1-0==0.42.2-1build1 +libpixman-1-dev==0.42.2-1build1 +libpkgconf3==1.8.1-2build1 +libplacebo338==6.338.2-2build1 +libpng16-16t64==1.6.43-5ubuntu0.5 +libpng-dev==1.6.43-5ubuntu0.5 +libpocketsphinx3==0.8.0+real5prealpha+1-15ubuntu5 +libpolkit-agent-1-0==124-2ubuntu1.24.04.2 +libpolkit-gobject-1-0==124-2ubuntu1.24.04.2 +libpoppler134==24.02.0-1ubuntu9.8 +libpopt0==1.19+dfsg-1build1 +libpostproc57==7:6.1.1-3ubuntu5 +libpotrace0==1.16-2build1 +libproc2-0==2:4.0.4-4ubuntu3.2 +libpsl5t64==0.21.2-1.1build1 +libptexenc1==2023.20230311.66589-9build3 +libpthread-stubs0-dev==0.4-1build3 +libpulse0==1:16.1+dfsg1-2ubuntu10.1 +libpython3.12-dev==3.12.3-1ubuntu0.12 +libpython3.12-minimal==3.12.3-1ubuntu0.12 +libpython3.12-stdlib==3.12.3-1ubuntu0.12 +libpython3.12t64==3.12.3-1ubuntu0.12 +libpython3-dev==3.12.3-0ubuntu2.1 +libpython3-stdlib==3.12.3-0ubuntu2.1 +libqpdf29t64==11.9.0-1.1ubuntu0.1 +libqt5core5t64==5.15.13+dfsg-1ubuntu1 +libqt5dbus5t64==5.15.13+dfsg-1ubuntu1 +libqt5gui5t64==5.15.13+dfsg-1ubuntu1 +libqt5network5t64==5.15.13+dfsg-1ubuntu1 +libqt5positioning5==5.15.13+dfsg-1 +libqt5printsupport5t64==5.15.13+dfsg-1ubuntu1 +libqt5qml5==5.15.13+dfsg-1ubuntu0.1 +libqt5qmlmodels5==5.15.13+dfsg-1ubuntu0.1 +libqt5quick5==5.15.13+dfsg-1ubuntu0.1 +libqt5sensors5==5.15.13-1 +libqt5svg5==5.15.13-1 +libqt5webchannel5==5.15.13-1 +libqt5webkit5==5.212.0~alpha4-36 +libqt5widgets5t64==5.15.13+dfsg-1ubuntu1 +libquadmath0==14.2.0-4ubuntu2~24.04.1 +librabbitmq4==0.11.0-1build2 +libraptor2-0==2.0.16-3ubuntu0.1 +librasqal3t64==0.9.33-2.1build1 +librav1e0==0.7.1-2 +libraw1394-11==2.1.2-2build3 +libraw23t64==0.21.2-2.1ubuntu0.24.04.1 +librdf0t64==1.0.17-3.1ubuntu3 +libreadline8t64==8.2-4build1 +libreoffice-base-core==4:24.2.7-0ubuntu0.24.04.4 +libreoffice-calc==4:24.2.7-0ubuntu0.24.04.4 +libreoffice-common==4:24.2.7-0ubuntu0.24.04.4 +libreoffice-core==4:24.2.7-0ubuntu0.24.04.4 +libreoffice-draw==4:24.2.7-0ubuntu0.24.04.4 +libreoffice-impress==4:24.2.7-0ubuntu0.24.04.4 +libreoffice-java-common==4:24.2.7-0ubuntu0.24.04.4 +libreoffice-style-colibre==4:24.2.7-0ubuntu0.24.04.4 +libreoffice-uiconfig-calc==4:24.2.7-0ubuntu0.24.04.4 +libreoffice-uiconfig-common==4:24.2.7-0ubuntu0.24.04.4 +libreoffice-uiconfig-draw==4:24.2.7-0ubuntu0.24.04.4 +libreoffice-uiconfig-impress==4:24.2.7-0ubuntu0.24.04.4 +libreoffice-uiconfig-writer==4:24.2.7-0ubuntu0.24.04.4 +libreoffice-writer==4:24.2.7-0ubuntu0.24.04.4 +librevenge-0.0-0==0.0.5-3build1 +librist4==0.2.10+dfsg-2 +librsvg2-2==2.58.0+dfsg-1build1 +librsvg2-common==2.58.0+dfsg-1build1 +librtmp1==2.4+20151223.gitfa8646d.1-2build7 +librubberband2==3.3.0+dfsg-2build1 +libsamplerate0==0.2.2-4build1 +libsasl2-2==2.1.28+dfsg1-5ubuntu3.1 +libsasl2-modules==2.1.28+dfsg1-5ubuntu3.1 +libsasl2-modules-db==2.1.28+dfsg1-5ubuntu3.1 +libsdl2-2.0-0==2.30.0+dfsg-1ubuntu3.1 +libseccomp2==2.5.5-1ubuntu3.1 +libselinux1==3.5-2ubuntu2.1 +libselinux1-dev==3.5-2ubuntu2.1 +libsemanage2==3.5-1build5 +libsemanage-common==3.5-1build5 +libsensors5==1:3.6.0-9build1 +libsensors-config==1:3.6.0-9build1 +libsepol2==3.5-2build1 +libsepol-dev==3.5-2build1 +libserd-0-0==0.32.2-1 +libsframe1==2.42-4ubuntu2.8 +libsharpyuv0==1.3.2-0.4build3 +libshine3==3.1.1-2build1 +libsigsegv2==2.14-1ubuntu2 +libslang2==2.3.3-3build2 +libsm6==2:1.2.3-1build3 +libsmartcols1==2.39.3-9ubuntu6.5 +libsm-dev==2:1.2.3-1build3 +libsnappy1v5==1.1.10-1build1 +libsndfile1==1.2.2-1ubuntu5.24.04.1 +libsndio7.0==1.9.0-0.3build3 +libsodium23==1.0.18-1ubuntu0.24.04.1 +libsord-0-0==0.16.16-2build1 +libsoxr0==0.1.3-4build3 +libspeex1==1.2.1-2ubuntu2.24.04.1 +libsphinxbase3t64==0.8+5prealpha+1-17build2 +libsqlite3-0==3.45.1-1ubuntu2.5 +libsratom-0-0==0.6.16-1build1 +libsrt1.5-gnutls==1.5.3-1build2 +libss2==1.47.0-2.4~exp1ubuntu4.1 +libssh-4==0.10.6-2ubuntu0.4 +libssh-gcrypt-4==0.10.6-2ubuntu0.4 +libssl3t64==3.0.13-0ubuntu3.7 +libstdc++-13-dev==13.3.0-6ubuntu2~24.04.1 +libstdc++6==14.2.0-4ubuntu2~24.04.1 +libstemmer0d==2.2.0-4build1 +libsuitesparseconfig7==1:7.6.1+dfsg-1build1 +libsvtav1enc1d1==1.7.0+dfsg-2build1 +libswresample4==7:6.1.1-3ubuntu5 +libswscale7==7:6.1.1-3ubuntu5 +libsynctex2==2023.20230311.66589-9build3 +libsystemd0==255.4-1ubuntu8.12 +libsystemd-shared==255.4-1ubuntu8.12 +libtasn1-6==4.19.0-3ubuntu0.24.04.2 +libteckit0==2.5.12+ds1-1 +libtesseract5==5.3.4-1build5 +libtexlua53-5==2023.20230311.66589-9build3 +libtext-charwidth-perl==0.04-11build3 +libtext-iconv-perl==1.7-8build3 +libtext-wrapi18n-perl==0.06-10 +libthai0==0.1.29-2build1 +libthai-data==0.1.29-2build1 +libtheora0==1.1.1+dfsg.1-16.1build3 +libtiff6==4.5.1+git230720-4ubuntu2.4 +libtinfo6==6.4+20240113-1ubuntu2 +libtirpc3t64==1.3.4+ds-1.1build1 +libtirpc-common==1.3.4+ds-1.1build1 +libtsan2==14.2.0-4ubuntu2~24.04.1 +libtwolame0==0.4.0-2build3 +libubsan1==14.2.0-4ubuntu2~24.04.1 +libuchardet0==0.0.8-1build1 +libudev1==255.4-1ubuntu8.12 +libudfread0==1.1.2-1build1 +libunibreak5==5.1-2build1 +libunistring5==1.1-2build1.1 +libuno-cppu3t64==4:24.2.7-0ubuntu0.24.04.4 +libuno-cppuhelpergcc3-3t64==4:24.2.7-0ubuntu0.24.04.4 +libunoloader-java==4:24.2.7-0ubuntu0.24.04.4 +libuno-purpenvhelpergcc3-3t64==4:24.2.7-0ubuntu0.24.04.4 +libuno-sal3t64==4:24.2.7-0ubuntu0.24.04.4 +libuno-salhelpergcc3-3t64==4:24.2.7-0ubuntu0.24.04.4 +libunwind8==1.6.2-3build1.1 +libusb-1.0-0==2:1.0.27-1 +libutempter0==1.2.1-3build1 +libuuid1==2.39.3-9ubuntu6.5 +libva2==2.20.0-2ubuntu0.1 +libva-drm2==2.20.0-2ubuntu0.1 +libva-x11-2==2.20.0-2ubuntu0.1 +libvdpau1==1.5-2build1 +libvidstab1.1==1.1.0-2build1 +libvisio-0.1-1==0.1.7-1build9 +libvorbis0a==1.3.7-1build3 +libvorbisenc2==1.3.7-1build3 +libvorbisfile3==1.3.7-1build3 +libvpl2==2023.3.0-1build1 +libvpx9==1.14.0-1ubuntu2.3 +libvulkan1==1.3.275.0-1build1 +libwacom9==2.10.0-2 +libwacom-common==2.10.0-2 +libwayland-client0==1.22.0-2.1build1 +libwayland-cursor0==1.22.0-2.1build1 +libwayland-egl1==1.22.0-2.1build1 +libwebp7==1.3.2-0.4build3 +libwebpdemux2==1.3.2-0.4build3 +libwebpmux3==1.3.2-0.4build3 +libwoff1==1.0.2-2build1 +libwpd-0.10-10==0.10.3-2build2 +libwpg-0.3-3==0.3.4-3build1 +libwps-0.4-4==0.4.14-2build1 +libx11-6==2:1.8.7-1build1 +libx11-data==2:1.8.7-1build1 +libx11-dev==2:1.8.7-1build1 +libx11-xcb1==2:1.8.7-1build1 +libx264-164==2:0.164.3108+git31e19f9-1 +libx265-199==3.5-2build1 +libxau6==1:1.0.9-1build6 +libxau-dev==1:1.0.9-1build6 +libxaw7==2:1.0.14-1build2 +libxcb1==1.15-1ubuntu2 +libxcb1-dev==1.15-1ubuntu2 +libxcb-dri3-0==1.15-1ubuntu2 +libxcb-glx0==1.15-1ubuntu2 +libxcb-icccm4==0.4.1-1.1build3 +libxcb-image0==0.4.0-2build1 +libxcb-keysyms1==0.4.0-1build4 +libxcb-present0==1.15-1ubuntu2 +libxcb-randr0==1.15-1ubuntu2 +libxcb-render0==1.15-1ubuntu2 +libxcb-render0-dev==1.15-1ubuntu2 +libxcb-render-util0==0.3.9-1build4 +libxcb-shape0==1.15-1ubuntu2 +libxcb-shm0==1.15-1ubuntu2 +libxcb-shm0-dev==1.15-1ubuntu2 +libxcb-sync1==1.15-1ubuntu2 +libxcb-util1==0.4.0-1build3 +libxcb-xfixes0==1.15-1ubuntu2 +libxcb-xinerama0==1.15-1ubuntu2 +libxcb-xinput0==1.15-1ubuntu2 +libxcb-xkb1==1.15-1ubuntu2 +libxcomposite1==1:0.4.5-1build3 +libxcursor1==1:1.2.1-1build1 +libxdamage1==1:1.1.6-1build1 +libxdmcp6==1:1.1.3-0ubuntu6 +libxdmcp-dev==1:1.1.3-0ubuntu6 +libxext6==2:1.3.4-1build2 +libxext-dev==2:1.3.4-1build2 +libxfixes3==1:6.0.0-2build1 +libxfont2==1:2.0.6-1build1 +libxi6==2:1.8.1-1build1 +libxinerama1==2:1.1.4-3build1 +libxkbcommon0==1.6.0-1build1 +libxkbcommon-x11-0==1.6.0-1build1 +libxkbfile1==1:1.1.0-1build4 +libxml2==2.9.14+dfsg-1.3ubuntu3.7 +libxmlb2==0.3.18-1 +libxmlsec1t64==1.2.39-5build2 +libxmlsec1t64-nss==1.2.39-5build2 +libxmu6==2:1.1.3-3build2 +libxmuu1==2:1.1.3-3build2 +libxpm4==1:3.5.17-1build2 +libxrandr2==2:1.5.2-2build1 +libxrender1==1:0.9.10-1.1build1 +libxrender-dev==1:0.9.10-1.1build1 +libxshmfence1==1.3-1build5 +libxslt1.1==1.1.39-0exp1ubuntu0.24.04.3 +libxss1==1:1.2.3-1build3 +libxt6t64==1:1.2.1-1.2build1 +libxtables12==1.8.10-3ubuntu2 +libxtst6==2:1.2.3-1.1build1 +libxv1==2:1.0.11-1.1build1 +libxvidcore4==2:1.3.7-1build1 +libxxf86vm1==1:1.1.4-1build4 +libxxhash0==0.8.2-2build1 +libyajl2==2.1.0-5build1 +libyaml-0-2==0.2.5-1build1 +libzimg2==3.0.5+ds1-1build1 +libzix-0-0==0.4.2-2build1 +libzmq5==4.3.5-1build2 +libzstd1==1.5.5+dfsg2-2build1.1 +libzvbi0t64==0.2.42-2 +libzvbi-common==0.2.42-2 +libzzip-0-13t64==0.13.72+dfsg.1-1.2build1 +linux-libc-dev==6.8.0-106.106 +locales==2.39-0ubuntu8.7 +login==1:4.13+dfsg1-4ubuntu3.2 +logrotate==3.21.0-2build1 +logsave==1.47.0-2.4~exp1ubuntu4.1 +lp-solve==5.5.2.5-2ubuntu0.1 +lsb-release==12.0-2 +lshw==02.19.git.2021.06.19.996aaad9c7-2build3 +lsof==4.95.0-1build3 +lto-disabled-list==47 +make==4.3-4.1build2 +man-db==2.12.0-4build2 +manpages==6.7-2 +mawk==1.3.4.20240123-1build1 +media-types==10.1.0 +mesa-libgallium==25.2.8-0ubuntu0.24.04.1 +mesa-vulkan-drivers==25.2.8-0ubuntu0.24.04.1 +motd-news-config==13ubuntu10.4 +mount==2.39.3-9ubuntu6.5 +nano==7.2-2ubuntu0.1 +ncurses-base==6.4+20240113-1ubuntu2 +ncurses-bin==6.4+20240113-1ubuntu2 +netbase==6.4 +netcat-openbsd==1.226-1ubuntu2 +netplan.io==1.1.2-8ubuntu1~24.04.1 +netplan-generator==1.1.2-8ubuntu1~24.04.1 +networkd-dispatcher==2.2.4-1 +nodejs==22.22.1-1nodesource1 +ocl-icd-libopencl1==2.3.2-1build1 +openjdk-21-jre-headless==21.0.10+7-1~24.04 +openssh-client==1:9.6p1-3ubuntu13.14 +openssl==3.0.13-0ubuntu3.7 +packagekit==1.2.8-2ubuntu1.4 +packagekit-tools==1.2.8-2ubuntu1.4 +pandoc==3.1.3+ds-2 +pandoc-data==3.1.3-1 +passwd==1:4.13+dfsg1-4ubuntu3.2 +pastebinit==1.6.2-1 +patch==2.7.6-7build3 +pci.ids==0.0~2024.03.31-1ubuntu0.1 +pdftk-java==3.3.3-2 +perl==5.38.2-3.2ubuntu0.2 +perl-base==5.38.2-3.2ubuntu0.2 +perl-modules-5.38==5.38.2-3.2ubuntu0.2 +pinentry-curses==1.2.1-3ubuntu5 +pipx==1.4.3-1 +pkgconf==1.8.1-2build1 +pkgconf-bin==1.8.1-2build1 +pkg-config==1.8.1-2build1 +polkitd==124-2ubuntu1.24.04.2 +poppler-data==0.4.12-1 +poppler-utils==24.02.0-1ubuntu9.8 +preview-latex-style==13.2-1 +procps==2:4.0.4-4ubuntu3.2 +psmisc==23.7-1build1 +publicsuffix==20231001.0357-0.1 +python3==3.12.3-0ubuntu2.1 +python3.12==3.12.3-1ubuntu0.12 +python3.12-dev==3.12.3-1ubuntu0.12 +python3.12-minimal==3.12.3-1ubuntu0.12 +python3.12-venv==3.12.3-1ubuntu0.12 +python3-apport==2.28.1-0ubuntu3.8 +python3-apt==2.7.7ubuntu5.2 +python3-argcomplete==3.1.4-1ubuntu0.1 +python3-attr==23.2.0-2 +python3-automat==22.10.0-2 +python3-babel==2.10.3-3build1 +python3-bcrypt==3.2.2-1build1 +python3-blinker==1.7.0-1 +python3-certifi==2023.11.17-1 +python3-cffi-backend==1.16.0-2build1 +python3-chardet==5.2.0+dfsg-1 +python3-click==8.1.6-2 +python3-colorama==0.4.6-4 +python3-commandnotfound==23.04.0 +python3-configobj==5.0.8-3 +python3-constantly==23.10.4-1 +python3-cryptography==41.0.7-4ubuntu0.1 +python3-dbus==1.3.2-5build3 +python3-debconf==1.5.86ubuntu1 +python3-dev==3.12.3-0ubuntu2.1 +python3-distro==1.9.0-1 +python3-distro-info==1.7build1 +python3-distupgrade==1:24.04.28 +python3-gdbm==3.12.3-0ubuntu1 +python3-gi==3.48.2-1 +python3-hamcrest==2.1.0-1 +python3-httplib2==0.20.4-3 +python3-hyperlink==21.0.0-5 +python3-idna==3.6-2ubuntu0.1 +python3-incremental==22.10.0-1 +python3-jinja2==3.1.2-1ubuntu1.3 +python3-jsonpatch==1.32-3 +python3-json-pointer==2.0-0ubuntu1 +python3-jsonschema==4.10.3-2ubuntu1 +python3-jwt==2.7.0-1 +python3-launchpadlib==1.11.0-6 +python3-lazr.restfulclient==0.14.6-1 +python3-lazr.uri==1.0.6-3 +python3-markdown-it==3.0.0-2 +python3-markupsafe==2.1.5-1build2 +python3-mdurl==0.1.2-1 +python3-minimal==3.12.3-0ubuntu2.1 +python3-netifaces==0.11.0-2build3 +python3-netplan==1.1.2-8ubuntu1~24.04.1 +python3-newt==0.52.24-2ubuntu2 +python3-oauthlib==3.2.2-1 +python3-openssl==23.2.0-1 +python3-packaging==24.0-1 +python3-pip==24.0+dfsg-1ubuntu1.3 +python3-pip-whl==24.0+dfsg-1ubuntu1.3 +python3-pkg-resources==68.1.2-2ubuntu1.2 +python3-platformdirs==4.2.0-1 +python3-problem-report==2.28.1-0ubuntu3.8 +python3-pyasn1==0.4.8-4ubuntu0.1 +python3-pyasn1-modules==0.2.8-1 +python3-pycurl==7.45.3-1build2 +python3-pygments==2.17.2+dfsg-1 +python3-pyparsing==3.1.1-1 +python3-pyrsistent==0.20.0-1build2 +python3-requests==2.31.0+dfsg-1ubuntu1.1 +python3-rich==13.7.1-1 +python3-serial==3.5-2 +python3-service-identity==24.1.0-1 +python3-setuptools==68.1.2-2ubuntu1.2 +python3-setuptools-whl==68.1.2-2ubuntu1.2 +python3-six==1.16.0-4 +python3-software-properties==0.99.49.4 +python3-systemd==235-1build4 +python3-twisted==24.3.0-1ubuntu0.1 +python3-typing-extensions==4.10.0-1 +python3-tz==2024.1-2 +python3-update-manager==1:24.04.12 +python3-urllib3==2.0.7-1ubuntu0.6 +python3-userpath==1.9.1-1 +python3-venv==3.12.3-0ubuntu2.1 +python3-wadllib==1.3.6-5 +python3-wheel==0.42.0-2 +python3-yaml==6.0.1-2build2 +python3-zope.interface==6.1-1build1 +python-apt-common==2.7.7ubuntu5.2 +python-babel-localedata==2.10.3-3build1 +qpdf==11.9.0-1.1ubuntu0.1 +readline-common==8.2-4build1 +ripgrep==14.1.0-1 +rpcsvc-proto==1.4.2-0ubuntu7 +rsync==3.2.7-1ubuntu1.2 +rsyslog==8.2312.0-3ubuntu9.1 +run-one==1.17-0ubuntu2 +sed==4.9-2build1 +sensible-utils==0.0.22 +session-migration==0.3.9build1 +sgml-base==1.31 +shared-mime-info==2.4-4 +show-motd==3.12 +snapd==2.73+ubuntu24.04 +software-properties-common==0.99.49.4 +sqlite3==3.45.1-1ubuntu2.5 +squashfs-tools==1:4.6.1-1build1 +sudo==1.9.15p5-3ubuntu5.24.04.1 +systemd==255.4-1ubuntu8.12 +systemd-dev==255.4-1ubuntu8.12 +systemd-hwe-hwdb==255.1.6 +systemd-resolved==255.4-1ubuntu8.12 +systemd-sysv==255.4-1ubuntu8.12 +systemd-timesyncd==255.4-1ubuntu8.12 +sysvinit-utils==3.08-6ubuntu3 +t1utils==1.41-4build3 +tar==1.35+dfsg-3build1 +teckit==2.5.12+ds1-1 +tesseract-ocr==5.3.4-1build5 +tesseract-ocr-eng==1:4.1.0-2 +tesseract-ocr-osd==1:4.1.0-2 +tex-common==6.18 +texlive-base==2023.20240207-1 +texlive-binaries==2023.20230311.66589-9build3 +texlive-fonts-recommended==2023.20240207-1 +texlive-lang-greek==2023.20240207-1 +texlive-latex-base==2023.20240207-1 +texlive-latex-extra==2023.20240207-1 +texlive-latex-recommended==2023.20240207-1 +texlive-pictures==2023.20240207-1 +texlive-science==2023.20240207-1 +texlive-xetex==2023.20240207-1 +time==1.9-0.2build1 +tipa==2:1.3-21 +tmux==3.4-1ubuntu0.1 +tree==2.1.1-2ubuntu3.24.04.2 +tzdata==2025b-0ubuntu0.24.04.1 +ubuntu-keyring==2023.11.28.1 +ubuntu-minimal==1.539.2 +ubuntu-mono==24.04-0ubuntu1 +ubuntu-pro-client==37.1ubuntu0~24.04 +ubuntu-pro-client-l10n==37.1ubuntu0~24.04 +ubuntu-release-upgrader-core==1:24.04.28 +ubuntu-wsl==1.539.2 +ucf==3.0043+nmu1 +udev==255.4-1ubuntu8.12 +unattended-upgrades==2.9.1+nmu4ubuntu1 +uno-libs-private==4:24.2.7-0ubuntu0.24.04.4 +unzip==6.0-28ubuntu4.1 +update-manager-core==1:24.04.12 +update-motd==3.12 +ure==4:24.2.7-0ubuntu0.24.04.4 +ure-java==4:24.2.7-0ubuntu0.24.04.4 +usb.ids==2024.03.18-1 +util-linux==2.39.3-9ubuntu6.5 +uuid-dev==2.39.3-9ubuntu6.5 +uuid-runtime==2.39.3-9ubuntu6.5 +vim==2:9.1.0016-1ubuntu7.9 +vim-common==2:9.1.0016-1ubuntu7.9 +vim-runtime==2:9.1.0016-1ubuntu7.9 +vim-tiny==2:9.1.0016-1ubuntu7.9 +wget==1.21.4-1ubuntu4.1 +whiptail==0.52.24-2ubuntu2 +wkhtmltopdf==0.12.6-2build2 +wsl-pro-service==0.1.18~24.04.3 +wsl-setup==0.5.10~24.04.1 +x11-common==1:7.7+23ubuntu3 +x11proto-core-dev==2023.2-1 +x11proto-dev==2023.2-1 +x11-xkb-utils==7.7+8build2 +xauth==1:1.1.2-1build1 +xdg-user-dirs==0.18-1build1 +xdg-utils==1.1.3-4.1ubuntu3 +xfonts-cyrillic==1:1.0.5+nmu1 +xfonts-encodings==1:1.0.5-0ubuntu2 +xfonts-scalable==1:1.0.3-1.3 +xfonts-utils==1:7.7+6build3 +xkb-data==2.41-2ubuntu1.1 +xml-core==0.19 +xorg-sgml-doctools==1:1.11-1.1 +xserver-common==2:21.1.12-1ubuntu1.5 +xtrans-dev==1.4.0-1 +xvfb==2:21.1.12-1ubuntu1.5 +xxd==2:9.1.0016-1ubuntu7.9 +xz-utils==5.6.1+really5.4.5-1ubuntu0.2 +zip==3.0-13ubuntu0.2 +zlib1g==1:1.3.dfsg-3.1ubuntu2.1 +zlib1g-dev==1:1.3.dfsg-3.1ubuntu2.1 diff --git a/reports/vm-prebuilt-inventory-20260325/new_dpkg_status.txt b/reports/vm-prebuilt-inventory-20260325/new_dpkg_status.txt new file mode 100644 index 0000000000000000000000000000000000000000..b7fabee7468d6bde3d14a0d6af39f24a00544d70 GIT binary patch literal 769308 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

A!?R&p9c%vzMJ+lX@5tD@g4bStp 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 ZeO`8G{IO%;8A$B1EyyYAja$#c#7vD| Oxp+D?-C1t&ct3t)T@eKU literal 0 HcmV?d00001 diff --git a/reports/vm-prebuilt-inventory-20260325/npm-global.txt b/reports/vm-prebuilt-inventory-20260325/npm-global.txt new file mode 100644 index 00000000..2bad233c --- /dev/null +++ b/reports/vm-prebuilt-inventory-20260325/npm-global.txt @@ -0,0 +1,22 @@ +@mermaid-js +corepack +docx +graphviz +markdownlint-cli +markdownlint-cli2 +markdown-toc +marked +npm +pdfjs-dist +pdf-lib +playwright +pptxgenjs +react +react-dom +react-icons +remark-cli +remark-preset-lint-recommended +sharp +ts-node +tsx +typescript diff --git a/reports/vm-prebuilt-inventory-20260325/pip-dist-info.txt b/reports/vm-prebuilt-inventory-20260325/pip-dist-info.txt new file mode 100644 index 00000000..9d0b3642 --- /dev/null +++ b/reports/vm-prebuilt-inventory-20260325/pip-dist-info.txt @@ -0,0 +1,83 @@ +backrefs-6.2 +beautifulsoup4-4.14.3 +blinker-1.9.0 +camelot_py-1.0.9 +charset_normalizer-3.4.6 +click-8.3.1 +contourpy-1.3.3 +cycler-0.12.1 +defusedxml-0.7.1 +deprecated-1.3.1 +docopt-0.6.2 +et_xmlfile-2.0.0 +flask-3.1.3 +flatbuffers-25.12.19 +fonttools-4.62.1 +ghp_import-2.1.0 +grip-4.6.2 +imageio_ffmpeg-0.6.0 +imageio-2.37.3 +img2pdf-0.6.3 +itsdangerous-2.2.0 +joblib-1.5.3 +kiwisolver-1.5.0 +lazy_loader-0.5 +lxml-6.0.2 +magika-0.6.3 +markdown-3.10.2 +markdownify-1.2.2 +markitdown-0.1.5 +marko-2.2.2 +matplotlib-3.10.8 +mergedeep-1.3.4 +mistune-3.2.0 +mkdocs_get_deps-0.2.2 +mkdocs_material_extensions-1.3.1 +mkdocs_material-9.7.6 +mkdocs-1.6.1 +mpmath-1.3.0 +networkx-3.6.1 +numpy-2.4.3 +odfpy-1.4.1 +onnxruntime-1.24.4 +opencv_contrib_python-4.13.0.92 +opencv_python_headless-4.13.0.92 +opencv_python-4.13.0.92 +openpyxl-3.1.5 +paginate-0.5.7 +pandas-3.0.1 +path_and_address-2.0.1 +pathspec-1.0.4 +pdf2image-1.17.0 +pdfkit-1.0.0 +pdfminer_six-20251230 +pdfplumber-0.11.9 +pikepdf-10.5.1 +pillow-12.1.1 +protobuf-7.34.1 +pymdown_extensions-10.21 +pymupdf-1.27.2.2 +pypdf-5.9.0 +pypdfium2-5.6.0 +pytesseract-0.3.13 +python_dateutil-2.9.0.post0 +python_docx-1.2.0 +python_dotenv-1.2.2 +python_pptx-1.0.2 +pyyaml_env_tag-1.1 +reportlab-4.4.10 +scikit_image-0.26.0 +scikit_learn-1.8.0 +scipy-1.17.1 +seaborn-0.13.2 +soupsieve-2.8.3 +sympy-1.14.0 +tabula_py-2.10.0 +tabulate-0.10.0 +threadpoolctl-3.6.0 +tifffile-2026.3.3 +wand-0.7.0 +watchdog-6.0.0 +werkzeug-3.1.7 +wrapt-2.1.2 +xlsxwriter-3.2.9 diff --git a/reports/vm-prebuilt-inventory-20260325/pip-freeze-new.txt b/reports/vm-prebuilt-inventory-20260325/pip-freeze-new.txt new file mode 100644 index 0000000000000000000000000000000000000000..270b48a98e0ead5f965e165752f859afb0e08394 GIT binary patch literal 1246 zcmY*Y!A|2)5c3&nKLtdR7T5y^?jUjHgwk%H5upuf3N0TGYmeU}MbS&7%#7{v%=`Yj z#{mzx!5waK&F2DVIOnPG2Pdd8;2BG7@QNNC8jL*3E0}Y|Q$*f^e{1X*ZSll!hb?P% zteHjjEHd>r?vnXK%5Ed0Bx@P$C9_J*19yp4XxIkxDb_?CsHG-Di_bujnu;|-zerVz zF7Xn`QaN>`Ub;Z314q@0s(O~HH{=;Z|G*9ofuerJQl^{(@#V)5Q6UtJWj~%+EexTE zZyQ-wIAyi?$E-SH!Y-1VFI4}*BM&>g|FB5Ioaa0=bl9_lE~@j?tWN%nF|Q53@?vqs zA+1+|u;zp`S9fUNvxhw~eYoO+_S3VvtcLR7x$eV zQoK)?ib$2!JF-tzj<{nf73)C8Ns~FJ>H~XTk#~*LP^XRcX4)&U{Y{KL^R~>N@Q-Wd zUPGMsA1$iQD#i1eI>@^#cQU4L#z2HO@;AZbJrGJ2+u|+vcI&qM-j{jk^lWt-)%D~{ zfy?N^5#KgtG_f)ATs+!-Ph_NX3o7aO&9~TltG68(!(n^-NThvoK)7;r+T-GA6Xk>U zKn&)UAKFwi?>I5-g7&aV8QQZcE(dhigt3YYIq93L{&r~w)~lG^{a|*($CFjG3RfKc E4;t>Xh5!Hn literal 0 HcmV?d00001 diff --git a/reports/vm-prebuilt-inventory-20260325/rebuild-comparison.md b/reports/vm-prebuilt-inventory-20260325/rebuild-comparison.md new file mode 100644 index 00000000..41620028 --- /dev/null +++ b/reports/vm-prebuilt-inventory-20260325/rebuild-comparison.md @@ -0,0 +1,52 @@ +# openagent-prebuilt Rebuild Comparison (2026-03-25) + +## Summary + +- Old prebuilt tar (before clean rebuild): `6,693,079,040` bytes (`6.23 GB`) +- New prebuilt tar (rebuilt from clean `openagent-build` + current setup scripts): `2,019,061,760` bytes (`1.88 GB`) +- Delta: `-4,674,017,280` bytes (`-4.35 GB`, about `-69.8%`) + +## Hash + +- Old SHA256: `61279AF095540D3C1290BDF8B2BA1F4094BD128C347E873BEF0F9D25A56986D6` +- New SHA256: `8D8F7F8718891C8242DEE44409EA92EB57B912FCBA53F427622C2DE70B92A022` + +## Package Count Changes + +- APT packages: + - old: `964` + - new: `386` + - source files: + - `apt-installed.txt` + - `apt-installed-new.txt` +- Python packages: + - old: `83` (from old dist-info snapshot extraction) + - new: `35` (from `pip list --format=freeze` in rebuilt distro) + - source files: + - `pip-dist-info.txt` + - `pip-freeze-new.txt` +- NPM global packages: + - old: `22` + - new: `5` + - source files: + - `npm-global.txt` + - `npm-global-new.txt` + +## New Prebuilt Size Hotspots + +Top large files (`>50 MB`) in rebuilt tar: + +- `opt/pw-browsers/chromium-1208/chrome-linux64/chrome` (`257.28 MB`) +- `opt/pw-browsers/chromium_headless_shell-1208/chrome-headless-shell-linux64/chrome-headless-shell` (`175.05 MB`) +- `usr/bin/node` (`118.90 MB`) +- `usr/lib/x86_64-linux-gnu/libLLVM-15.so.1` (`111.46 MB`) +- `usr/local/bin/uv` (`56.33 MB`) + +## Notes + +- During the first rebuild attempt, `05_pip.sh` did not fail even when pip installs failed. +- Root causes fixed in source: + - `setup.sh`: `pip_install` now conditionally adds `--break-system-packages` only when supported. + - `05_pip.sh`: now uses `set -euo pipefail` to fail fast on pip errors. + - `03_apt.sh`: each `apt_install` call now hard-fails on error (`|| exit 1`). +- After fixes, rebuild completed and dependencies were actually installed. From 3f1a6f9cfa83d53165fc059787556b2551723657 Mon Sep 17 00:00:00 2001 From: zhanghr136 Date: Fri, 27 Mar 2026 09:38:31 +0800 Subject: [PATCH 13/34] feat: add uninstallation script to clean up user data --- libs/openagent_demo/electron/package.json | 4 +++- libs/openagent_demo/electron/resources/installer.nsh | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 libs/openagent_demo/electron/resources/installer.nsh diff --git a/libs/openagent_demo/electron/package.json b/libs/openagent_demo/electron/package.json index 6dbd8113..6f3e2699 100644 --- a/libs/openagent_demo/electron/package.json +++ b/libs/openagent_demo/electron/package.json @@ -77,7 +77,9 @@ }, "nsis": { "oneClick": false, - "allowToChangeInstallationDirectory": true + "allowToChangeInstallationDirectory": true, + "deleteAppDataOnUninstall": true, + "include": "resources/installer.nsh" } } } diff --git a/libs/openagent_demo/electron/resources/installer.nsh b/libs/openagent_demo/electron/resources/installer.nsh new file mode 100644 index 00000000..7060541b --- /dev/null +++ b/libs/openagent_demo/electron/resources/installer.nsh @@ -0,0 +1,3 @@ +!macro customUnInstall + RMDir /r "$PROFILE\.openagent" +!macroend From d16536e165fcfaa904b0ddd081cdd0b766ad9160 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Thu, 26 Mar 2026 19:18:53 +0800 Subject: [PATCH 14/34] chore(repo): track wsl prebuilt tar with LFS and ignore dist zip --- .gitattributes | 1 + .gitignore | 3 +++ libs/openagent/sandbox/vm/wsl/prebuilt/openagent-prebuilt.tar | 3 +++ 3 files changed, 7 insertions(+) create mode 100644 libs/openagent/sandbox/vm/wsl/prebuilt/openagent-prebuilt.tar diff --git a/.gitattributes b/.gitattributes index 0793bb27..c88eb060 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ libs/openagent/sandbox/vm/setup/*.sh text eol=lf libs/openagent/sandbox/vm/setup/steps/*.sh text eol=lf +libs/openagent/sandbox/vm/wsl/prebuilt/openagent-prebuilt.tar filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 02e7f45a..ff18f0b1 100644 --- a/.gitignore +++ b/.gitignore @@ -220,3 +220,6 @@ mounts/ # Git worktrees .trees/ + +# Electron build artifact +libs/openagent_demo/electron/dist.zip diff --git a/libs/openagent/sandbox/vm/wsl/prebuilt/openagent-prebuilt.tar b/libs/openagent/sandbox/vm/wsl/prebuilt/openagent-prebuilt.tar new file mode 100644 index 00000000..717a5a6d --- /dev/null +++ b/libs/openagent/sandbox/vm/wsl/prebuilt/openagent-prebuilt.tar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb39247403141d4bd20516ce9fbb25d5956a11f838e577959a7bed4bc6514a79 +size 2019061760 From 5c0c87838a2e02e8b56a890c5bd2c49cf99fc518 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Fri, 27 Mar 2026 14:15:50 +0800 Subject: [PATCH 15/34] fix(windows-vm): improve onboarding recovery and setup status --- .gitattributes | 2 + .../hexagent/computer/local/vm_win.py | 2 +- .../backend/hexagent_api/agent_manager.py | 4 +- .../backend/hexagent_api/routes/setup.py | 6 +- .../src/components/OnboardingWizard.tsx | 136 +++++++++++++++++- .../frontend/src/components/SettingsModal.tsx | 23 ++- libs/hexagent_demo/frontend/src/vmSetup.tsx | 12 ++ 7 files changed, 166 insertions(+), 19 deletions(-) diff --git a/.gitattributes b/.gitattributes index c88eb060..c3482579 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ libs/openagent/sandbox/vm/setup/*.sh text eol=lf libs/openagent/sandbox/vm/setup/steps/*.sh text eol=lf +libs/hexagent/sandbox/vm/setup/*.sh text eol=lf +libs/hexagent/sandbox/vm/setup/steps/*.sh text eol=lf libs/openagent/sandbox/vm/wsl/prebuilt/openagent-prebuilt.tar filter=lfs diff=lfs merge=lfs -text diff --git a/libs/hexagent/hexagent/computer/local/vm_win.py b/libs/hexagent/hexagent/computer/local/vm_win.py index 9ea9bb37..58fc0acb 100644 --- a/libs/hexagent/hexagent/computer/local/vm_win.py +++ b/libs/hexagent/hexagent/computer/local/vm_win.py @@ -336,7 +336,7 @@ async def mount( for r in resolved_new: probe = await self._vm.shell(f"findmnt -n {shlex.quote(r.guest_path)}") if probe.exit_code != 0: - from openagent.computer.local._wsl import _session_user_from_guest_mount_path, _win_path_to_wsl + from hexagent.computer.local._wsl import _session_user_from_guest_mount_path, _win_path_to_wsl wsl_host = _win_path_to_wsl(r.host_path) qguest = shlex.quote(r.guest_path) diff --git a/libs/hexagent_demo/backend/hexagent_api/agent_manager.py b/libs/hexagent_demo/backend/hexagent_api/agent_manager.py index 50dc7d59..20a3164e 100644 --- a/libs/hexagent_demo/backend/hexagent_api/agent_manager.py +++ b/libs/hexagent_demo/backend/hexagent_api/agent_manager.py @@ -152,7 +152,7 @@ async def _ensure_computer( import shutil if sys.platform == "win32": - from openagent.computer.local._wsl import _resolve_wsl_exe + from hexagent.computer.local._wsl import _resolve_wsl_exe vm_backend_ready = _resolve_wsl_exe() is not None else: @@ -469,7 +469,7 @@ def _schedule_vm_warmup(self) -> None: import shutil if sys.platform == "win32": - from openagent.computer.local._wsl import _resolve_wsl_exe + from hexagent.computer.local._wsl import _resolve_wsl_exe vm_backend_available = _resolve_wsl_exe() is not None else: diff --git a/libs/hexagent_demo/backend/hexagent_api/routes/setup.py b/libs/hexagent_demo/backend/hexagent_api/routes/setup.py index 9f800fc9..fbddd5d4 100644 --- a/libs/hexagent_demo/backend/hexagent_api/routes/setup.py +++ b/libs/hexagent_demo/backend/hexagent_api/routes/setup.py @@ -1419,11 +1419,13 @@ async def check_markers(self) -> dict[str, object]: if sys.platform == "win32": instance_status = await _wsl_instance_status() shell = lambda cmd: _wsl_shell(cmd, user="root") + if not _wsl_distro_ready_for_cowork(instance_status): + return {"provisioned": False, "steps_done": [], "total_steps": len(_PROVISION_STEPS)} else: instance_status = await _lima_instance_status() shell = _lima_shell - if instance_status != "Running": - return {"provisioned": False, "steps_done": [], "total_steps": len(_PROVISION_STEPS)} + if instance_status != "Running": + return {"provisioned": False, "steps_done": [], "total_steps": len(_PROVISION_STEPS)} rc, stdout, _ = await shell(f"ls {_SETUP_MARKER_DIR}/*.done 2>/dev/null || true") if rc != 0 or not stdout.strip(): diff --git a/libs/hexagent_demo/frontend/src/components/OnboardingWizard.tsx b/libs/hexagent_demo/frontend/src/components/OnboardingWizard.tsx index 60e2fd6a..94298b13 100644 --- a/libs/hexagent_demo/frontend/src/components/OnboardingWizard.tsx +++ b/libs/hexagent_demo/frontend/src/components/OnboardingWizard.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import faviconSvg from "../assets/favicon.svg"; import { Eye, EyeOff, ArrowRight, ChevronDown, ChevronRight, @@ -84,10 +84,52 @@ function stepIndex(s: Step): number { return STEPS.indexOf(s); } +const ONBOARDING_DRAFT_KEY = "hexagent-onboarding-draft-v1"; + +interface OnboardingDraft { + step?: Step; + selectedProviderId?: string; + apiKey?: string; + modelId?: string; + displayName?: string; + baseUrl?: string; + sumProviderId?: string; + sumApiKey?: string; + sumModelId?: string; + sumDisplayName?: string; + sumBaseUrl?: string; + sumSameAsMain?: boolean; + searchProvider?: string; + searchKey?: string; + fetchProvider?: string; + fetchKey?: string; + e2bKey?: string; + vmSkipped?: boolean; +} + +function loadOnboardingDraft(): OnboardingDraft | null { + try { + const raw = localStorage.getItem(ONBOARDING_DRAFT_KEY); + if (!raw) return null; + return JSON.parse(raw) as OnboardingDraft; + } catch { + return null; + } +} + +function saveOnboardingDraft(draft: OnboardingDraft): void { + localStorage.setItem(ONBOARDING_DRAFT_KEY, JSON.stringify(draft)); +} + +function clearOnboardingDraft(): void { + localStorage.removeItem(ONBOARDING_DRAFT_KEY); +} + // --------------------------------------------------------------------------- export default function OnboardingWizard({ open, onComplete, settings, onSettingsChange }: OnboardingWizardProps) { const { dispatch } = useAppContext(); + const draftReadyRef = useRef(false); const [step, setStep] = useState("welcome"); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); @@ -126,6 +168,7 @@ export default function OnboardingWizard({ open, onComplete, settings, onSetting // VM setup — shared with Settings via VMSetupProvider (single source of truth) const vm = useVMSetup(); + const vmAutoBootstrapping = vm.autoBootstrapping; const [vmSkipped, setVmSkipped] = useState(false); const [showSkipConfirm, setShowSkipConfirm] = useState(false); const [showDepsPrompt, setShowDepsPrompt] = useState(false); @@ -141,14 +184,84 @@ export default function OnboardingWizard({ open, onComplete, settings, onSetting const vmUsable = vmPhase1 === "done" && vmPhase2 === "done"; const vmPhase1NeedsRestart = /restart windows|重启.*windows|重启.*电脑|reboot/i.test(vmPhase1Error || ""); - // Load server config and reset name on open + // Load server config and restore onboarding draft on open useEffect(() => { - if (open) { - getServerConfig().then(setConfig).catch(() => {}); - onSettingsChange((prev) => ({ ...prev, fullName: "" })); + if (!open) { + draftReadyRef.current = false; + return; } + + getServerConfig().then(setConfig).catch(() => {}); + + const draft = loadOnboardingDraft(); + if (draft) { + if (draft.step && STEPS.includes(draft.step)) setStep(draft.step); + if (draft.selectedProviderId) setSelectedProvider(PROVIDERS.find((p) => p.id === draft.selectedProviderId) ?? null); + if (typeof draft.apiKey === "string") setApiKey(draft.apiKey); + if (typeof draft.modelId === "string") setModelId(draft.modelId); + if (typeof draft.displayName === "string") setDisplayName(draft.displayName); + if (typeof draft.baseUrl === "string") setBaseUrl(draft.baseUrl); + if (draft.sumProviderId) setSumProvider(PROVIDERS.find((p) => p.id === draft.sumProviderId) ?? null); + if (typeof draft.sumApiKey === "string") setSumApiKey(draft.sumApiKey); + if (typeof draft.sumModelId === "string") setSumModelId(draft.sumModelId); + if (typeof draft.sumDisplayName === "string") setSumDisplayName(draft.sumDisplayName); + if (typeof draft.sumBaseUrl === "string") setSumBaseUrl(draft.sumBaseUrl); + if (typeof draft.sumSameAsMain === "boolean") setSumSameAsMain(draft.sumSameAsMain); + if (typeof draft.searchProvider === "string") setSearchProvider(draft.searchProvider); + if (typeof draft.searchKey === "string") setSearchKey(draft.searchKey); + if (typeof draft.fetchProvider === "string") setFetchProvider(draft.fetchProvider); + if (typeof draft.fetchKey === "string") setFetchKey(draft.fetchKey); + if (typeof draft.e2bKey === "string") setE2bKey(draft.e2bKey); + if (typeof draft.vmSkipped === "boolean") setVmSkipped(draft.vmSkipped); + } + + draftReadyRef.current = true; }, [open]); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { + if (!open || !draftReadyRef.current) return; + saveOnboardingDraft({ + step, + selectedProviderId: selectedProvider?.id, + apiKey, + modelId, + displayName, + baseUrl, + sumProviderId: sumProvider?.id, + sumApiKey, + sumModelId, + sumDisplayName, + sumBaseUrl, + sumSameAsMain, + searchProvider, + searchKey, + fetchProvider, + fetchKey, + e2bKey, + vmSkipped, + }); + }, [ + open, + step, + selectedProvider, + apiKey, + modelId, + displayName, + baseUrl, + sumProvider, + sumApiKey, + sumModelId, + sumDisplayName, + sumBaseUrl, + sumSameAsMain, + searchProvider, + searchKey, + fetchProvider, + fetchKey, + e2bKey, + vmSkipped, + ]); + if (!open) return null; // ── Navigation ── @@ -226,6 +339,7 @@ export default function OnboardingWizard({ open, onComplete, settings, onSetting const saved = await updateServerConfig(updated); dispatch({ type: "SET_SERVER_CONFIG", payload: saved }); + clearOnboardingDraft(); onComplete(); } catch (e: unknown) { setError(e instanceof Error ? e.message : "Failed to save configuration"); @@ -820,7 +934,11 @@ export default function OnboardingWizard({ open, onComplete, settings, onSetting {vmPhase1 === "done" && Installed} {vmPhase1 === "running" && vmPhase1Msg && {vmPhase1Msg}} {vmPhase1 === "pending" && ( - + vmAutoBootstrapping ? ( + Auto installing... + ) : ( + + ) )} {vmPhase1 === "error" && ( vmPhase1NeedsRestart ? ( @@ -848,7 +966,11 @@ export default function OnboardingWizard({ open, onComplete, settings, onSetting {vmPhase2 === "done" && Ready} {vmPhase2 === "running" && vmPhase2Msg && {vmPhase2Msg}} {vmPhase2 === "pending" && vmPhase1 === "done" && ( - + vmAutoBootstrapping ? ( + Auto installing... + ) : ( + + ) )} {vmPhase2 === "error" && ( diff --git a/libs/hexagent_demo/frontend/src/components/SettingsModal.tsx b/libs/hexagent_demo/frontend/src/components/SettingsModal.tsx index c2009b23..9ab9392e 100644 --- a/libs/hexagent_demo/frontend/src/components/SettingsModal.tsx +++ b/libs/hexagent_demo/frontend/src/components/SettingsModal.tsx @@ -1687,6 +1687,7 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { const { vmStatus, + autoBootstrapping, phase1, phase1Msg, phase1Error, @@ -1733,9 +1734,9 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { } }; - // Cowork mode should follow backend truth from /api/setup/vm (vm_ready). - // Keep phase fallback only when status payload is unavailable. - const vmUsable = vmStatus?.vm_ready ?? (phase1 === "done" && phase2 === "done"); + // Cowork mode should prefer backend truth from /api/setup/vm (vm_ready), + // but keep UI-consistent fallback when phase1+phase2 are already done. + const vmUsable = (vmStatus?.vm_ready === true) || (phase1 === "done" && phase2 === "done"); const allDone = vmUsable && phase3 === "done"; const anyRunning = phase1 === "running" || phase2 === "running" || phase3 === "running"; const coreError = phase1 === "error" || phase2 === "error"; @@ -1838,9 +1839,13 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { {phase1 === "done" && Installed} {phase1 === "running" && phase1Msg && {phase1Msg}} {phase1 === "pending" && ( - + autoBootstrapping ? ( + Auto installing... + ) : ( + + ) )} {phase1 === "error" && ( phase1NeedsRestart ? ( @@ -1890,7 +1895,11 @@ function SandboxTab({ config, onConfigChange }: ConfigTabProps) { {phase2 === "done" && Ready} {phase2 === "running" && phase2Msg && {phase2Msg}} {phase2 === "pending" && phase1 === "done" && ( - + autoBootstrapping ? ( + Auto installing... + ) : ( + + ) )} {phase2 === "error" && ( diff --git a/libs/hexagent_demo/frontend/src/vmSetup.tsx b/libs/hexagent_demo/frontend/src/vmSetup.tsx index 742bac12..ea3797c6 100644 --- a/libs/hexagent_demo/frontend/src/vmSetup.tsx +++ b/libs/hexagent_demo/frontend/src/vmSetup.tsx @@ -50,6 +50,7 @@ export type PhaseStatus = "checking" | "pending" | "running" | "done" | "error"; export interface VMSetupContextValue { vmStatus: VMStatus | null; + autoBootstrapping: boolean; phase1: PhaseStatus; phase1Msg: string; @@ -108,6 +109,7 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { const [provStepMsg, setProvStepMsg] = useState>({}); const [provLog, setProvLog] = useState(null); const autoBootstrapTriggeredRef = useRef(false); + const [autoBootstrapping, setAutoBootstrapping] = useState(false); // SSE abort controllers (kept alive across renders, never aborted on unmount) const installCtrl = useRef(null); @@ -527,6 +529,7 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { if (phase1 === "pending") { autoBootstrapTriggeredRef.current = true; + setAutoBootstrapping(true); notify("Detected first-time Windows setup. Starting VM runtime install automatically...", "info"); void doInstallLima(); return; @@ -534,13 +537,22 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { if (phase1 === "done" && phase2 === "pending") { autoBootstrapTriggeredRef.current = true; + setAutoBootstrapping(true); notify("Runtime is ready. Starting VM instance setup automatically...", "info"); attachBuild(); } }, [vmStatus, phase1, phase2]); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { + if (!autoBootstrapping) return; + if (phase2 === "done" || phase1 === "error" || phase2 === "error") { + setAutoBootstrapping(false); + } + }, [autoBootstrapping, phase1, phase2]); + const value: VMSetupContextValue = { vmStatus, + autoBootstrapping, phase1, phase1Msg, phase1Error, phase2, phase2Msg, phase2Error, phase3, phase3Error, From 077da7dfba5328a5120da7ccc1b3be6ee85ea61c Mon Sep 17 00:00:00 2001 From: "Anqi (Anthony) Tang" Date: Fri, 27 Mar 2026 16:47:12 +0800 Subject: [PATCH 16/34] review(pr13): address admin feedback, add setup_lite, fix CI failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admin feedback: - Revert harness/environment.py: restore ValueError on empty datetime; graceful fallback was rejected — failure must surface, not be hidden - Revert langchain/middleware.py: remove mid-conversation working-directory refresh; cwd is intentionally immutable after conversation start - Restore sandbox/vm/setup/ to pre-PR state; full package list must not be overwritten by this branch - Add sandbox/vm/setup_lite/ as a lite variant for demo/Electron deployments: trimmed APT/npm/pip baselines, bundled-browser detection, China-mirror support - Add vm_setup_lite_dir() to paths.py; point Lima and WSL provisioners in routes/setup.py at setup_lite/ (fix Lima tar to derive folder name dynamically instead of hardcoding "setup") - Fix .gitignore: correct stale openagent_demo reference, ignore .vite/ cache, WSL prebuilt tars, and reports/ directory CI fixes: - Fix ruff format: collapse multi-line f-string shell commands in vm_win.py; add missing blank line in test_vm.py - Fix ruff lint in _wsl.py: SYSTEMROOT capitalisation (SIM112), Path.cwd() over os.getcwd() (PTH109), noqa for assert (S101) and magic number (PLR2004); noqa PLR0912 on mount() in vm_win.py - Fix mypy method-assign errors in test_wsl.py - Fix test_mount_idempotent_self_heals_missing_live_mount: patch _win_path_to_wsl so the test runs on Linux CI where tmp_path is not a Windows drive-letter path --- .gitignore | 11 +- libs/hexagent/hexagent/computer/local/_wsl.py | 10 +- .../hexagent/computer/local/vm_win.py | 12 +- libs/hexagent/hexagent/harness/environment.py | 16 +- .../hexagent/hexagent/langchain/middleware.py | 23 - libs/hexagent/sandbox/vm/setup/setup.sh | 144 +-- .../hexagent/sandbox/vm/setup/steps/03_apt.sh | 68 +- .../hexagent/sandbox/vm/setup/steps/04_npm.sh | 34 +- .../hexagent/sandbox/vm/setup/steps/05_pip.sh | 64 +- .../sandbox/vm/setup/steps/06_playwright.sh | 49 +- libs/hexagent/sandbox/vm/setup_lite/setup.sh | 458 +++++++++ .../sandbox/vm/setup_lite/steps/01_base.sh | 9 + .../sandbox/vm/setup_lite/steps/02_nodejs.sh | 60 ++ .../sandbox/vm/setup_lite/steps/03_apt.sh | 41 + .../sandbox/vm/setup_lite/steps/04_npm.sh | 11 + .../sandbox/vm/setup_lite/steps/05_pip.sh | 18 + .../vm/setup_lite/steps/06_playwright.sh | 78 ++ .../vm/setup_lite/steps/07_finalize.sh | 10 + .../sandbox/vm/setup_lite/steps/08_cleanup.sh | 10 + .../unit_tests/computer/test_local_vm_win.py | 3 +- .../tests/unit_tests/computer/test_vm.py | 1 + .../tests/unit_tests/computer/test_wsl.py | 4 +- .../unit_tests/harness/test_environment.py | 2 - .../backend/hexagent_api/paths.py | 10 + .../backend/hexagent_api/routes/setup.py | 8 +- .../vm/wsl/prebuilt/openagent-prebuilt.tar | 3 - .../apt-installed-new.txt | Bin 3090 -> 0 bytes .../apt-installed.txt | 964 ------------------ .../new_dpkg_status.txt | Bin 769308 -> 0 bytes .../npm-global-new.txt | Bin 90 -> 0 bytes .../npm-global.txt | 22 - .../pip-dist-info.txt | 83 -- .../pip-freeze-new.txt | Bin 1246 -> 0 bytes .../rebuild-comparison.md | 52 - 34 files changed, 907 insertions(+), 1371 deletions(-) create mode 100755 libs/hexagent/sandbox/vm/setup_lite/setup.sh create mode 100755 libs/hexagent/sandbox/vm/setup_lite/steps/01_base.sh create mode 100755 libs/hexagent/sandbox/vm/setup_lite/steps/02_nodejs.sh create mode 100755 libs/hexagent/sandbox/vm/setup_lite/steps/03_apt.sh create mode 100755 libs/hexagent/sandbox/vm/setup_lite/steps/04_npm.sh create mode 100755 libs/hexagent/sandbox/vm/setup_lite/steps/05_pip.sh create mode 100755 libs/hexagent/sandbox/vm/setup_lite/steps/06_playwright.sh create mode 100755 libs/hexagent/sandbox/vm/setup_lite/steps/07_finalize.sh create mode 100755 libs/hexagent/sandbox/vm/setup_lite/steps/08_cleanup.sh delete mode 100644 libs/openagent/sandbox/vm/wsl/prebuilt/openagent-prebuilt.tar delete mode 100644 reports/vm-prebuilt-inventory-20260325/apt-installed-new.txt delete mode 100644 reports/vm-prebuilt-inventory-20260325/apt-installed.txt delete mode 100644 reports/vm-prebuilt-inventory-20260325/new_dpkg_status.txt delete mode 100644 reports/vm-prebuilt-inventory-20260325/npm-global-new.txt delete mode 100644 reports/vm-prebuilt-inventory-20260325/npm-global.txt delete mode 100644 reports/vm-prebuilt-inventory-20260325/pip-dist-info.txt delete mode 100644 reports/vm-prebuilt-inventory-20260325/pip-freeze-new.txt delete mode 100644 reports/vm-prebuilt-inventory-20260325/rebuild-comparison.md diff --git a/.gitignore b/.gitignore index 6aa7f618..6c9f2a64 100644 --- a/.gitignore +++ b/.gitignore @@ -222,4 +222,13 @@ mounts/ .trees/ # Electron build artifact -libs/openagent_demo/electron/dist.zip +libs/hexagent_demo/electron/dist.zip + +# Vite dev-server cache +**/.vite/ + +# WSL prebuilt VM image (build artifact — generate with prepare-wsl-prebuilt.ps1) +**/wsl/prebuilt/*.tar + +# One-off investigation/diagnostic reports +reports/ diff --git a/libs/hexagent/hexagent/computer/local/_wsl.py b/libs/hexagent/hexagent/computer/local/_wsl.py index 214a870a..7a6aa956 100644 --- a/libs/hexagent/hexagent/computer/local/_wsl.py +++ b/libs/hexagent/hexagent/computer/local/_wsl.py @@ -54,7 +54,7 @@ def _resolve_wsl_exe() -> str | None: w = shutil.which("wsl.exe") or shutil.which("wsl") if w: return w - system_root = os.environ.get("SystemRoot") or os.environ.get("WINDIR") + system_root = os.environ.get("SYSTEMROOT") or os.environ.get("WINDIR") if not system_root: system_root = r"C:\Windows" candidate = Path(system_root) / "System32" / "wsl.exe" @@ -72,13 +72,13 @@ def _stable_host_cwd() -> str: unexpected context. Force a stable host cwd to avoid inheriting stale per-session paths. """ - system_root = os.environ.get("SystemRoot") or os.environ.get("WINDIR") or r"C:\Windows" + system_root = os.environ.get("SYSTEMROOT") or os.environ.get("WINDIR") or r"C:\Windows" # ``wsl.exe`` exists under System32 on supported hosts; use that directory # as a stable cwd if available, otherwise fall back to the process cwd. safe_dir = Path(system_root) / "System32" if safe_dir.is_dir(): return str(safe_dir) - return os.getcwd() + return str(Path.cwd()) def _ensure_proactor_event_loop() -> None: @@ -136,7 +136,7 @@ class WslVM: def __init__(self, instance: str) -> None: _check_wsl_prerequisites() wsl_exe = _resolve_wsl_exe() - assert wsl_exe is not None + assert wsl_exe is not None # noqa: S101 self._wsl_exe = wsl_exe self._instance = instance self._unc_prefix: str | None = None # cached after first successful probe @@ -698,7 +698,7 @@ def _session_user_from_guest_mount_path(guest_path: str) -> str | None: after ``sessions`` matches the Linux account created for that sandbox session. """ parts = guest_path.split("/") - if len(parts) >= 3 and parts[1] == "sessions" and parts[2]: + if len(parts) >= 3 and parts[1] == "sessions" and parts[2]: # noqa: PLR2004 return parts[2] return None diff --git a/libs/hexagent/hexagent/computer/local/vm_win.py b/libs/hexagent/hexagent/computer/local/vm_win.py index 58fc0acb..890433a2 100644 --- a/libs/hexagent/hexagent/computer/local/vm_win.py +++ b/libs/hexagent/hexagent/computer/local/vm_win.py @@ -281,7 +281,7 @@ async def stop(self) -> None: return await self._vm.stop() - async def mount( + async def mount( # noqa: PLR0912 self, mounts: Mount | list[Mount], *, @@ -605,19 +605,13 @@ async def _create_user(self, name: str) -> None: sudo_probe = await self._vm.shell("command -v sudo >/dev/null 2>&1") sudo_prefix = "sudo " if sudo_probe.exit_code == 0 else "" - create_cmd = ( - f"{sudo_prefix}useradd -m -d {qhome} -s /bin/bash " - f"--no-log-init -K SUB_UID_COUNT=0 -K SUB_GID_COUNT=0 {qname}" - ) + create_cmd = f"{sudo_prefix}useradd -m -d {qhome} -s /bin/bash --no-log-init -K SUB_UID_COUNT=0 -K SUB_GID_COUNT=0 {qname}" result = await self._vm.shell(create_cmd) if result.exit_code != 0: # Some Ubuntu/WSL images reject useradd with SUB_UID/GID_COUNT=0. # Retry without those overrides for compatibility. We retry # regardless of locale-specific stderr text. - fallback_cmd = ( - f"{sudo_prefix}useradd -m -d {qhome} -s /bin/bash " - f"--no-log-init {qname}" - ) + fallback_cmd = f"{sudo_prefix}useradd -m -d {qhome} -s /bin/bash --no-log-init {qname}" result = await self._vm.shell(fallback_cmd) if result.exit_code != 0: msg = f"Failed to create session user '{name}': {result.stderr}" diff --git a/libs/hexagent/hexagent/harness/environment.py b/libs/hexagent/hexagent/harness/environment.py index e6861289..27ff1efb 100644 --- a/libs/hexagent/hexagent/harness/environment.py +++ b/libs/hexagent/hexagent/harness/environment.py @@ -72,15 +72,13 @@ async def resolve(self) -> EnvironmentContext: # Parse into a timezone-aware datetime. # Shell outputs ISO 8601 with numeric offset, e.g. "2026-02-14T10:30:00-0800". raw_dt = values[5] - if raw_dt: - try: - now = datetime.strptime(raw_dt, "%Y-%m-%dT%H:%M:%S%z") - except ValueError: - now = datetime.strptime(raw_dt[:19], "%Y-%m-%dT%H:%M:%S") # noqa: DTZ007 - else: - # Some hosts occasionally return empty stdout for this probe. - # Degrade gracefully so environment detection does not block task execution. - now = datetime.now().astimezone() + if not raw_dt: + msg = f"Environment shell returned empty datetime (raw output: {result.stdout!r})" + raise ValueError(msg) + try: + now = datetime.strptime(raw_dt, "%Y-%m-%dT%H:%M:%S%z") + except ValueError: + now = datetime.strptime(raw_dt[:19], "%Y-%m-%dT%H:%M:%S") # noqa: DTZ007 return EnvironmentContext( working_dir=values[0], diff --git a/libs/hexagent/hexagent/langchain/middleware.py b/libs/hexagent/hexagent/langchain/middleware.py index 32a504b5..fb8f66ac 100644 --- a/libs/hexagent/hexagent/langchain/middleware.py +++ b/libs/hexagent/hexagent/langchain/middleware.py @@ -381,26 +381,6 @@ async def abefore_model( Group 3: Annotators (system reminders). """ messages: list[BaseMessage] = list(state["messages"]) - env_prompt_updated = False - - # Workspace path can change after agent creation (e.g. cowork warm-up - # creates the agent, then PATCH mounts the user's folder and sets - # default_cwd). Tools see the new cwd, but the system prompt still - # lists the old ``pwd`` from EnvironmentResolver — follow-up turns may - # run size/list commands against the wrong directory. Refresh when pwd - # drifts (same pattern as compaction rebuild). - if self._environment_resolver is not None and self._prompt_profile is not None: - fresh_env = await self._environment_resolver.resolve() - old_wd = self._context.environment.working_dir if self._context.environment else None - if fresh_env.working_dir != old_wd: - self._context = replace(self._context, environment=fresh_env) - new_content = compose(self._prompt_profile, self._context) - if self._custom_prompt: - new_content = f"{self._custom_prompt}\n\n{new_content}" - if messages and isinstance(messages[0], SystemMessage): - messages[0] = SystemMessage(content=new_content) - self._system_prompt = new_content - env_prompt_updated = True # --- GROUP 1: Intercepts (compaction phases) --- phase = CompactionPhase(state.get("compaction_phase", CompactionPhase.NONE)) @@ -479,9 +459,6 @@ async def abefore_model( if images_extracted: return {"messages": LangGraphOverwrite(messages)} - if env_prompt_updated: - return {"messages": LangGraphOverwrite(messages)} - return None @hook_config(can_jump_to=["model"]) diff --git a/libs/hexagent/sandbox/vm/setup/setup.sh b/libs/hexagent/sandbox/vm/setup/setup.sh index e1cc2179..ce3187e3 100755 --- a/libs/hexagent/sandbox/vm/setup/setup.sh +++ b/libs/hexagent/sandbox/vm/setup/setup.sh @@ -1,6 +1,6 @@ #!/bin/bash # ============================================================================= -# HexAgent VM Setup - Orchestrator +# HexAgent VM Setup — Orchestrator # ============================================================================= # Discovers and runs step scripts in order with progress reporting, # resumability (marker files), concurrency protection (flock), and @@ -21,7 +21,7 @@ set -uo pipefail # No -e: we handle errors per-step. -# ------ Constants ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +# ── Constants ──────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" STEPS_DIR="${SCRIPT_DIR}/steps" MARKER_DIR="/var/lib/hexagent/setup" @@ -29,31 +29,18 @@ LOG_DIR="/var/log/hexagent/setup" LOCK_FILE="/var/run/hexagent-setup.lock" LOCK_FD=9 -# ------ Environment (inherited by steps) ------------------------------------------------------------------------------------------------------------------------ +# ── Environment (inherited by steps) ──────────────────────────────────────── export DEBIAN_FRONTEND=noninteractive export PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers export MARKER_DIR LOG_DIR -if grep -qi microsoft /proc/version 2>/dev/null; then - _OPENAGENT_DEFAULT_CN_MIRRORS=1 -else - _OPENAGENT_DEFAULT_CN_MIRRORS=0 -fi -OPENAGENT_USE_CN_MIRRORS="${OPENAGENT_USE_CN_MIRRORS:-${_OPENAGENT_DEFAULT_CN_MIRRORS}}" -OPENAGENT_APT_MIRROR="${OPENAGENT_APT_MIRROR:-https://mirrors.ustc.edu.cn/ubuntu}" -OPENAGENT_APT_PORTS_MIRROR="${OPENAGENT_APT_PORTS_MIRROR:-https://mirrors.ustc.edu.cn/ubuntu-ports}" -OPENAGENT_PIP_INDEX_URL="${OPENAGENT_PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}" -OPENAGENT_NPM_REGISTRY="${OPENAGENT_NPM_REGISTRY:-https://registry.npmmirror.com}" -OPENAGENT_PLAYWRIGHT_DOWNLOAD_HOST="${OPENAGENT_PLAYWRIGHT_DOWNLOAD_HOST:-https://npmmirror.com/mirrors/playwright}" -export OPENAGENT_USE_CN_MIRRORS OPENAGENT_APT_MIRROR OPENAGENT_APT_PORTS_MIRROR -export OPENAGENT_PIP_INDEX_URL OPENAGENT_NPM_REGISTRY OPENAGENT_PLAYWRIGHT_DOWNLOAD_HOST - -# ------ CLI defaults --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +# ── CLI defaults ───────────────────────────────────────────────────────────── FORCE=false SINGLE_STEP="" LIST_ONLY=false RESET=false -# ------ CLI parsing ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +# ── CLI parsing ────────────────────────────────────────────────────────────── usage() { echo "Usage: sudo bash setup.sh [OPTIONS]" echo "" @@ -76,16 +63,16 @@ while [[ $# -gt 0 ]]; do esac done -# ------ Root check --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +# ── Root check ─────────────────────────────────────────────────────────────── if [[ "$(id -u)" -ne 0 ]]; then echo "ERROR: Must run as root (sudo)." >&2 exit 1 fi -# ------ Directory setup ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +# ── Directory setup ────────────────────────────────────────────────────────── mkdir -p "$MARKER_DIR" "$LOG_DIR" -# ------ emit() -progress protocol ------------------------------------------------------------------------------------------------------------------------------------------ +# ── emit() — progress protocol ────────────────────────────────────────────── # Writes to fd 3 which points to the original stdout (what the backend reads). # All other output (package managers) goes to the log file. emit() { @@ -95,7 +82,7 @@ emit() { } export -f emit -# ------ apt_install -retry wrapper ------------------------------------------------------------------------------------------------------------------------------------------ +# ── apt_install — retry wrapper ────────────────────────────────────────────── apt_install() { local max_attempts=5 local delay=3 @@ -144,34 +131,22 @@ apt_install() { } export -f apt_install -# ------ pip_install -retry wrapper ------------------------------------------------------------------------------------------------------------------------------------------ +# ── pip_install — retry wrapper ────────────────────────────────────────────── pip_install() { local max_attempts=5 local delay=5 local attempt=1 - local use_cn_mirrors="${OPENAGENT_USE_CN_MIRRORS:-0}" local pip_opts=( + --break-system-packages --timeout 120 --retries 3 - --no-cache-dir ) - if pip3 help install 2>/dev/null | grep -q -- "--break-system-packages"; then - pip_opts+=(--break-system-packages) - fi while [[ $attempt -le $max_attempts ]]; do echo ">>> pip install attempt $attempt/$max_attempts (${#} packages)" if pip3 install "${pip_opts[@]}" "$@"; then return 0 fi - if [[ "$use_cn_mirrors" == "1" ]]; then - echo ">>> Mirror install failed, retrying with official PyPI..." - if PIP_INDEX_URL="https://pypi.org/simple" \ - PIP_EXTRA_INDEX_URL="" \ - pip3 install "${pip_opts[@]}" "$@"; then - return 0 - fi - fi echo ">>> Attempt $attempt failed. Retrying in ${delay}s..." sleep $delay delay=$((delay * 2)) @@ -189,14 +164,6 @@ pip_install() { pkg_ok=true break fi - if [[ "$use_cn_mirrors" == "1" ]]; then - if PIP_INDEX_URL="https://pypi.org/simple" \ - PIP_EXTRA_INDEX_URL="" \ - pip3 install "${pip_opts[@]}" "$pkg" 2>&1; then - pkg_ok=true - break - fi - fi echo ">>> Failed: $pkg (attempt $pkg_attempt/3)" sleep $((pkg_attempt * 3)) pkg_attempt=$((pkg_attempt + 1)) @@ -215,61 +182,11 @@ pip_install() { } export -f pip_install -configure_cn_mirrors() { - if [[ "${OPENAGENT_USE_CN_MIRRORS}" != "1" ]]; then - return 0 - fi - - emit _meta progress "Applying China mirrors (APT/PIP/NPM/Playwright)" - - # sed replacement escapes for arbitrary mirror strings (e.g. containing '&' or '|') - local apt_mirror_esc apt_ports_mirror_esc - apt_mirror_esc="${OPENAGENT_APT_MIRROR//\\/\\\\}" - apt_mirror_esc="${apt_mirror_esc//&/\\&}" - apt_mirror_esc="${apt_mirror_esc//|/\\|}" - apt_ports_mirror_esc="${OPENAGENT_APT_PORTS_MIRROR//\\/\\\\}" - apt_ports_mirror_esc="${apt_ports_mirror_esc//&/\\&}" - apt_ports_mirror_esc="${apt_ports_mirror_esc//|/\\|}" - - if [[ -f /etc/apt/sources.list ]]; then - cp -n /etc/apt/sources.list /etc/apt/sources.list.openagent.bak 2>/dev/null || true - sed -Ei \ - -e "s|https?://(archive|security)\.ubuntu\.com/ubuntu|${apt_mirror_esc}|g" \ - -e "s|https?://ports\.ubuntu\.com/ubuntu-ports|${apt_ports_mirror_esc}|g" \ - /etc/apt/sources.list 2>/dev/null || true - fi - - if [[ -f /etc/apt/sources.list.d/ubuntu.sources ]]; then - cp -n /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.openagent.bak 2>/dev/null || true - sed -Ei \ - -e "s|^URIs:[[:space:]]*https?://(archive|security)\.ubuntu\.com/ubuntu/?$|URIs: ${apt_mirror_esc}|g" \ - -e "s|^URIs:[[:space:]]*https?://ports\.ubuntu\.com/ubuntu-ports/?$|URIs: ${apt_ports_mirror_esc}|g" \ - /etc/apt/sources.list.d/ubuntu.sources 2>/dev/null || true - fi - - cat >/etc/pip.conf </etc/profile.d/openagent-mirrors.sh < "${MARKER_DIR}/$1.done"; } -# ------ Step discovery --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +# ── Step discovery ─────────────────────────────────────────────────────────── discover_steps() { for f in "${STEPS_DIR}"/*.sh; do [[ -f "$f" ]] || continue @@ -282,41 +199,41 @@ step_desc() { sed -n '2s/^# *//p' "$1" } -# ------ --reset ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +# ── --reset ────────────────────────────────────────────────────────────────── if [[ "$RESET" == true ]]; then rm -f "${MARKER_DIR}"/*.done echo "All markers cleared." exit 0 fi -# ------ --list --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +# ── --list ─────────────────────────────────────────────────────────────────── if [[ "$LIST_ONLY" == true ]]; then while IFS= read -r step_file; do step_id="$(basename "$step_file" .sh)" if step_done "$step_id"; then echo "[done] $step_id ($(cat "${MARKER_DIR}/${step_id}.done"))" else - echo "[pending] $step_id -$(step_desc "$step_file")" + echo "[pending] $step_id — $(step_desc "$step_file")" fi done < <(discover_steps) exit 0 fi -# ------ Concurrency lock --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +# ── Concurrency lock ───────────────────────────────────────────────────────── exec 9>"$LOCK_FILE" if ! flock -n $LOCK_FD; then echo "ERROR: Another setup instance is running (lockfile: $LOCK_FILE)" >&2 exit 1 fi -# ------ fd redirection --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -# fd 3 = original stdout -backend reads @@SETUP: lines from here -# stdout + stderr -log file (all package manager noise) +# ── fd redirection ─────────────────────────────────────────────────────────── +# fd 3 = original stdout → backend reads @@SETUP: lines from here +# stdout + stderr → log file (all package manager noise) LOGFILE="${LOG_DIR}/setup-$(date +%Y%m%d-%H%M%S).log" exec 3>&1 exec 1>>"$LOGFILE" 2>&1 -# ------ Signal handling ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +# ── Signal handling ────────────────────────────────────────────────────────── HEARTBEAT_PID="" cleanup() { @@ -332,7 +249,7 @@ cancelled() { } trap cancelled SIGTERM SIGINT -# ------ Heartbeat ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +# ── Heartbeat ──────────────────────────────────────────────────────────────── start_heartbeat() { local step_id="$1" ( @@ -352,7 +269,7 @@ stop_heartbeat() { fi } -# ------ Preflight ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +# ── Preflight ──────────────────────────────────────────────────────────────── preflight() { emit _meta start "Preflight checks" @@ -364,9 +281,7 @@ preflight() { esac export ARCH - configure_cn_mirrors - - # Disk space (require >= 10 GB free on /) + # Disk space (require ≥10 GB free on /) local free_kb free_kb=$(df / --output=avail | tail -1 | tr -d ' ') if (( free_kb < 10485760 )); then @@ -377,7 +292,7 @@ preflight() { emit _meta done "Preflight OK (arch=$ARCH, free=$((free_kb / 1024))MB)" } -# ------ run_step --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +# ── run_step ───────────────────────────────────────────────────────────────── run_step() { local step_file="$1" local step_id @@ -410,12 +325,12 @@ run_step() { mark_done "$step_id" emit "$step_id" done "Completed in ${elapsed}s" else - emit "$step_id" error "Failed (exit $rc) after ${elapsed}s - see $LOGFILE" + emit "$step_id" error "Failed (exit $rc) after ${elapsed}s — see $LOGFILE" return $rc fi } -# ------ Main --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +# ── Main ───────────────────────────────────────────────────────────────────── main() { preflight @@ -447,7 +362,7 @@ main() { done < <(discover_steps) if [[ $failed -gt 0 ]]; then - emit _meta error "Setup failed - re-run to resume from failed step" + emit _meta error "Setup failed — re-run to resume from failed step" exit 1 fi @@ -455,4 +370,3 @@ main() { } main - diff --git a/libs/hexagent/sandbox/vm/setup/steps/03_apt.sh b/libs/hexagent/sandbox/vm/setup/steps/03_apt.sh index 82bec2e1..8e1929a0 100755 --- a/libs/hexagent/sandbox/vm/setup/steps/03_apt.sh +++ b/libs/hexagent/sandbox/vm/setup/steps/03_apt.sh @@ -1,31 +1,71 @@ #!/bin/bash -# System packages (minimal baseline; install extras on demand) +# System packages (core utils, Python, Java, PDF, LaTeX, fonts, etc.) set -uo pipefail # No -e: apt_install handles its own errors with retries. apt-get update -emit 03_apt progress "group=core_utils" +emit 03_apt progress "group=core_utils (17 packages)" apt_install \ - bash coreutils wget curl git zip unzip jq tree ripgrep \ - file findutils patch sqlite3 || exit 1 + bash coreutils wget curl git zip unzip bzip2 xz-utils \ + file findutils patch perl jq tree sqlite3 ripgrep \ + netcat-openbsd apt-transport-https software-properties-common -emit 03_apt progress "group=build_tools" -apt_install build-essential pkg-config || exit 1 +emit 03_apt progress "group=build_tools (2 packages)" +apt_install build-essential pkg-config -emit 03_apt progress "group=python" -apt_install python3 python3-dev python3-pip python3-venv pipx || exit 1 +emit 03_apt progress "group=python (5 packages)" +apt_install python3 python3-dev python3-pip python3-venv pipx -emit 03_apt progress "group=media" -apt_install imagemagick graphviz || exit 1 +emit 03_apt progress "group=java (1 package)" +apt_install default-jre-headless -emit 03_apt progress "group=fonts" +emit 03_apt progress "group=pdf_tools (5 packages)" +apt_install poppler-utils qpdf pdftk-java wkhtmltopdf ghostscript + +emit 03_apt progress "group=pandoc (1 package)" +apt_install pandoc + +emit 03_apt progress "group=libreoffice (5 packages)" +apt_install \ + libreoffice-writer libreoffice-calc libreoffice-impress \ + libreoffice-common libreoffice-java-common + +emit 03_apt progress "group=media (3 packages)" +apt_install imagemagick graphviz ffmpeg + +emit 03_apt progress "group=ocr (2 packages)" +apt_install tesseract-ocr tesseract-ocr-eng + +emit 03_apt progress "group=latex (9 packages)" +apt_install \ + texlive-base texlive-latex-base texlive-latex-recommended \ + texlive-latex-extra texlive-fonts-recommended texlive-xetex \ + texlive-science texlive-pictures latexmk + +emit 03_apt progress "group=fonts (12 packages)" +apt_install \ + fonts-liberation2 fonts-dejavu fonts-freefont-ttf \ + fonts-noto-cjk fonts-noto-color-emoji \ + fonts-crosextra-caladea fonts-crosextra-carlito \ + fonts-lmodern fonts-texgyre fonts-opensymbol \ + fonts-wqy-zenhei fonts-ipafont-gothic + +emit 03_apt progress "group=x11_display (5 packages)" +apt_install \ + xvfb x11-xkb-utils xfonts-scalable xfonts-cyrillic xfonts-utils + +emit 03_apt progress "group=browser_libs (17 packages)" apt_install \ - fonts-liberation2 fonts-dejavu || exit 1 + libnss3 libnss3-tools libatk1.0-0t64 libatk-bridge2.0-0t64 \ + libcups2t64 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \ + libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 \ + libcairo2 libatspi2.0-0t64 libgtk-3-0t64 libgtk-4-1 -emit 03_apt progress "group=dev_libs" +emit 03_apt progress "group=dev_libs (7 packages)" apt_install \ - libffi-dev zlib1g-dev libpng-dev libfreetype-dev libbz2-dev || exit 1 + libffi-dev zlib1g-dev libpng-dev libfreetype-dev libcairo2-dev \ + libglib2.0-dev libbz2-dev # Cleanup emit 03_apt progress "Cleaning apt cache" diff --git a/libs/hexagent/sandbox/vm/setup/steps/04_npm.sh b/libs/hexagent/sandbox/vm/setup/steps/04_npm.sh index 014e24f0..2d6fd675 100755 --- a/libs/hexagent/sandbox/vm/setup/steps/04_npm.sh +++ b/libs/hexagent/sandbox/vm/setup/steps/04_npm.sh @@ -1,11 +1,31 @@ #!/bin/bash -# NPM global packages (minimal baseline; install extras on demand) +# NPM global packages set -euo pipefail -if [[ "${OPENAGENT_USE_CN_MIRRORS:-0}" == "1" ]]; then - emit 04_npm progress "Configuring npm registry mirror" - npm config set registry "${OPENAGENT_NPM_REGISTRY}" >/dev/null 2>&1 || true -fi - emit 04_npm progress "Installing npm global packages" -npm install -g typescript tsx playwright +npm install -g \ + docx@9 \ + pptxgenjs@4.0.1 \ + pdf-lib@1.17.1 \ + pdfjs-dist \ + marked \ + markdown-toc \ + markdownlint-cli \ + markdownlint-cli2 \ + remark-cli \ + remark-preset-lint-recommended \ + @mermaid-js/mermaid-cli \ + graphviz \ + react \ + react-dom \ + react-icons \ + typescript \ + ts-node \ + tsx \ + sharp \ + playwright + +if [[ "$ARCH" == "x86_64" ]]; then + emit 04_npm progress "Installing markdown-pdf (x86_64 only)" + npm install -g markdown-pdf +fi diff --git a/libs/hexagent/sandbox/vm/setup/steps/05_pip.sh b/libs/hexagent/sandbox/vm/setup/steps/05_pip.sh index 1960556a..e0236d87 100755 --- a/libs/hexagent/sandbox/vm/setup/steps/05_pip.sh +++ b/libs/hexagent/sandbox/vm/setup/steps/05_pip.sh @@ -1,16 +1,64 @@ #!/bin/bash -# Python packages (minimal baseline; install extras on demand) -set -euo pipefail +# Python packages (11 batches) +set -uo pipefail +# No -e: pip_install handles its own retries. -emit 05_pip progress "Core data & visualization" -pip_install numpy pandas matplotlib pillow +# Preflight: fix blinker conflict with system Flask +emit 05_pip progress "Preflight: fixing blinker" +pip3 install --break-system-packages --timeout 120 --ignore-installed blinker -emit 05_pip progress "Web & HTTP" -pip_install requests beautifulsoup4 lxml +emit 05_pip progress "Batch 1/11 — Core numeric (4 packages)" +pip_install numpy pandas scipy sympy -emit 05_pip progress "Utilities" +emit 05_pip progress "Batch 2/11 — ML / CV (3 packages)" +pip_install scikit-learn scikit-image onnxruntime + +emit 05_pip progress "Batch 3/11 — ML / CV OpenCV (3 packages)" +pip_install opencv-python opencv-contrib-python opencv-python-headless + +emit 05_pip progress "Batch 4/11 — Visualization (3 packages)" +pip_install matplotlib seaborn networkx + +emit 05_pip progress "Batch 5/11 — Image / media (5 packages)" +pip_install pillow imageio imageio-ffmpeg Wand pytesseract + +emit 05_pip progress "Batch 6/11 — PDF tools (11 packages)" +pip_install \ + pdfplumber pdfminer.six pypdf pikepdf pdf2image pdfkit \ + img2pdf camelot-py tabula-py reportlab pypdfium2 pymupdf + +emit 05_pip progress "Batch 7/11 — Office documents (5 packages)" +pip_install python-docx python-pptx openpyxl xlsxwriter odfpy + +emit 05_pip progress "Batch 8/11 — Markdown / docs (11 packages)" pip_install \ - uv click pyyaml python-dotenv tabulate + markitdown markdownify markdown grip mistune markdown-it-py \ + marko mkdocs mkdocs-material mkdocs-material-extensions \ + mkdocs-get-deps pymdown-extensions + +emit 05_pip progress "Batch 9/11 — Web / HTTP (5 packages)" +pip_install requests beautifulsoup4 lxml Flask httplib2 + +emit 05_pip progress "Batch 10/11 — Automation / browser (3 packages)" +pip_install playwright unoserver pyoo + +emit 05_pip progress "Batch 11/11 — System utilities (14 packages)" +pip_install \ + uv magika click colorama coloredlogs humanfriendly tabulate \ + python-dotenv psutil watchdog sounddevice pycairo graphviz freetype-py + +# Foundational (many already installed as transitive deps — pip will no-op) +emit 05_pip progress "Foundational / low-level (17 packages)" +pip_install \ + attrs bcrypt jsonschema python-magic livereload tornado PyYAML \ + certifi charset-normalizer cryptography defusedxml idna joblib \ + packaging protobuf python-dateutil pytz typing_extensions urllib3 + +# Platform-specific +if [[ "$ARCH" == "x86_64" ]]; then + emit 05_pip progress "Platform-specific: mediapipe (x86_64 only)" + pip_install "mediapipe>=0.10.32" +fi # Cleanup emit 05_pip progress "Cleaning pip cache" diff --git a/libs/hexagent/sandbox/vm/setup/steps/06_playwright.sh b/libs/hexagent/sandbox/vm/setup/steps/06_playwright.sh index c1b344b8..f017ffbf 100755 --- a/libs/hexagent/sandbox/vm/setup/steps/06_playwright.sh +++ b/libs/hexagent/sandbox/vm/setup/steps/06_playwright.sh @@ -6,13 +6,11 @@ set -euo pipefail dpkg --configure -a || true apt-get install -y -f || true -# Install Playwright OS deps (apt) retry-wrapped +# Install Playwright OS deps (apt) — retry-wrapped max_attempts=5 -deps_ok=0 for ((attempt = 1; attempt <= max_attempts; attempt++)); do emit 06_playwright progress "install-deps attempt $attempt/$max_attempts" if npx playwright install-deps chromium; then - deps_ok=1 break fi echo ">>> Retrying in 5s..." @@ -22,52 +20,9 @@ for ((attempt = 1; attempt <= max_attempts; attempt++)); do apt-get update || true done -if [[ $deps_ok -ne 1 ]]; then - emit 06_playwright error "Failed to install Playwright system dependencies" - exit 1 -fi - # Download Chromium binary emit 06_playwright progress "Downloading Chromium binary" -mirror_host="${OPENAGENT_PLAYWRIGHT_DOWNLOAD_HOST:-}" -browser_ok=0 - -# If bundled browsers are already present in the VM image, skip network download. -if find /opt/pw-browsers -type f \( -name "chrome-headless-shell" -o -name "chrome" \) 2>/dev/null | grep -q .; then - emit 06_playwright progress "Bundled Playwright browser detected under /opt/pw-browsers, skipping download" - browser_ok=1 -fi - -for ((attempt = 1; attempt <= max_attempts; attempt++)); do - if [[ $browser_ok -eq 1 ]]; then - break - fi - - if [[ "${OPENAGENT_USE_CN_MIRRORS:-0}" == "1" && -n "$mirror_host" ]]; then - emit 06_playwright progress "browser install attempt $attempt/$max_attempts (mirror)" - if PLAYWRIGHT_DOWNLOAD_HOST="$mirror_host" \ - PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers \ - npx playwright install chromium; then - browser_ok=1 - break - fi - emit 06_playwright progress "Mirror unavailable, retrying with official host" - else - emit 06_playwright progress "browser install attempt $attempt/$max_attempts" - fi - - if env -u PLAYWRIGHT_DOWNLOAD_HOST PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers npx playwright install chromium; then - browser_ok=1 - break - fi - echo ">>> Browser download attempt $attempt failed. Retrying in 5s..." - sleep 5 -done - -if [[ $browser_ok -ne 1 ]]; then - emit 06_playwright error "Failed to download Chromium browser" - exit 1 -fi +PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers npx playwright install chromium # Allow PDF operations in ImageMagick (if restricted) if [[ -f /etc/ImageMagick-6/policy.xml ]] && \ diff --git a/libs/hexagent/sandbox/vm/setup_lite/setup.sh b/libs/hexagent/sandbox/vm/setup_lite/setup.sh new file mode 100755 index 00000000..071ecc58 --- /dev/null +++ b/libs/hexagent/sandbox/vm/setup_lite/setup.sh @@ -0,0 +1,458 @@ +#!/bin/bash +# ============================================================================= +# HexAgent VM Setup (Lite) — Orchestrator +# ============================================================================= +# Lite variant: minimal baseline packages for demo/Electron deployments. +# Discovers and runs step scripts in order with progress reporting, +# resumability (marker files), concurrency protection (flock), and +# heartbeat for long-running operations. +# +# Usage: +# sudo bash setup.sh # Run all steps (skip completed) +# sudo bash setup.sh --force # Re-run all steps ignoring markers +# sudo bash setup.sh --step 05_pip # Run a single step +# sudo bash setup.sh --list # Show step status +# sudo bash setup.sh --reset # Clear all markers +# +# Progress protocol (stdout): +# @@SETUP::: +# Statuses: start, progress, done, skip, error, heartbeat +# ============================================================================= + +set -uo pipefail +# No -e: we handle errors per-step. + +# ── Constants ──────────────────────────────────────────────────────────────── +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +STEPS_DIR="${SCRIPT_DIR}/steps" +MARKER_DIR="/var/lib/hexagent/setup" +LOG_DIR="/var/log/hexagent/setup" +LOCK_FILE="/var/run/hexagent-setup.lock" +LOCK_FD=9 + +# ── Environment (inherited by steps) ──────────────────────────────────────── +export DEBIAN_FRONTEND=noninteractive +export PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers +export MARKER_DIR LOG_DIR +if grep -qi microsoft /proc/version 2>/dev/null; then + _OPENAGENT_DEFAULT_CN_MIRRORS=1 +else + _OPENAGENT_DEFAULT_CN_MIRRORS=0 +fi +OPENAGENT_USE_CN_MIRRORS="${OPENAGENT_USE_CN_MIRRORS:-${_OPENAGENT_DEFAULT_CN_MIRRORS}}" +OPENAGENT_APT_MIRROR="${OPENAGENT_APT_MIRROR:-https://mirrors.ustc.edu.cn/ubuntu}" +OPENAGENT_APT_PORTS_MIRROR="${OPENAGENT_APT_PORTS_MIRROR:-https://mirrors.ustc.edu.cn/ubuntu-ports}" +OPENAGENT_PIP_INDEX_URL="${OPENAGENT_PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}" +OPENAGENT_NPM_REGISTRY="${OPENAGENT_NPM_REGISTRY:-https://registry.npmmirror.com}" +OPENAGENT_PLAYWRIGHT_DOWNLOAD_HOST="${OPENAGENT_PLAYWRIGHT_DOWNLOAD_HOST:-https://npmmirror.com/mirrors/playwright}" +export OPENAGENT_USE_CN_MIRRORS OPENAGENT_APT_MIRROR OPENAGENT_APT_PORTS_MIRROR +export OPENAGENT_PIP_INDEX_URL OPENAGENT_NPM_REGISTRY OPENAGENT_PLAYWRIGHT_DOWNLOAD_HOST + +# ── CLI defaults ───────────────────────────────────────────────────────────── +FORCE=false +SINGLE_STEP="" +LIST_ONLY=false +RESET=false + +# ── CLI parsing ────────────────────────────────────────────────────────────── +usage() { + echo "Usage: sudo bash setup.sh [OPTIONS]" + echo "" + echo "Options:" + echo " --force Re-run all steps (ignore markers)" + echo " --step Run a single step (e.g. --step 05_pip)" + echo " --list Show steps and their completion status" + echo " --reset Clear all markers, then exit" + echo " -h, --help Show this help" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --force) FORCE=true; shift ;; + --step) SINGLE_STEP="$2"; shift 2 ;; + --list) LIST_ONLY=true; shift ;; + --reset) RESET=true; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1"; usage; exit 1 ;; + esac +done + +# ── Root check ─────────────────────────────────────────────────────────────── +if [[ "$(id -u)" -ne 0 ]]; then + echo "ERROR: Must run as root (sudo)." >&2 + exit 1 +fi + +# ── Directory setup ────────────────────────────────────────────────────────── +mkdir -p "$MARKER_DIR" "$LOG_DIR" + +# ── emit() — progress protocol ────────────────────────────────────────────── +# Writes to fd 3 which points to the original stdout (what the backend reads). +# All other output (package managers) goes to the log file. +emit() { + # Usage: emit + local step_id="$1" status="$2" message="${3:-}" + printf '@@SETUP:%s:%s:%s\n' "$step_id" "$status" "$message" >&3 +} +export -f emit + +# ── apt_install — retry wrapper ────────────────────────────────────────────── +apt_install() { + local max_attempts=5 + local delay=3 + local attempt=1 + + while [[ $attempt -le $max_attempts ]]; do + echo ">>> apt-get install attempt $attempt/$max_attempts" + if [[ $attempt -eq 1 ]]; then + apt-get install -y --no-install-recommends "$@" && return 0 + else + apt-get install -y --no-install-recommends --fix-missing "$@" && return 0 + fi + + echo ">>> Attempt $attempt failed. Retrying in ${delay}s..." + sleep $delay + dpkg --configure -a || true + apt-get install -y -f || true + delay=$((delay * 2)) + attempt=$((attempt + 1)) + done + + # Final fallback: per-package download then bulk install + echo ">>> Bulk install failed. Falling back to per-package download..." + dpkg --configure -a || true + apt-get install -y -f || true + + local pkg + for pkg in "$@"; do + local pkg_attempt=1 + while [[ $pkg_attempt -le 3 ]]; do + apt-get install -y --no-install-recommends -d "$pkg" 2>/dev/null && break + echo ">>> Download failed for $pkg (attempt $pkg_attempt/3)" + sleep $((pkg_attempt * 2)) + pkg_attempt=$((pkg_attempt + 1)) + done + done + + echo ">>> Installing all packages from local cache..." + if apt-get install -y --no-install-recommends "$@"; then + return 0 + fi + + echo ">>> ERROR: apt-get install failed after all retries" + echo ">>> Failed packages: $*" + return 1 +} +export -f apt_install + +# ── pip_install — retry wrapper ────────────────────────────────────────────── +pip_install() { + local max_attempts=5 + local delay=5 + local attempt=1 + local use_cn_mirrors="${OPENAGENT_USE_CN_MIRRORS:-0}" + local pip_opts=( + --timeout 120 + --retries 3 + --no-cache-dir + ) + if pip3 help install 2>/dev/null | grep -q -- "--break-system-packages"; then + pip_opts+=(--break-system-packages) + fi + + while [[ $attempt -le $max_attempts ]]; do + echo ">>> pip install attempt $attempt/$max_attempts (${#} packages)" + if pip3 install "${pip_opts[@]}" "$@"; then + return 0 + fi + if [[ "$use_cn_mirrors" == "1" ]]; then + echo ">>> Mirror install failed, retrying with official PyPI..." + if PIP_INDEX_URL="https://pypi.org/simple" \ + PIP_EXTRA_INDEX_URL="" \ + pip3 install "${pip_opts[@]}" "$@"; then + return 0 + fi + fi + echo ">>> Attempt $attempt failed. Retrying in ${delay}s..." + sleep $delay + delay=$((delay * 2)) + attempt=$((attempt + 1)) + done + + # Final fallback: install one at a time + echo ">>> Batch install failed. Falling back to per-package install..." + local pkg failed=() + for pkg in "$@"; do + local pkg_attempt=1 + local pkg_ok=false + while [[ $pkg_attempt -le 3 ]]; do + if pip3 install "${pip_opts[@]}" "$pkg" 2>&1; then + pkg_ok=true + break + fi + if [[ "$use_cn_mirrors" == "1" ]]; then + if PIP_INDEX_URL="https://pypi.org/simple" \ + PIP_EXTRA_INDEX_URL="" \ + pip3 install "${pip_opts[@]}" "$pkg" 2>&1; then + pkg_ok=true + break + fi + fi + echo ">>> Failed: $pkg (attempt $pkg_attempt/3)" + sleep $((pkg_attempt * 3)) + pkg_attempt=$((pkg_attempt + 1)) + done + if [[ "$pkg_ok" == false ]]; then + failed+=("$pkg") + fi + done + + if [[ ${#failed[@]} -gt 0 ]]; then + echo ">>> ERROR: These packages failed after all retries:" + printf '>>> %s\n' "${failed[@]}" + return 1 + fi + return 0 +} +export -f pip_install + +configure_cn_mirrors() { + if [[ "${OPENAGENT_USE_CN_MIRRORS}" != "1" ]]; then + return 0 + fi + + emit _meta progress "Applying China mirrors (APT/PIP/NPM/Playwright)" + + # sed replacement escapes for arbitrary mirror strings (e.g. containing '&' or '|') + local apt_mirror_esc apt_ports_mirror_esc + apt_mirror_esc="${OPENAGENT_APT_MIRROR//\\/\\\\}" + apt_mirror_esc="${apt_mirror_esc//&/\\&}" + apt_mirror_esc="${apt_mirror_esc//|/\\|}" + apt_ports_mirror_esc="${OPENAGENT_APT_PORTS_MIRROR//\\/\\\\}" + apt_ports_mirror_esc="${apt_ports_mirror_esc//&/\\&}" + apt_ports_mirror_esc="${apt_ports_mirror_esc//|/\\|}" + + if [[ -f /etc/apt/sources.list ]]; then + cp -n /etc/apt/sources.list /etc/apt/sources.list.openagent.bak 2>/dev/null || true + sed -Ei \ + -e "s|https?://(archive|security)\.ubuntu\.com/ubuntu|${apt_mirror_esc}|g" \ + -e "s|https?://ports\.ubuntu\.com/ubuntu-ports|${apt_ports_mirror_esc}|g" \ + /etc/apt/sources.list 2>/dev/null || true + fi + + if [[ -f /etc/apt/sources.list.d/ubuntu.sources ]]; then + cp -n /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.openagent.bak 2>/dev/null || true + sed -Ei \ + -e "s|^URIs:[[:space:]]*https?://(archive|security)\.ubuntu\.com/ubuntu/?$|URIs: ${apt_mirror_esc}|g" \ + -e "s|^URIs:[[:space:]]*https?://ports\.ubuntu\.com/ubuntu-ports/?$|URIs: ${apt_ports_mirror_esc}|g" \ + /etc/apt/sources.list.d/ubuntu.sources 2>/dev/null || true + fi + + cat >/etc/pip.conf </etc/profile.d/openagent-mirrors.sh < "${MARKER_DIR}/$1.done"; } + +# ── Step discovery ─────────────────────────────────────────────────────────── +discover_steps() { + for f in "${STEPS_DIR}"/*.sh; do + [[ -f "$f" ]] || continue + echo "$f" + done | sort +} + +# Get step description from the second line (# comment) of a step file. +step_desc() { + sed -n '2s/^# *//p' "$1" +} + +# ── --reset ────────────────────────────────────────────────────────────────── +if [[ "$RESET" == true ]]; then + rm -f "${MARKER_DIR}"/*.done + echo "All markers cleared." + exit 0 +fi + +# ── --list ─────────────────────────────────────────────────────────────────── +if [[ "$LIST_ONLY" == true ]]; then + while IFS= read -r step_file; do + step_id="$(basename "$step_file" .sh)" + if step_done "$step_id"; then + echo "[done] $step_id ($(cat "${MARKER_DIR}/${step_id}.done"))" + else + echo "[pending] $step_id — $(step_desc "$step_file")" + fi + done < <(discover_steps) + exit 0 +fi + +# ── Concurrency lock ───────────────────────────────────────────────────────── +exec 9>"$LOCK_FILE" +if ! flock -n $LOCK_FD; then + echo "ERROR: Another setup instance is running (lockfile: $LOCK_FILE)" >&2 + exit 1 +fi + +# ── fd redirection ─────────────────────────────────────────────────────────── +# fd 3 = original stdout → backend reads @@SETUP: lines from here +# stdout + stderr → log file (all package manager noise) +LOGFILE="${LOG_DIR}/setup-$(date +%Y%m%d-%H%M%S).log" +exec 3>&1 +exec 1>>"$LOGFILE" 2>&1 + +# ── Signal handling ────────────────────────────────────────────────────────── +HEARTBEAT_PID="" + +cleanup() { + [[ -n "$HEARTBEAT_PID" ]] && kill "$HEARTBEAT_PID" 2>/dev/null || true + flock -u $LOCK_FD 2>/dev/null || true + rm -f "$LOCK_FILE" +} +trap cleanup EXIT + +cancelled() { + emit _meta error "Cancelled by signal" + exit 130 +} +trap cancelled SIGTERM SIGINT + +# ── Heartbeat ──────────────────────────────────────────────────────────────── +start_heartbeat() { + local step_id="$1" + ( + while true; do + sleep 15 + emit "$step_id" heartbeat "" + done + ) & + HEARTBEAT_PID=$! +} + +stop_heartbeat() { + if [[ -n "$HEARTBEAT_PID" ]]; then + kill "$HEARTBEAT_PID" 2>/dev/null || true + wait "$HEARTBEAT_PID" 2>/dev/null || true + HEARTBEAT_PID="" + fi +} + +# ── Preflight ──────────────────────────────────────────────────────────────── +preflight() { + emit _meta start "Preflight checks" + + # Architecture + ARCH="$(uname -m)" + case "$ARCH" in + x86_64|aarch64) ;; + *) emit _meta error "Unsupported architecture: $ARCH"; exit 1 ;; + esac + export ARCH + + configure_cn_mirrors + + # Disk space (require >= 10 GB free on /) + local free_kb + free_kb=$(df / --output=avail | tail -1 | tr -d ' ') + if (( free_kb < 10485760 )); then + emit _meta error "Insufficient disk space: $((free_kb / 1024))MB free, need 10GB+" + exit 1 + fi + + emit _meta done "Preflight OK (arch=$ARCH, free=$((free_kb / 1024))MB)" +} + +# ── run_step ───────────────────────────────────────────────────────────────── +run_step() { + local step_file="$1" + local step_id + step_id="$(basename "$step_file" .sh)" + + # Skip if already completed (unless --force) + if [[ "$FORCE" != true ]] && step_done "$step_id"; then + emit "$step_id" skip "Already completed ($(cat "${MARKER_DIR}/${step_id}.done"))" + return 0 + fi + + local desc + desc="$(step_desc "$step_file")" + emit "$step_id" start "${desc:-$step_id}" + + start_heartbeat "$step_id" + local start_ts + start_ts=$(date +%s) + + # Run step in a subshell so it inherits exported functions + fd 3 + # but cannot kill the orchestrator on failure. + ( source "$step_file" ) + local rc=$? + + stop_heartbeat + + local elapsed=$(( $(date +%s) - start_ts )) + + if [[ $rc -eq 0 ]]; then + mark_done "$step_id" + emit "$step_id" done "Completed in ${elapsed}s" + else + emit "$step_id" error "Failed (exit $rc) after ${elapsed}s — see $LOGFILE" + return $rc + fi +} + +# ── Main ───────────────────────────────────────────────────────────────────── +main() { + preflight + + # Count total steps + local total=0 + while IFS= read -r _; do + total=$((total + 1)) + done < <(discover_steps) + emit _meta progress "total_steps=$total" + + # Single step mode + if [[ -n "$SINGLE_STEP" ]]; then + local target="${STEPS_DIR}/${SINGLE_STEP}.sh" + if [[ ! -f "$target" ]]; then + emit _meta error "Step not found: $SINGLE_STEP" + exit 1 + fi + run_step "$target" + exit $? + fi + + # Run all steps in order + local failed=0 + while IFS= read -r step_file; do + if ! run_step "$step_file"; then + failed=1 + break + fi + done < <(discover_steps) + + if [[ $failed -gt 0 ]]; then + emit _meta error "Setup failed — re-run to resume from failed step" + exit 1 + fi + + emit _meta done "All steps complete" +} + +main diff --git a/libs/hexagent/sandbox/vm/setup_lite/steps/01_base.sh b/libs/hexagent/sandbox/vm/setup_lite/steps/01_base.sh new file mode 100755 index 00000000..a52dc1a3 --- /dev/null +++ b/libs/hexagent/sandbox/vm/setup_lite/steps/01_base.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Base prerequisites +set -euo pipefail + +emit 01_base progress "Running apt-get update" +apt-get update + +emit 01_base progress "Installing ca-certificates, curl, gnupg" +apt_install ca-certificates curl gnupg diff --git a/libs/hexagent/sandbox/vm/setup_lite/steps/02_nodejs.sh b/libs/hexagent/sandbox/vm/setup_lite/steps/02_nodejs.sh new file mode 100755 index 00000000..2bc68aa6 --- /dev/null +++ b/libs/hexagent/sandbox/vm/setup_lite/steps/02_nodejs.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Node.js 22.x (NodeSource with tarball fallback) +set -euo pipefail + +NODE_MAJOR=22 + +# --- Attempt 1: NodeSource apt repo (retried) --- +nodesource_ok=false +for attempt in 1 2 3; do + emit 02_nodejs progress "NodeSource setup attempt $attempt/3" + if curl -fsSL --retry 3 --retry-delay 5 \ + "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash -; then + if apt-get install -y nodejs; then + nodesource_ok=true + break + fi + fi + echo ">>> Attempt $attempt failed. Retrying in $((attempt * 5))s..." + sleep $((attempt * 5)) +done + +# --- Attempt 2: Official binary tarball --- +if [[ "$nodesource_ok" == false ]]; then + emit 02_nodejs progress "Falling back to official binary tarball" + apt-get remove -y nodejs npm 2>/dev/null || true + + case "$ARCH" in + x86_64) NODE_ARCH="x64" ;; + aarch64) NODE_ARCH="arm64" ;; + *) echo "ERROR: Unsupported architecture: $ARCH"; exit 1 ;; + esac + + NODE_VERSION=$(curl -fsSL --retry 3 \ + "https://nodejs.org/dist/latest-v${NODE_MAJOR}.x/" \ + | grep -oP 'node-v\K[0-9]+\.[0-9]+\.[0-9]+' | head -1) + + if [[ -z "$NODE_VERSION" ]]; then + echo "ERROR: Could not determine latest Node.js ${NODE_MAJOR}.x version" + exit 1 + fi + + TARBALL="node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" + URL="https://nodejs.org/dist/v${NODE_VERSION}/${TARBALL}" + + emit 02_nodejs progress "Downloading Node.js v${NODE_VERSION} for ${NODE_ARCH}" + curl -fsSL --retry 3 --retry-delay 5 -o "/tmp/${TARBALL}" "$URL" + tar -xJf "/tmp/${TARBALL}" -C /usr/local --strip-components=1 + rm -f "/tmp/${TARBALL}" +fi + +# --- Verify --- +node --version +npm --version + +if ! command -v npm >/dev/null 2>&1; then + echo "ERROR: npm is not available after Node.js installation" + exit 1 +fi + +emit 02_nodejs progress "Node.js $(node --version) with npm $(npm --version) installed" diff --git a/libs/hexagent/sandbox/vm/setup_lite/steps/03_apt.sh b/libs/hexagent/sandbox/vm/setup_lite/steps/03_apt.sh new file mode 100755 index 00000000..82bec2e1 --- /dev/null +++ b/libs/hexagent/sandbox/vm/setup_lite/steps/03_apt.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# System packages (minimal baseline; install extras on demand) +set -uo pipefail +# No -e: apt_install handles its own errors with retries. + +apt-get update + +emit 03_apt progress "group=core_utils" +apt_install \ + bash coreutils wget curl git zip unzip jq tree ripgrep \ + file findutils patch sqlite3 || exit 1 + +emit 03_apt progress "group=build_tools" +apt_install build-essential pkg-config || exit 1 + +emit 03_apt progress "group=python" +apt_install python3 python3-dev python3-pip python3-venv pipx || exit 1 + +emit 03_apt progress "group=media" +apt_install imagemagick graphviz || exit 1 + +emit 03_apt progress "group=fonts" +apt_install \ + fonts-liberation2 fonts-dejavu || exit 1 + +emit 03_apt progress "group=dev_libs" +apt_install \ + libffi-dev zlib1g-dev libpng-dev libfreetype-dev libbz2-dev || exit 1 + +# Cleanup +emit 03_apt progress "Cleaning apt cache" +rm -rf /var/lib/apt/lists/* +apt-get autoremove -y +apt-get clean + +# Verify nothing is broken +if ! dpkg --audit 2>/dev/null || dpkg -l | grep -q '^iF'; then + echo ">>> WARNING: Some packages are in a broken state" + dpkg -l | grep '^iF' || true + exit 1 +fi diff --git a/libs/hexagent/sandbox/vm/setup_lite/steps/04_npm.sh b/libs/hexagent/sandbox/vm/setup_lite/steps/04_npm.sh new file mode 100755 index 00000000..014e24f0 --- /dev/null +++ b/libs/hexagent/sandbox/vm/setup_lite/steps/04_npm.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# NPM global packages (minimal baseline; install extras on demand) +set -euo pipefail + +if [[ "${OPENAGENT_USE_CN_MIRRORS:-0}" == "1" ]]; then + emit 04_npm progress "Configuring npm registry mirror" + npm config set registry "${OPENAGENT_NPM_REGISTRY}" >/dev/null 2>&1 || true +fi + +emit 04_npm progress "Installing npm global packages" +npm install -g typescript tsx playwright diff --git a/libs/hexagent/sandbox/vm/setup_lite/steps/05_pip.sh b/libs/hexagent/sandbox/vm/setup_lite/steps/05_pip.sh new file mode 100755 index 00000000..1960556a --- /dev/null +++ b/libs/hexagent/sandbox/vm/setup_lite/steps/05_pip.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Python packages (minimal baseline; install extras on demand) +set -euo pipefail + +emit 05_pip progress "Core data & visualization" +pip_install numpy pandas matplotlib pillow + +emit 05_pip progress "Web & HTTP" +pip_install requests beautifulsoup4 lxml + +emit 05_pip progress "Utilities" +pip_install \ + uv click pyyaml python-dotenv tabulate + +# Cleanup +emit 05_pip progress "Cleaning pip cache" +pip3 cache purge +rm -rf /root/.cache/pip /tmp/pip-* 2>/dev/null || true diff --git a/libs/hexagent/sandbox/vm/setup_lite/steps/06_playwright.sh b/libs/hexagent/sandbox/vm/setup_lite/steps/06_playwright.sh new file mode 100755 index 00000000..c1b344b8 --- /dev/null +++ b/libs/hexagent/sandbox/vm/setup_lite/steps/06_playwright.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# Playwright browsers + ImageMagick policy +set -euo pipefail + +# Recover any broken dpkg/apt state from prior steps +dpkg --configure -a || true +apt-get install -y -f || true + +# Install Playwright OS deps (apt) retry-wrapped +max_attempts=5 +deps_ok=0 +for ((attempt = 1; attempt <= max_attempts; attempt++)); do + emit 06_playwright progress "install-deps attempt $attempt/$max_attempts" + if npx playwright install-deps chromium; then + deps_ok=1 + break + fi + echo ">>> Retrying in 5s..." + sleep 5 + dpkg --configure -a || true + apt-get install -y -f || true + apt-get update || true +done + +if [[ $deps_ok -ne 1 ]]; then + emit 06_playwright error "Failed to install Playwright system dependencies" + exit 1 +fi + +# Download Chromium binary +emit 06_playwright progress "Downloading Chromium binary" +mirror_host="${OPENAGENT_PLAYWRIGHT_DOWNLOAD_HOST:-}" +browser_ok=0 + +# If bundled browsers are already present in the VM image, skip network download. +if find /opt/pw-browsers -type f \( -name "chrome-headless-shell" -o -name "chrome" \) 2>/dev/null | grep -q .; then + emit 06_playwright progress "Bundled Playwright browser detected under /opt/pw-browsers, skipping download" + browser_ok=1 +fi + +for ((attempt = 1; attempt <= max_attempts; attempt++)); do + if [[ $browser_ok -eq 1 ]]; then + break + fi + + if [[ "${OPENAGENT_USE_CN_MIRRORS:-0}" == "1" && -n "$mirror_host" ]]; then + emit 06_playwright progress "browser install attempt $attempt/$max_attempts (mirror)" + if PLAYWRIGHT_DOWNLOAD_HOST="$mirror_host" \ + PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers \ + npx playwright install chromium; then + browser_ok=1 + break + fi + emit 06_playwright progress "Mirror unavailable, retrying with official host" + else + emit 06_playwright progress "browser install attempt $attempt/$max_attempts" + fi + + if env -u PLAYWRIGHT_DOWNLOAD_HOST PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers npx playwright install chromium; then + browser_ok=1 + break + fi + echo ">>> Browser download attempt $attempt failed. Retrying in 5s..." + sleep 5 +done + +if [[ $browser_ok -ne 1 ]]; then + emit 06_playwright error "Failed to download Chromium browser" + exit 1 +fi + +# Allow PDF operations in ImageMagick (if restricted) +if [[ -f /etc/ImageMagick-6/policy.xml ]] && \ + grep -q 'rights="none" pattern="PDF"' /etc/ImageMagick-6/policy.xml; then + sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' \ + /etc/ImageMagick-6/policy.xml + emit 06_playwright progress "ImageMagick PDF policy updated" +fi diff --git a/libs/hexagent/sandbox/vm/setup_lite/steps/07_finalize.sh b/libs/hexagent/sandbox/vm/setup_lite/steps/07_finalize.sh new file mode 100755 index 00000000..5552893c --- /dev/null +++ b/libs/hexagent/sandbox/vm/setup_lite/steps/07_finalize.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Symlinks and session directory +set -euo pipefail + +emit 07_finalize progress "Creating python symlink" +ln -sf /usr/bin/python3 /usr/bin/python + +emit 07_finalize progress "Creating /sessions directory" +mkdir -p /sessions +chmod 755 /sessions diff --git a/libs/hexagent/sandbox/vm/setup_lite/steps/08_cleanup.sh b/libs/hexagent/sandbox/vm/setup_lite/steps/08_cleanup.sh new file mode 100755 index 00000000..e78b631c --- /dev/null +++ b/libs/hexagent/sandbox/vm/setup_lite/steps/08_cleanup.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Cache cleanup to minimize image size +set -euo pipefail + +emit 08_cleanup progress "Cleaning apt cache" +apt-get clean + +emit 08_cleanup progress "Removing temporary files" +rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 2>/dev/null || true +rm -rf /root/.cache /root/.npm/_cacache 2>/dev/null || true diff --git a/libs/hexagent/tests/unit_tests/computer/test_local_vm_win.py b/libs/hexagent/tests/unit_tests/computer/test_local_vm_win.py index f8fb5e71..6bccba0f 100644 --- a/libs/hexagent/tests/unit_tests/computer/test_local_vm_win.py +++ b/libs/hexagent/tests/unit_tests/computer/test_local_vm_win.py @@ -191,7 +191,8 @@ async def test_mount_idempotent_self_heals_missing_live_mount(self, tmp_path: An ] ) - await mgr.mount(Mount(source=str(d), target="code")) + with patch("hexagent.computer.local._wsl._win_path_to_wsl", return_value="/mnt/c/code"): + await mgr.mount(Mount(source=str(d), target="code")) vm.apply_mounts.assert_not_awaited() assert vm.shell.await_count == 3 diff --git a/libs/hexagent/tests/unit_tests/computer/test_vm.py b/libs/hexagent/tests/unit_tests/computer/test_vm.py index 05e6dc37..90e0a7eb 100644 --- a/libs/hexagent/tests/unit_tests/computer/test_vm.py +++ b/libs/hexagent/tests/unit_tests/computer/test_vm.py @@ -195,6 +195,7 @@ async def test_run_translates_vm_error_to_cli_error(self) -> None: with pytest.raises(CLIError, match="boom"): await computer.run("bad") + class TestUpload: """Tests for upload().""" diff --git a/libs/hexagent/tests/unit_tests/computer/test_wsl.py b/libs/hexagent/tests/unit_tests/computer/test_wsl.py index 36f90da2..f2ed9766 100644 --- a/libs/hexagent/tests/unit_tests/computer/test_wsl.py +++ b/libs/hexagent/tests/unit_tests/computer/test_wsl.py @@ -214,8 +214,8 @@ async def test_new_session_defaults_to_session_home(self) -> None: vm._vm = backend vm._instance = "openagent" vm._lock = asyncio.Lock() - vm._generate_unique_name = AsyncMock(return_value="alice") - vm._create_user = AsyncMock() + vm._generate_unique_name = AsyncMock(return_value="alice") # type: ignore[method-assign] + vm._create_user = AsyncMock() # type: ignore[method-assign] computer = await LocalVM.computer(vm) await computer.run("pwd") diff --git a/libs/hexagent/tests/unit_tests/harness/test_environment.py b/libs/hexagent/tests/unit_tests/harness/test_environment.py index 202d7b6f..a48fa284 100644 --- a/libs/hexagent/tests/unit_tests/harness/test_environment.py +++ b/libs/hexagent/tests/unit_tests/harness/test_environment.py @@ -6,8 +6,6 @@ from datetime import UTC, datetime from unittest.mock import AsyncMock -import pytest - from hexagent.harness.environment import EnvironmentResolver from hexagent.types import CLIResult diff --git a/libs/hexagent_demo/backend/hexagent_api/paths.py b/libs/hexagent_demo/backend/hexagent_api/paths.py index 2e252fd0..d2923c40 100644 --- a/libs/hexagent_demo/backend/hexagent_api/paths.py +++ b/libs/hexagent_demo/backend/hexagent_api/paths.py @@ -111,3 +111,13 @@ def vm_setup_dir() -> Path: system (Lima, WSL, cloud VMs, etc.). """ return vm_dir() / "setup" + + +def vm_setup_lite_dir() -> Path: + """Lite VM setup scripts (``sandbox/vm/setup_lite/``). + + Minimal baseline variant for demo/Electron deployments. Installs only + the packages needed for the demo and defers heavier dependencies to + on-demand installation. Supports China-region mirrors. + """ + return vm_dir() / "setup_lite" diff --git a/libs/hexagent_demo/backend/hexagent_api/routes/setup.py b/libs/hexagent_demo/backend/hexagent_api/routes/setup.py index fbddd5d4..a12ba5c7 100644 --- a/libs/hexagent_demo/backend/hexagent_api/routes/setup.py +++ b/libs/hexagent_demo/backend/hexagent_api/routes/setup.py @@ -27,7 +27,7 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import Response, StreamingResponse -from hexagent_api.paths import data_dir, deps_dir, vm_lima_dir, vm_setup_dir +from hexagent_api.paths import data_dir, deps_dir, vm_lima_dir, vm_setup_dir, vm_setup_lite_dir logger = logging.getLogger(__name__) @@ -1227,7 +1227,7 @@ async def _run_lima(self, **kwargs: object) -> None: # 2. Copy setup directory into VM self._emit("progress", {"step": "copying", "message": "Copying setup files to VM..."}) - setup_dir = vm_setup_dir() + setup_dir = vm_setup_lite_dir() if not setup_dir.is_dir(): self._emit("error", {"message": f"Setup directory not found: {setup_dir}"}) self._status = "error" @@ -1238,7 +1238,7 @@ async def _run_lima(self, **kwargs: object) -> None: with tempfile.TemporaryDirectory(prefix="hexagent_setup_") as tmp: tar_path = os.path.join(tmp, "setup.tar.gz") _sp.run( - ["tar", "-czf", tar_path, "-C", str(setup_dir.parent), "setup"], + ["tar", "-czf", tar_path, "-C", str(setup_dir.parent), setup_dir.name], check=True, ) copy_proc = await asyncio.create_subprocess_exec( @@ -1334,7 +1334,7 @@ async def _run_wsl(self, **kwargs: object) -> None: return self._emit("progress", {"step": "copying", "message": "Preparing setup files in WSL..."}) - setup_dir = vm_setup_dir() + setup_dir = vm_setup_lite_dir() if not setup_dir.is_dir(): self._emit("error", {"message": f"Setup directory not found: {setup_dir}"}) self._status = "error" diff --git a/libs/openagent/sandbox/vm/wsl/prebuilt/openagent-prebuilt.tar b/libs/openagent/sandbox/vm/wsl/prebuilt/openagent-prebuilt.tar deleted file mode 100644 index 717a5a6d..00000000 --- a/libs/openagent/sandbox/vm/wsl/prebuilt/openagent-prebuilt.tar +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb39247403141d4bd20516ce9fbb25d5956a11f838e577959a7bed4bc6514a79 -size 2019061760 diff --git a/reports/vm-prebuilt-inventory-20260325/apt-installed-new.txt b/reports/vm-prebuilt-inventory-20260325/apt-installed-new.txt deleted file mode 100644 index 42f8bd6ff8c23dc3f52b2957a960fff46f4aa2c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3090 ycmezW&z8ZKftP^`NRHB@;V_yGM)SdFIWSrdjFtnV<-lk;Fj@|bmIEWL8~^|WaonH) diff --git a/reports/vm-prebuilt-inventory-20260325/apt-installed.txt b/reports/vm-prebuilt-inventory-20260325/apt-installed.txt deleted file mode 100644 index df481158..00000000 --- a/reports/vm-prebuilt-inventory-20260325/apt-installed.txt +++ /dev/null @@ -1,964 +0,0 @@ -adduser==3.137ubuntu1 -adwaita-icon-theme==46.0-1 -apparmor==4.0.1really4.0.1-0ubuntu0.24.04.5 -apport==2.28.1-0ubuntu3.8 -apport-core-dump-handler==2.28.1-0ubuntu3.8 -apport-symptoms==0.25 -appstream==1.0.2-1build6 -apt==2.8.3 -apt-transport-https==2.8.3 -apt-utils==2.8.3 -at-spi2-common==2.52.0-1build1 -at-spi2-core==2.52.0-1build1 -base-files==13ubuntu10.4 -base-passwd==3.6.3build1 -bash==5.2.21-2ubuntu4 -bash-completion==1:2.11-8 -bc==1.07.1-3ubuntu4 -binutils==2.42-4ubuntu2.8 -binutils-common==2.42-4ubuntu2.8 -binutils-x86-64-linux-gnu==2.42-4ubuntu2.8 -bsdextrautils==2.39.3-9ubuntu6.5 -bsdutils==1:2.39.3-9ubuntu6.5 -build-essential==12.10ubuntu1 -byobu==6.11-0ubuntu1 -bzip2==1.0.8-5.1build0.1 -ca-certificates==20240203 -ca-certificates-java==20240118 -cloud-guest-utils==0.33-1 -cloud-init==25.2-0ubuntu1~24.04.1 -command-not-found==23.04.0 -console-setup==1.226ubuntu1 -console-setup-linux==1.226ubuntu1 -coreutils==9.4-3ubuntu6.2 -cpp==4:13.2.0-7ubuntu1 -cpp-13==13.3.0-6ubuntu2~24.04.1 -cpp-13-x86-64-linux-gnu==13.3.0-6ubuntu2~24.04.1 -cpp-x86-64-linux-gnu==4:13.2.0-7ubuntu1 -cron==3.0pl1-184ubuntu2 -cron-daemon-common==3.0pl1-184ubuntu2 -curl==8.5.0-2ubuntu10.8 -dash==0.5.12-6ubuntu5 -dbus==1.14.10-4ubuntu4.1 -dbus-bin==1.14.10-4ubuntu4.1 -dbus-daemon==1.14.10-4ubuntu4.1 -dbus-session-bus-common==1.14.10-4ubuntu4.1 -dbus-system-bus-common==1.14.10-4ubuntu4.1 -dbus-user-session==1.14.10-4ubuntu4.1 -dbus-x11==1.14.10-4ubuntu4.1 -dconf-gsettings-backend==0.40.0-4ubuntu0.1 -dconf-service==0.40.0-4ubuntu0.1 -debconf==1.5.86ubuntu1 -debconf-i18n==1.5.86ubuntu1 -debianutils==5.17build1 -default-jre-headless==2:1.21-75+exp1 -dhcpcd-base==1:10.0.6-1ubuntu3.2 -diffutils==1:3.10-1build1 -dirmngr==2.4.4-2ubuntu17.4 -distro-info==1.7build1 -distro-info-data==0.60ubuntu0.5 -dmsetup==2:1.02.185-3ubuntu3.2 -dpkg==1.22.6ubuntu6.5 -dpkg-dev==1.22.6ubuntu6.5 -e2fsprogs==1.47.0-2.4~exp1ubuntu4.1 -e2fsprogs-l10n==1.47.0-2.4~exp1ubuntu4.1 -eatmydata==131-1ubuntu1 -ed==1.20.1-1 -eject==2.39.3-9ubuntu6.5 -ethtool==1:6.7-1build1 -fdisk==2.39.3-9ubuntu6.5 -ffmpeg==7:6.1.1-3ubuntu5 -file==1:5.45-3build1 -findutils==4.9.0-5build1 -fontconfig==2.15.0-1.1ubuntu2 -fontconfig-config==2.15.0-1.1ubuntu2 -fonts-crosextra-caladea==20200211-2 -fonts-crosextra-carlito==20230309-2 -fonts-dejavu==2.37-8 -fonts-dejavu-core==2.37-8 -fonts-dejavu-extra==2.37-8 -fonts-dejavu-mono==2.37-8 -fonts-freefont-ttf==20211204+svn4273-2 -fonts-gfs-baskerville==1.1-6 -fonts-gfs-porson==1.1-7 -fonts-ipafont-gothic==00303-21ubuntu1 -fonts-liberation==1:2.1.5-3 -fonts-liberation2==1:2.1.5-3 -fonts-lmodern==2.005-1 -fonts-noto-cjk==1:20230817+repack1-3 -fonts-noto-color-emoji==2.047-0ubuntu0.24.04.1 -fonts-opensymbol==4:102.12+LibO24.2.7-0ubuntu0.24.04.4 -fonts-texgyre==20180621-6 -fonts-ubuntu==0.869+git20240321-0ubuntu1 -fonts-urw-base35==20200910-8 -fonts-wqy-zenhei==0.9.45-8 -fuse3==3.14.0-5build1 -g++==4:13.2.0-7ubuntu1 -g++-13==13.3.0-6ubuntu2~24.04.1 -g++-13-x86-64-linux-gnu==13.3.0-6ubuntu2~24.04.1 -g++-x86-64-linux-gnu==4:13.2.0-7ubuntu1 -gawk==1:5.2.1-2build3 -gcc==4:13.2.0-7ubuntu1 -gcc-13==13.3.0-6ubuntu2~24.04.1 -gcc-13-base==13.3.0-6ubuntu2~24.04.1 -gcc-13-x86-64-linux-gnu==13.3.0-6ubuntu2~24.04.1 -gcc-14-base==14.2.0-4ubuntu2~24.04.1 -gcc-x86-64-linux-gnu==4:13.2.0-7ubuntu1 -gdisk==1.0.10-1build1 -gettext-base==0.21-14ubuntu2 -ghostscript==10.02.1~dfsg1-0ubuntu7.8 -gir1.2-girepository-2.0==1.80.1-1 -gir1.2-glib-2.0==2.80.0-6ubuntu3.8 -gir1.2-packagekitglib-1.0==1.2.8-2ubuntu1.4 -git==1:2.43.0-1ubuntu7.3 -git-man==1:2.43.0-1ubuntu7.3 -gnupg==2.4.4-2ubuntu17.4 -gnupg-l10n==2.4.4-2ubuntu17.4 -gnupg-utils==2.4.4-2ubuntu17.4 -gpg==2.4.4-2ubuntu17.4 -gpg-agent==2.4.4-2ubuntu17.4 -gpgconf==2.4.4-2ubuntu17.4 -gpgsm==2.4.4-2ubuntu17.4 -gpgv==2.4.4-2ubuntu17.4 -gpg-wks-client==2.4.4-2ubuntu17.4 -graphviz==2.42.2-9ubuntu0.1 -grep==3.11-4build1 -groff-base==1.23.0-3build2 -gsettings-desktop-schemas==46.1-0ubuntu1 -gtk-update-icon-cache==3.24.41-4ubuntu1.3 -gzip==1.12-1ubuntu3.1 -hicolor-icon-theme==0.17-2 -hostname==3.23+nmu2ubuntu2 -humanity-icon-theme==0.6.16 -imagemagick==8:6.9.12.98+dfsg1-5.2build2 -imagemagick-6.q16==8:6.9.12.98+dfsg1-5.2build2 -imagemagick-6-common==8:6.9.12.98+dfsg1-5.2build2 -info==7.1-3build2 -init==1.66ubuntu1 -init-system-helpers==1.66ubuntu1 -install-info==7.1-3build2 -iproute2==6.1.0-1ubuntu6.2 -iputils-ping==3:20240117-1ubuntu0.1 -iso-codes==4.16.0-1 -java-common==0.75+exp1 -jq==1.7.1-3ubuntu0.24.04.1 -kbd==2.6.4-2ubuntu2 -keyboard-configuration==1.226ubuntu1 -keyboxd==2.4.4-2ubuntu17.4 -kmod==31+20240202-2ubuntu7.1 -krb5-locales==1.20.1-6ubuntu2.6 -landscape-client==24.02-0ubuntu5.7 -landscape-common==24.02-0ubuntu5.7 -latexmk==1:4.83-1 -less==590-2ubuntu2.1 -libabsl20220623t64==20220623.1-3.1ubuntu3.2 -libabw-0.1-1==0.1.3-1build4 -libacl1==2.3.2-1build1.1 -libann0==1.1.2+doc-9build1 -libaom3==3.8.2-2ubuntu0.1 -libapache-pom-java==29-2 -libapparmor1==4.0.1really4.0.1-0ubuntu0.24.04.5 -libappstream5==1.0.2-1build6 -libapt-pkg6.0t64==2.8.3 -libarchive13t64==3.7.2-2ubuntu0.5 -libargon2-1==0~20190702+dfsg-4build1 -libasan8==14.2.0-4ubuntu2~24.04.1 -libasound2-data==1.2.11-1ubuntu0.2 -libasound2t64==1.2.11-1ubuntu0.2 -libass9==1:0.17.1-2build1 -libassuan0==2.5.6-1build1 -libasyncns0==0.8-6build4 -libatk1.0-0t64==2.52.0-1build1 -libatk-bridge2.0-0t64==2.52.0-1build1 -libatm1t64==1:2.5.1-5.1build1 -libatomic1==14.2.0-4ubuntu2~24.04.1 -libatspi2.0-0t64==2.52.0-1build1 -libattr1==1:2.5.2-1build1.1 -libaudit1==1:3.1.2-2.1build1.1 -libaudit-common==1:3.1.2-2.1build1.1 -libavahi-client3==0.8-13ubuntu6.1 -libavahi-common3==0.8-13ubuntu6.1 -libavahi-common-data==0.8-13ubuntu6.1 -libavc1394-0==0.5.4-5build3 -libavcodec60==7:6.1.1-3ubuntu5 -libavdevice60==7:6.1.1-3ubuntu5 -libavfilter9==7:6.1.1-3ubuntu5 -libavformat60==7:6.1.1-3ubuntu5 -libavutil58==7:6.1.1-3ubuntu5 -libbcprov-java==1.77-1 -libbinutils==2.42-4ubuntu2.8 -libblas3==3.12.0-3build1.1 -libblkid1==2.39.3-9ubuntu6.5 -libblkid-dev==2.39.3-9ubuntu6.5 -libbluray2==1:1.3.4-1build1 -libboost-iostreams1.83.0==1.83.0-2.1ubuntu3.2 -libboost-locale1.83.0==1.83.0-2.1ubuntu3.2 -libboost-thread1.83.0==1.83.0-2.1ubuntu3.2 -libbpf1==1:1.3.0-2build2 -libbrotli1==1.1.0-2build2 -libbrotli-dev==1.1.0-2build2 -libbs2b0==3.1.0+dfsg-7build1 -libbsd0==0.12.1-1build1.1 -libbz2-1.0==1.0.8-5.1build0.1 -libbz2-dev==1.0.8-5.1build0.1 -libc6==2.39-0ubuntu8.7 -libc6-dev==2.39-0ubuntu8.7 -libcaca0==0.99.beta20-4ubuntu0.1 -libcairo2==1.18.0-3build1 -libcairo2-dev==1.18.0-3build1 -libcairo-gobject2==1.18.0-3build1 -libcairo-script-interpreter2==1.18.0-3build1 -libcap2==1:2.66-5ubuntu2.2 -libcap2-bin==1:2.66-5ubuntu2.2 -libcap-ng0==0.8.4-2build2 -libc-bin==2.39-0ubuntu8.7 -libcbor0.10==0.10.2-1.2ubuntu2 -libcc1-0==14.2.0-4ubuntu2~24.04.1 -libc-dev-bin==2.39-0ubuntu8.7 -libcdio19t64==2.1.0-4.1ubuntu1.2 -libcdio-cdda2t64==10.2+2.0.1-1.1build2 -libcdio-paranoia2t64==10.2+2.0.1-1.1build2 -libcdr-0.1-1==0.1.7-1build2 -libcdt5==2.42.2-9ubuntu0.1 -libcgraph6==2.42.2-9ubuntu0.1 -libchromaprint1==1.5.1-5 -libcjson1==1.7.17-1 -libclucene-contribs1t64==2.3.3.4+dfsg-1.2ubuntu2 -libclucene-core1t64==2.3.3.4+dfsg-1.2ubuntu2 -libcodec2-1.2==1.2.0-2build1 -libcolamd3==1:7.6.1+dfsg-1build1 -libcolord2==1.4.7-1build2 -libcom-err2==1.47.0-2.4~exp1ubuntu4.1 -libcommons-lang3-java==3.14.0-1 -libcommons-logging-java==1.3.0-1ubuntu1 -libcommons-parent-java==56-1 -libcrypt1==1:4.4.36-4build1 -libcrypt-dev==1:4.4.36-4build1 -libcryptsetup12==2:2.7.0-1ubuntu4.2 -libctf0==2.42-4ubuntu2.8 -libctf-nobfd0==2.42-4ubuntu2.8 -libcups2t64==2.4.7-1.2ubuntu7.9 -libcurl3t64-gnutls==8.5.0-2ubuntu10.8 -libcurl4t64==8.5.0-2ubuntu10.8 -libdatrie1==0.2.13-3build1 -libdav1d7==1.4.1-1build1 -libdb5.3t64==5.3.28+dfsg2-7 -libdbus-1-3==1.14.10-4ubuntu4.1 -libdc1394-25==2.2.6-4build1 -libdconf1==0.40.0-4ubuntu0.1 -libde265-0==1.0.15-1build3 -libdebconfclient0==0.271ubuntu3 -libdecor-0-0==0.2.2-1build2 -libdeflate0==1.19-1build1.1 -libdevmapper1.02.1==2:1.02.185-3ubuntu3.2 -libdouble-conversion3==3.3.0-1build1 -libdpkg-perl==1.22.6ubuntu6.5 -libdrm2==2.4.125-1ubuntu0.1~24.04.1 -libdrm-amdgpu1==2.4.125-1ubuntu0.1~24.04.1 -libdrm-common==2.4.125-1ubuntu0.1~24.04.1 -libdrm-intel1==2.4.125-1ubuntu0.1~24.04.1 -libduktape207==2.7.0+tests-0ubuntu3 -libdw1t64==0.190-1.1ubuntu0.1 -libeatmydata1==131-1ubuntu1 -libe-book-0.1-1==0.1.3-2build6 -libedit2==3.1-20230828-1build1 -libegl1==1.7.0-1build1 -libegl-mesa0==25.2.8-0ubuntu0.24.04.1 -libelf1t64==0.190-1.1ubuntu0.1 -libeot0==0.01-5build3 -libepoxy0==1.5.10-1build1 -libepubgen-0.1-1==0.1.1-1ubuntu6 -liberror-perl==0.17029-2 -libestr0==0.1.11-1build1 -libetonyek-0.1-1==0.1.10-5build1 -libevdev2==1.13.1+dfsg-1build1 -libevent-core-2.1-7t64==2.1.12-stable-9ubuntu2 -libexpat1==2.6.1-2ubuntu0.4 -libexpat1-dev==2.6.1-2ubuntu0.4 -libext2fs2t64==1.47.0-2.4~exp1ubuntu4.1 -libexttextcat-2.0-0==3.4.7-1build1 -libexttextcat-data==3.4.7-1build1 -libfastjson4==1.2304.0-1build1 -libfdisk1==2.39.3-9ubuntu6.5 -libffi8==3.4.6-1build1 -libffi-dev==3.4.6-1build1 -libfftw3-double3==3.3.10-1ubuntu3 -libfido2-1==1.14.0-1build3 -libflac12t64==1.4.3+ds-2.1ubuntu2 -libflite1==2.2-6build3 -libfontbox-java==1:1.8.16-5 -libfontconfig1==2.15.0-1.1ubuntu2 -libfontconfig-dev==2.15.0-1.1ubuntu2 -libfontenc1==1:1.1.8-1build1 -libfreehand-0.1-1==0.1.2-3build3 -libfreetype6==2.13.2+dfsg-1ubuntu0.1 -libfreetype-dev==2.13.2+dfsg-1ubuntu0.1 -libfribidi0==1.0.13-3build1 -libfuse3-3==3.14.0-5build1 -libgbm1==25.2.8-0ubuntu0.24.04.1 -libgcc-13-dev==13.3.0-6ubuntu2~24.04.1 -libgcc-s1==14.2.0-4ubuntu2~24.04.1 -libgcrypt20==1.10.3-2build1 -libgd3==2.3.3-9ubuntu5 -libgdbm6t64==1.23-5.1build1 -libgdbm-compat4t64==1.23-5.1build1 -libgdk-pixbuf-2.0-0==2.42.10+dfsg-3ubuntu3.2 -libgdk-pixbuf2.0-bin==2.42.10+dfsg-3ubuntu3.2 -libgdk-pixbuf2.0-common==2.42.10+dfsg-3ubuntu3.2 -libgfortran5==14.2.0-4ubuntu2~24.04.1 -libgif7==5.2.2-1ubuntu1 -libgirepository-1.0-1==1.80.1-1 -libgirepository-2.0-0==2.80.0-6ubuntu3.8 -libgl1==1.7.0-1build1 -libgl1-mesa-dri==25.2.8-0ubuntu0.24.04.1 -libgles2==1.7.0-1build1 -libglib2.0-0t64==2.80.0-6ubuntu3.8 -libglib2.0-bin==2.80.0-6ubuntu3.8 -libglib2.0-data==2.80.0-6ubuntu3.8 -libglib2.0-dev==2.80.0-6ubuntu3.8 -libglib2.0-dev-bin==2.80.0-6ubuntu3.8 -libglvnd0==1.7.0-1build1 -libglx0==1.7.0-1build1 -libglx-mesa0==25.2.8-0ubuntu0.24.04.1 -libgme0==0.6.3-7build1 -libgmp10==2:6.3.0+dfsg-2ubuntu6.1 -libgnutls30t64==3.8.3-1.1ubuntu3.4 -libgomp1==14.2.0-4ubuntu2~24.04.1 -libgpg-error0==1.47-3build2.1 -libgpg-error-l10n==1.47-3build2.1 -libgpgme11t64==1.18.0-4.1ubuntu4 -libgpgmepp6t64==1.18.0-4.1ubuntu4 -libgpm2==1.20.7-11 -libgprofng0==2.42-4ubuntu2.8 -libgraphene-1.0-0==1.10.8-3build2 -libgraphite2-3==1.3.14-2build1 -libgs10==10.02.1~dfsg1-0ubuntu7.8 -libgs10-common==10.02.1~dfsg1-0ubuntu7.8 -libgs-common==10.02.1~dfsg1-0ubuntu7.8 -libgsm1==1.0.22-1build1 -libgssapi-krb5-2==1.20.1-6ubuntu2.6 -libgstreamer1.0-0==1.24.2-1ubuntu0.1 -libgstreamer-plugins-base1.0-0==1.24.2-1ubuntu0.3 -libgtk-3-0t64==3.24.41-4ubuntu1.3 -libgtk-3-bin==3.24.41-4ubuntu1.3 -libgtk-3-common==3.24.41-4ubuntu1.3 -libgtk-4-1==4.14.5+ds-0ubuntu0.9 -libgtk-4-common==4.14.5+ds-0ubuntu0.9 -libgts-0.7-5t64==0.7.6+darcs121130-5.2build1 -libgudev-1.0-0==1:238-5ubuntu1 -libgvc6==2.42.2-9ubuntu0.1 -libgvpr2==2.42.2-9ubuntu0.1 -libharfbuzz0b==8.3.0-2build2 -libharfbuzz-icu0==8.3.0-2build2 -libheif1==1.17.6-1ubuntu4.2 -libheif-plugin-aomdec==1.17.6-1ubuntu4.2 -libheif-plugin-libde265==1.17.6-1ubuntu4.2 -libhogweed6t64==3.9.1-2.2build1.1 -libhunspell-1.7-0==1.7.2+really1.7.2-10build3 -libhwasan0==14.2.0-4ubuntu2~24.04.1 -libhwy1t64==1.0.7-8.1build1 -libhyphen0==2.8.8-7build3 -libice6==2:1.0.10-1build3 -libice-dev==2:1.0.10-1build3 -libicu74==74.2-1ubuntu3.1 -libidn12==1.42-1build1 -libidn2-0==2.3.7-2build1.1 -libiec61883-0==1.2.0-6build1 -libijs-0.35==0.35-15.1build1 -libinput10==1.25.0-1ubuntu3.3 -libinput-bin==1.25.0-1ubuntu3.3 -libisl23==0.26-3build1.1 -libitm1==14.2.0-4ubuntu2~24.04.1 -libjack-jackd2-0==1.9.21~dfsg-3ubuntu3 -libjansson4==2.14-2build2 -libjbig0==2.1-6.1ubuntu2 -libjbig2dec0==0.20-1build3 -libjpeg8==8c-2ubuntu11 -libjpeg-turbo8==2.1.5-2ubuntu2 -libjq1==1.7.1-3ubuntu0.24.04.1 -libjs-jquery==3.6.1+dfsg+~3.5.14-1 -libjson-c5==0.17-1build1 -libjs-sphinxdoc==7.2.6-6 -libjs-underscore==1.13.4~dfsg+~1.11.4-3 -libjxl0.7==0.7.0-10.2ubuntu6.1 -libk5crypto3==1.20.1-6ubuntu2.6 -libkeyutils1==1.6.3-3build1 -libkmod2==31+20240202-2ubuntu7.1 -libkpathsea6==2023.20230311.66589-9build3 -libkrb5-3==1.20.1-6ubuntu2.6 -libkrb5support0==1.20.1-6ubuntu2.6 -libksba8==1.6.6-1build1 -liblab-gamut1==2.42.2-9ubuntu0.1 -liblangtag1==0.6.7-1build2 -liblangtag-common==0.6.7-1build2 -liblapack3==3.12.0-3build1.1 -liblcms2-2==2.14-2build1 -libldap2==2.6.10+dfsg-0ubuntu0.24.04.1 -libldap-common==2.6.10+dfsg-0ubuntu0.24.04.1 -liblept5==1.82.0-3build4 -liblerc4==4.0.0+ds-4ubuntu2 -liblibreoffice-java==4:24.2.7-0ubuntu0.24.04.4 -liblilv-0-0==0.24.22-1build1 -libllvm20==1:20.1.2-0ubuntu1~24.04.2 -liblocale-gettext-perl==1.07-6ubuntu5 -liblqr-1-0==0.4.2-2.1build2 -liblsan0==14.2.0-4ubuntu2~24.04.1 -libltdl7==2.4.7-7build1 -liblua5.4-0==5.4.6-3build2 -liblz4-1==1.9.4-1build1.1 -liblzma5==5.6.1+really5.4.5-1ubuntu0.2 -liblzo2-2==2.10-2build4 -libmagic1t64==1:5.45-3build1 -libmagickcore-6.q16-7t64==8:6.9.12.98+dfsg1-5.2build2 -libmagickwand-6.q16-7t64==8:6.9.12.98+dfsg1-5.2build2 -libmagic-mgc==1:5.45-3build1 -libmbedcrypto7t64==2.28.8-1 -libmd0==1.1.0-2build1.1 -libmd4c0==0.4.8-1build1 -libmhash2==0.9.9.9-9build3 -libmnl0==1.0.5-2build1 -libmount1==2.39.3-9ubuntu6.5 -libmount-dev==2.39.3-9ubuntu6.5 -libmp3lame0==3.100-6build1 -libmpc3==1.3.1-1build1.1 -libmpfr6==4.2.1-1build1.1 -libmpg123-0t64==1.32.5-1ubuntu1.1 -libmspub-0.1-1==0.1.4-3build7 -libmtdev1t64==1.1.6-1.1build1 -libmwaw-0.3-3==0.3.22-1build1 -libmysofa1==1.3.2+dfsg-2ubuntu2 -libmythes-1.2-0==2:1.2.5-1build1 -libncursesw6==6.4+20240113-1ubuntu2 -libnetplan1==1.1.2-8ubuntu1~24.04.1 -libnettle8t64==3.9.1-2.2build1.1 -libnewt0.52==0.52.24-2ubuntu2 -libnghttp2-14==1.59.0-1ubuntu0.2 -libnorm1t64==1.5.9+dfsg-3.1build1 -libnpth0t64==1.6-3.1build1 -libnspr4==2:4.35-1.1build1 -libnss3==2:3.98-1ubuntu0.1 -libnss3-tools==2:3.98-1ubuntu0.1 -libnss-systemd==255.4-1ubuntu8.12 -libnuma1==2.0.18-1ubuntu0.24.04.1 -libodfgen-0.1-1==0.1.8-2build3 -libogg0==1.3.5-3build1 -libonig5==6.9.9-1build1 -libopenal1==1:1.23.1-4build1 -libopenal-data==1:1.23.1-4build1 -libopenjp2-7==2.5.0-2ubuntu0.4 -libopenmpt0t64==0.7.3-1.1build3 -libopus0==1.4-1build1 -liborc-0.4-0t64==1:0.4.38-1ubuntu0.1 -liborcus-0.18-0==0.19.2-3build3 -liborcus-parser-0.18-0==0.19.2-3build3 -libp11-kit0==0.25.3-4ubuntu2.1 -libpackagekit-glib2-18==1.2.8-2ubuntu1.4 -libpagemaker-0.0-0==0.0.4-1build4 -libpam0g==1.5.3-5ubuntu5.5 -libpam-cap==1:2.66-5ubuntu2.2 -libpam-modules==1.5.3-5ubuntu5.5 -libpam-modules-bin==1.5.3-5ubuntu5.5 -libpam-runtime==1.5.3-5ubuntu5.5 -libpam-systemd==255.4-1ubuntu8.12 -libpango-1.0-0==1.52.1+ds-1build1 -libpangocairo-1.0-0==1.52.1+ds-1build1 -libpangoft2-1.0-0==1.52.1+ds-1build1 -libpaper1==1.1.29build1 -libpaper-utils==1.1.29build1 -libpathplan4==2.42.2-9ubuntu0.1 -libpciaccess0==0.17-3ubuntu0.24.04.2 -libpcre2-16-0==10.42-4ubuntu2.1 -libpcre2-32-0==10.42-4ubuntu2.1 -libpcre2-8-0==10.42-4ubuntu2.1 -libpcre2-dev==10.42-4ubuntu2.1 -libpcre2-posix3==10.42-4ubuntu2.1 -libpcsclite1==2.0.3-1build1 -libpdfbox-java==1:1.8.16-5 -libperl5.38t64==5.38.2-3.2ubuntu0.2 -libpgm-5.3-0t64==5.3.128~dfsg-2.1build1 -libpipeline1==1.5.7-2 -libpixman-1-0==0.42.2-1build1 -libpixman-1-dev==0.42.2-1build1 -libpkgconf3==1.8.1-2build1 -libplacebo338==6.338.2-2build1 -libpng16-16t64==1.6.43-5ubuntu0.5 -libpng-dev==1.6.43-5ubuntu0.5 -libpocketsphinx3==0.8.0+real5prealpha+1-15ubuntu5 -libpolkit-agent-1-0==124-2ubuntu1.24.04.2 -libpolkit-gobject-1-0==124-2ubuntu1.24.04.2 -libpoppler134==24.02.0-1ubuntu9.8 -libpopt0==1.19+dfsg-1build1 -libpostproc57==7:6.1.1-3ubuntu5 -libpotrace0==1.16-2build1 -libproc2-0==2:4.0.4-4ubuntu3.2 -libpsl5t64==0.21.2-1.1build1 -libptexenc1==2023.20230311.66589-9build3 -libpthread-stubs0-dev==0.4-1build3 -libpulse0==1:16.1+dfsg1-2ubuntu10.1 -libpython3.12-dev==3.12.3-1ubuntu0.12 -libpython3.12-minimal==3.12.3-1ubuntu0.12 -libpython3.12-stdlib==3.12.3-1ubuntu0.12 -libpython3.12t64==3.12.3-1ubuntu0.12 -libpython3-dev==3.12.3-0ubuntu2.1 -libpython3-stdlib==3.12.3-0ubuntu2.1 -libqpdf29t64==11.9.0-1.1ubuntu0.1 -libqt5core5t64==5.15.13+dfsg-1ubuntu1 -libqt5dbus5t64==5.15.13+dfsg-1ubuntu1 -libqt5gui5t64==5.15.13+dfsg-1ubuntu1 -libqt5network5t64==5.15.13+dfsg-1ubuntu1 -libqt5positioning5==5.15.13+dfsg-1 -libqt5printsupport5t64==5.15.13+dfsg-1ubuntu1 -libqt5qml5==5.15.13+dfsg-1ubuntu0.1 -libqt5qmlmodels5==5.15.13+dfsg-1ubuntu0.1 -libqt5quick5==5.15.13+dfsg-1ubuntu0.1 -libqt5sensors5==5.15.13-1 -libqt5svg5==5.15.13-1 -libqt5webchannel5==5.15.13-1 -libqt5webkit5==5.212.0~alpha4-36 -libqt5widgets5t64==5.15.13+dfsg-1ubuntu1 -libquadmath0==14.2.0-4ubuntu2~24.04.1 -librabbitmq4==0.11.0-1build2 -libraptor2-0==2.0.16-3ubuntu0.1 -librasqal3t64==0.9.33-2.1build1 -librav1e0==0.7.1-2 -libraw1394-11==2.1.2-2build3 -libraw23t64==0.21.2-2.1ubuntu0.24.04.1 -librdf0t64==1.0.17-3.1ubuntu3 -libreadline8t64==8.2-4build1 -libreoffice-base-core==4:24.2.7-0ubuntu0.24.04.4 -libreoffice-calc==4:24.2.7-0ubuntu0.24.04.4 -libreoffice-common==4:24.2.7-0ubuntu0.24.04.4 -libreoffice-core==4:24.2.7-0ubuntu0.24.04.4 -libreoffice-draw==4:24.2.7-0ubuntu0.24.04.4 -libreoffice-impress==4:24.2.7-0ubuntu0.24.04.4 -libreoffice-java-common==4:24.2.7-0ubuntu0.24.04.4 -libreoffice-style-colibre==4:24.2.7-0ubuntu0.24.04.4 -libreoffice-uiconfig-calc==4:24.2.7-0ubuntu0.24.04.4 -libreoffice-uiconfig-common==4:24.2.7-0ubuntu0.24.04.4 -libreoffice-uiconfig-draw==4:24.2.7-0ubuntu0.24.04.4 -libreoffice-uiconfig-impress==4:24.2.7-0ubuntu0.24.04.4 -libreoffice-uiconfig-writer==4:24.2.7-0ubuntu0.24.04.4 -libreoffice-writer==4:24.2.7-0ubuntu0.24.04.4 -librevenge-0.0-0==0.0.5-3build1 -librist4==0.2.10+dfsg-2 -librsvg2-2==2.58.0+dfsg-1build1 -librsvg2-common==2.58.0+dfsg-1build1 -librtmp1==2.4+20151223.gitfa8646d.1-2build7 -librubberband2==3.3.0+dfsg-2build1 -libsamplerate0==0.2.2-4build1 -libsasl2-2==2.1.28+dfsg1-5ubuntu3.1 -libsasl2-modules==2.1.28+dfsg1-5ubuntu3.1 -libsasl2-modules-db==2.1.28+dfsg1-5ubuntu3.1 -libsdl2-2.0-0==2.30.0+dfsg-1ubuntu3.1 -libseccomp2==2.5.5-1ubuntu3.1 -libselinux1==3.5-2ubuntu2.1 -libselinux1-dev==3.5-2ubuntu2.1 -libsemanage2==3.5-1build5 -libsemanage-common==3.5-1build5 -libsensors5==1:3.6.0-9build1 -libsensors-config==1:3.6.0-9build1 -libsepol2==3.5-2build1 -libsepol-dev==3.5-2build1 -libserd-0-0==0.32.2-1 -libsframe1==2.42-4ubuntu2.8 -libsharpyuv0==1.3.2-0.4build3 -libshine3==3.1.1-2build1 -libsigsegv2==2.14-1ubuntu2 -libslang2==2.3.3-3build2 -libsm6==2:1.2.3-1build3 -libsmartcols1==2.39.3-9ubuntu6.5 -libsm-dev==2:1.2.3-1build3 -libsnappy1v5==1.1.10-1build1 -libsndfile1==1.2.2-1ubuntu5.24.04.1 -libsndio7.0==1.9.0-0.3build3 -libsodium23==1.0.18-1ubuntu0.24.04.1 -libsord-0-0==0.16.16-2build1 -libsoxr0==0.1.3-4build3 -libspeex1==1.2.1-2ubuntu2.24.04.1 -libsphinxbase3t64==0.8+5prealpha+1-17build2 -libsqlite3-0==3.45.1-1ubuntu2.5 -libsratom-0-0==0.6.16-1build1 -libsrt1.5-gnutls==1.5.3-1build2 -libss2==1.47.0-2.4~exp1ubuntu4.1 -libssh-4==0.10.6-2ubuntu0.4 -libssh-gcrypt-4==0.10.6-2ubuntu0.4 -libssl3t64==3.0.13-0ubuntu3.7 -libstdc++-13-dev==13.3.0-6ubuntu2~24.04.1 -libstdc++6==14.2.0-4ubuntu2~24.04.1 -libstemmer0d==2.2.0-4build1 -libsuitesparseconfig7==1:7.6.1+dfsg-1build1 -libsvtav1enc1d1==1.7.0+dfsg-2build1 -libswresample4==7:6.1.1-3ubuntu5 -libswscale7==7:6.1.1-3ubuntu5 -libsynctex2==2023.20230311.66589-9build3 -libsystemd0==255.4-1ubuntu8.12 -libsystemd-shared==255.4-1ubuntu8.12 -libtasn1-6==4.19.0-3ubuntu0.24.04.2 -libteckit0==2.5.12+ds1-1 -libtesseract5==5.3.4-1build5 -libtexlua53-5==2023.20230311.66589-9build3 -libtext-charwidth-perl==0.04-11build3 -libtext-iconv-perl==1.7-8build3 -libtext-wrapi18n-perl==0.06-10 -libthai0==0.1.29-2build1 -libthai-data==0.1.29-2build1 -libtheora0==1.1.1+dfsg.1-16.1build3 -libtiff6==4.5.1+git230720-4ubuntu2.4 -libtinfo6==6.4+20240113-1ubuntu2 -libtirpc3t64==1.3.4+ds-1.1build1 -libtirpc-common==1.3.4+ds-1.1build1 -libtsan2==14.2.0-4ubuntu2~24.04.1 -libtwolame0==0.4.0-2build3 -libubsan1==14.2.0-4ubuntu2~24.04.1 -libuchardet0==0.0.8-1build1 -libudev1==255.4-1ubuntu8.12 -libudfread0==1.1.2-1build1 -libunibreak5==5.1-2build1 -libunistring5==1.1-2build1.1 -libuno-cppu3t64==4:24.2.7-0ubuntu0.24.04.4 -libuno-cppuhelpergcc3-3t64==4:24.2.7-0ubuntu0.24.04.4 -libunoloader-java==4:24.2.7-0ubuntu0.24.04.4 -libuno-purpenvhelpergcc3-3t64==4:24.2.7-0ubuntu0.24.04.4 -libuno-sal3t64==4:24.2.7-0ubuntu0.24.04.4 -libuno-salhelpergcc3-3t64==4:24.2.7-0ubuntu0.24.04.4 -libunwind8==1.6.2-3build1.1 -libusb-1.0-0==2:1.0.27-1 -libutempter0==1.2.1-3build1 -libuuid1==2.39.3-9ubuntu6.5 -libva2==2.20.0-2ubuntu0.1 -libva-drm2==2.20.0-2ubuntu0.1 -libva-x11-2==2.20.0-2ubuntu0.1 -libvdpau1==1.5-2build1 -libvidstab1.1==1.1.0-2build1 -libvisio-0.1-1==0.1.7-1build9 -libvorbis0a==1.3.7-1build3 -libvorbisenc2==1.3.7-1build3 -libvorbisfile3==1.3.7-1build3 -libvpl2==2023.3.0-1build1 -libvpx9==1.14.0-1ubuntu2.3 -libvulkan1==1.3.275.0-1build1 -libwacom9==2.10.0-2 -libwacom-common==2.10.0-2 -libwayland-client0==1.22.0-2.1build1 -libwayland-cursor0==1.22.0-2.1build1 -libwayland-egl1==1.22.0-2.1build1 -libwebp7==1.3.2-0.4build3 -libwebpdemux2==1.3.2-0.4build3 -libwebpmux3==1.3.2-0.4build3 -libwoff1==1.0.2-2build1 -libwpd-0.10-10==0.10.3-2build2 -libwpg-0.3-3==0.3.4-3build1 -libwps-0.4-4==0.4.14-2build1 -libx11-6==2:1.8.7-1build1 -libx11-data==2:1.8.7-1build1 -libx11-dev==2:1.8.7-1build1 -libx11-xcb1==2:1.8.7-1build1 -libx264-164==2:0.164.3108+git31e19f9-1 -libx265-199==3.5-2build1 -libxau6==1:1.0.9-1build6 -libxau-dev==1:1.0.9-1build6 -libxaw7==2:1.0.14-1build2 -libxcb1==1.15-1ubuntu2 -libxcb1-dev==1.15-1ubuntu2 -libxcb-dri3-0==1.15-1ubuntu2 -libxcb-glx0==1.15-1ubuntu2 -libxcb-icccm4==0.4.1-1.1build3 -libxcb-image0==0.4.0-2build1 -libxcb-keysyms1==0.4.0-1build4 -libxcb-present0==1.15-1ubuntu2 -libxcb-randr0==1.15-1ubuntu2 -libxcb-render0==1.15-1ubuntu2 -libxcb-render0-dev==1.15-1ubuntu2 -libxcb-render-util0==0.3.9-1build4 -libxcb-shape0==1.15-1ubuntu2 -libxcb-shm0==1.15-1ubuntu2 -libxcb-shm0-dev==1.15-1ubuntu2 -libxcb-sync1==1.15-1ubuntu2 -libxcb-util1==0.4.0-1build3 -libxcb-xfixes0==1.15-1ubuntu2 -libxcb-xinerama0==1.15-1ubuntu2 -libxcb-xinput0==1.15-1ubuntu2 -libxcb-xkb1==1.15-1ubuntu2 -libxcomposite1==1:0.4.5-1build3 -libxcursor1==1:1.2.1-1build1 -libxdamage1==1:1.1.6-1build1 -libxdmcp6==1:1.1.3-0ubuntu6 -libxdmcp-dev==1:1.1.3-0ubuntu6 -libxext6==2:1.3.4-1build2 -libxext-dev==2:1.3.4-1build2 -libxfixes3==1:6.0.0-2build1 -libxfont2==1:2.0.6-1build1 -libxi6==2:1.8.1-1build1 -libxinerama1==2:1.1.4-3build1 -libxkbcommon0==1.6.0-1build1 -libxkbcommon-x11-0==1.6.0-1build1 -libxkbfile1==1:1.1.0-1build4 -libxml2==2.9.14+dfsg-1.3ubuntu3.7 -libxmlb2==0.3.18-1 -libxmlsec1t64==1.2.39-5build2 -libxmlsec1t64-nss==1.2.39-5build2 -libxmu6==2:1.1.3-3build2 -libxmuu1==2:1.1.3-3build2 -libxpm4==1:3.5.17-1build2 -libxrandr2==2:1.5.2-2build1 -libxrender1==1:0.9.10-1.1build1 -libxrender-dev==1:0.9.10-1.1build1 -libxshmfence1==1.3-1build5 -libxslt1.1==1.1.39-0exp1ubuntu0.24.04.3 -libxss1==1:1.2.3-1build3 -libxt6t64==1:1.2.1-1.2build1 -libxtables12==1.8.10-3ubuntu2 -libxtst6==2:1.2.3-1.1build1 -libxv1==2:1.0.11-1.1build1 -libxvidcore4==2:1.3.7-1build1 -libxxf86vm1==1:1.1.4-1build4 -libxxhash0==0.8.2-2build1 -libyajl2==2.1.0-5build1 -libyaml-0-2==0.2.5-1build1 -libzimg2==3.0.5+ds1-1build1 -libzix-0-0==0.4.2-2build1 -libzmq5==4.3.5-1build2 -libzstd1==1.5.5+dfsg2-2build1.1 -libzvbi0t64==0.2.42-2 -libzvbi-common==0.2.42-2 -libzzip-0-13t64==0.13.72+dfsg.1-1.2build1 -linux-libc-dev==6.8.0-106.106 -locales==2.39-0ubuntu8.7 -login==1:4.13+dfsg1-4ubuntu3.2 -logrotate==3.21.0-2build1 -logsave==1.47.0-2.4~exp1ubuntu4.1 -lp-solve==5.5.2.5-2ubuntu0.1 -lsb-release==12.0-2 -lshw==02.19.git.2021.06.19.996aaad9c7-2build3 -lsof==4.95.0-1build3 -lto-disabled-list==47 -make==4.3-4.1build2 -man-db==2.12.0-4build2 -manpages==6.7-2 -mawk==1.3.4.20240123-1build1 -media-types==10.1.0 -mesa-libgallium==25.2.8-0ubuntu0.24.04.1 -mesa-vulkan-drivers==25.2.8-0ubuntu0.24.04.1 -motd-news-config==13ubuntu10.4 -mount==2.39.3-9ubuntu6.5 -nano==7.2-2ubuntu0.1 -ncurses-base==6.4+20240113-1ubuntu2 -ncurses-bin==6.4+20240113-1ubuntu2 -netbase==6.4 -netcat-openbsd==1.226-1ubuntu2 -netplan.io==1.1.2-8ubuntu1~24.04.1 -netplan-generator==1.1.2-8ubuntu1~24.04.1 -networkd-dispatcher==2.2.4-1 -nodejs==22.22.1-1nodesource1 -ocl-icd-libopencl1==2.3.2-1build1 -openjdk-21-jre-headless==21.0.10+7-1~24.04 -openssh-client==1:9.6p1-3ubuntu13.14 -openssl==3.0.13-0ubuntu3.7 -packagekit==1.2.8-2ubuntu1.4 -packagekit-tools==1.2.8-2ubuntu1.4 -pandoc==3.1.3+ds-2 -pandoc-data==3.1.3-1 -passwd==1:4.13+dfsg1-4ubuntu3.2 -pastebinit==1.6.2-1 -patch==2.7.6-7build3 -pci.ids==0.0~2024.03.31-1ubuntu0.1 -pdftk-java==3.3.3-2 -perl==5.38.2-3.2ubuntu0.2 -perl-base==5.38.2-3.2ubuntu0.2 -perl-modules-5.38==5.38.2-3.2ubuntu0.2 -pinentry-curses==1.2.1-3ubuntu5 -pipx==1.4.3-1 -pkgconf==1.8.1-2build1 -pkgconf-bin==1.8.1-2build1 -pkg-config==1.8.1-2build1 -polkitd==124-2ubuntu1.24.04.2 -poppler-data==0.4.12-1 -poppler-utils==24.02.0-1ubuntu9.8 -preview-latex-style==13.2-1 -procps==2:4.0.4-4ubuntu3.2 -psmisc==23.7-1build1 -publicsuffix==20231001.0357-0.1 -python3==3.12.3-0ubuntu2.1 -python3.12==3.12.3-1ubuntu0.12 -python3.12-dev==3.12.3-1ubuntu0.12 -python3.12-minimal==3.12.3-1ubuntu0.12 -python3.12-venv==3.12.3-1ubuntu0.12 -python3-apport==2.28.1-0ubuntu3.8 -python3-apt==2.7.7ubuntu5.2 -python3-argcomplete==3.1.4-1ubuntu0.1 -python3-attr==23.2.0-2 -python3-automat==22.10.0-2 -python3-babel==2.10.3-3build1 -python3-bcrypt==3.2.2-1build1 -python3-blinker==1.7.0-1 -python3-certifi==2023.11.17-1 -python3-cffi-backend==1.16.0-2build1 -python3-chardet==5.2.0+dfsg-1 -python3-click==8.1.6-2 -python3-colorama==0.4.6-4 -python3-commandnotfound==23.04.0 -python3-configobj==5.0.8-3 -python3-constantly==23.10.4-1 -python3-cryptography==41.0.7-4ubuntu0.1 -python3-dbus==1.3.2-5build3 -python3-debconf==1.5.86ubuntu1 -python3-dev==3.12.3-0ubuntu2.1 -python3-distro==1.9.0-1 -python3-distro-info==1.7build1 -python3-distupgrade==1:24.04.28 -python3-gdbm==3.12.3-0ubuntu1 -python3-gi==3.48.2-1 -python3-hamcrest==2.1.0-1 -python3-httplib2==0.20.4-3 -python3-hyperlink==21.0.0-5 -python3-idna==3.6-2ubuntu0.1 -python3-incremental==22.10.0-1 -python3-jinja2==3.1.2-1ubuntu1.3 -python3-jsonpatch==1.32-3 -python3-json-pointer==2.0-0ubuntu1 -python3-jsonschema==4.10.3-2ubuntu1 -python3-jwt==2.7.0-1 -python3-launchpadlib==1.11.0-6 -python3-lazr.restfulclient==0.14.6-1 -python3-lazr.uri==1.0.6-3 -python3-markdown-it==3.0.0-2 -python3-markupsafe==2.1.5-1build2 -python3-mdurl==0.1.2-1 -python3-minimal==3.12.3-0ubuntu2.1 -python3-netifaces==0.11.0-2build3 -python3-netplan==1.1.2-8ubuntu1~24.04.1 -python3-newt==0.52.24-2ubuntu2 -python3-oauthlib==3.2.2-1 -python3-openssl==23.2.0-1 -python3-packaging==24.0-1 -python3-pip==24.0+dfsg-1ubuntu1.3 -python3-pip-whl==24.0+dfsg-1ubuntu1.3 -python3-pkg-resources==68.1.2-2ubuntu1.2 -python3-platformdirs==4.2.0-1 -python3-problem-report==2.28.1-0ubuntu3.8 -python3-pyasn1==0.4.8-4ubuntu0.1 -python3-pyasn1-modules==0.2.8-1 -python3-pycurl==7.45.3-1build2 -python3-pygments==2.17.2+dfsg-1 -python3-pyparsing==3.1.1-1 -python3-pyrsistent==0.20.0-1build2 -python3-requests==2.31.0+dfsg-1ubuntu1.1 -python3-rich==13.7.1-1 -python3-serial==3.5-2 -python3-service-identity==24.1.0-1 -python3-setuptools==68.1.2-2ubuntu1.2 -python3-setuptools-whl==68.1.2-2ubuntu1.2 -python3-six==1.16.0-4 -python3-software-properties==0.99.49.4 -python3-systemd==235-1build4 -python3-twisted==24.3.0-1ubuntu0.1 -python3-typing-extensions==4.10.0-1 -python3-tz==2024.1-2 -python3-update-manager==1:24.04.12 -python3-urllib3==2.0.7-1ubuntu0.6 -python3-userpath==1.9.1-1 -python3-venv==3.12.3-0ubuntu2.1 -python3-wadllib==1.3.6-5 -python3-wheel==0.42.0-2 -python3-yaml==6.0.1-2build2 -python3-zope.interface==6.1-1build1 -python-apt-common==2.7.7ubuntu5.2 -python-babel-localedata==2.10.3-3build1 -qpdf==11.9.0-1.1ubuntu0.1 -readline-common==8.2-4build1 -ripgrep==14.1.0-1 -rpcsvc-proto==1.4.2-0ubuntu7 -rsync==3.2.7-1ubuntu1.2 -rsyslog==8.2312.0-3ubuntu9.1 -run-one==1.17-0ubuntu2 -sed==4.9-2build1 -sensible-utils==0.0.22 -session-migration==0.3.9build1 -sgml-base==1.31 -shared-mime-info==2.4-4 -show-motd==3.12 -snapd==2.73+ubuntu24.04 -software-properties-common==0.99.49.4 -sqlite3==3.45.1-1ubuntu2.5 -squashfs-tools==1:4.6.1-1build1 -sudo==1.9.15p5-3ubuntu5.24.04.1 -systemd==255.4-1ubuntu8.12 -systemd-dev==255.4-1ubuntu8.12 -systemd-hwe-hwdb==255.1.6 -systemd-resolved==255.4-1ubuntu8.12 -systemd-sysv==255.4-1ubuntu8.12 -systemd-timesyncd==255.4-1ubuntu8.12 -sysvinit-utils==3.08-6ubuntu3 -t1utils==1.41-4build3 -tar==1.35+dfsg-3build1 -teckit==2.5.12+ds1-1 -tesseract-ocr==5.3.4-1build5 -tesseract-ocr-eng==1:4.1.0-2 -tesseract-ocr-osd==1:4.1.0-2 -tex-common==6.18 -texlive-base==2023.20240207-1 -texlive-binaries==2023.20230311.66589-9build3 -texlive-fonts-recommended==2023.20240207-1 -texlive-lang-greek==2023.20240207-1 -texlive-latex-base==2023.20240207-1 -texlive-latex-extra==2023.20240207-1 -texlive-latex-recommended==2023.20240207-1 -texlive-pictures==2023.20240207-1 -texlive-science==2023.20240207-1 -texlive-xetex==2023.20240207-1 -time==1.9-0.2build1 -tipa==2:1.3-21 -tmux==3.4-1ubuntu0.1 -tree==2.1.1-2ubuntu3.24.04.2 -tzdata==2025b-0ubuntu0.24.04.1 -ubuntu-keyring==2023.11.28.1 -ubuntu-minimal==1.539.2 -ubuntu-mono==24.04-0ubuntu1 -ubuntu-pro-client==37.1ubuntu0~24.04 -ubuntu-pro-client-l10n==37.1ubuntu0~24.04 -ubuntu-release-upgrader-core==1:24.04.28 -ubuntu-wsl==1.539.2 -ucf==3.0043+nmu1 -udev==255.4-1ubuntu8.12 -unattended-upgrades==2.9.1+nmu4ubuntu1 -uno-libs-private==4:24.2.7-0ubuntu0.24.04.4 -unzip==6.0-28ubuntu4.1 -update-manager-core==1:24.04.12 -update-motd==3.12 -ure==4:24.2.7-0ubuntu0.24.04.4 -ure-java==4:24.2.7-0ubuntu0.24.04.4 -usb.ids==2024.03.18-1 -util-linux==2.39.3-9ubuntu6.5 -uuid-dev==2.39.3-9ubuntu6.5 -uuid-runtime==2.39.3-9ubuntu6.5 -vim==2:9.1.0016-1ubuntu7.9 -vim-common==2:9.1.0016-1ubuntu7.9 -vim-runtime==2:9.1.0016-1ubuntu7.9 -vim-tiny==2:9.1.0016-1ubuntu7.9 -wget==1.21.4-1ubuntu4.1 -whiptail==0.52.24-2ubuntu2 -wkhtmltopdf==0.12.6-2build2 -wsl-pro-service==0.1.18~24.04.3 -wsl-setup==0.5.10~24.04.1 -x11-common==1:7.7+23ubuntu3 -x11proto-core-dev==2023.2-1 -x11proto-dev==2023.2-1 -x11-xkb-utils==7.7+8build2 -xauth==1:1.1.2-1build1 -xdg-user-dirs==0.18-1build1 -xdg-utils==1.1.3-4.1ubuntu3 -xfonts-cyrillic==1:1.0.5+nmu1 -xfonts-encodings==1:1.0.5-0ubuntu2 -xfonts-scalable==1:1.0.3-1.3 -xfonts-utils==1:7.7+6build3 -xkb-data==2.41-2ubuntu1.1 -xml-core==0.19 -xorg-sgml-doctools==1:1.11-1.1 -xserver-common==2:21.1.12-1ubuntu1.5 -xtrans-dev==1.4.0-1 -xvfb==2:21.1.12-1ubuntu1.5 -xxd==2:9.1.0016-1ubuntu7.9 -xz-utils==5.6.1+really5.4.5-1ubuntu0.2 -zip==3.0-13ubuntu0.2 -zlib1g==1:1.3.dfsg-3.1ubuntu2.1 -zlib1g-dev==1:1.3.dfsg-3.1ubuntu2.1 diff --git a/reports/vm-prebuilt-inventory-20260325/new_dpkg_status.txt b/reports/vm-prebuilt-inventory-20260325/new_dpkg_status.txt deleted file mode 100644 index b7fabee7468d6bde3d14a0d6af39f24a00544d70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 769308 zcmeF)S+gBSk{@_HkJ)^OYkVQ37C>SlNLF)XMR6;17m65Ou&Sh&7O|2=0E7T=E0dL( z-sdaNX#AJ|>xj%e=iH586ChzQ+_PmyczAfY|9y$b|NZ}6Kl8Q}j|2VUM=4pNVbo$N}U!8eazxwWe{o6b9P2Io#_D;3o-s{!I zS9OKIPtW`~_3+i12WK8l-)ZAveV<)uu)&9An?5i_>KcM@{ znK#e8GqrTBT1BJQy&4C)KR(htz{de{Meo^vhy5>oJ`(FJvt~yt}^i|$} zRpY)j@!`pt-`AZ!Kdt}g7k8bnYrd&5{^yCB_pA3?XTGd!pH^E&_-%dDyv9MJyH?-c zuiw6^QS}{YzB}>hPJMr`D0k(b4`{{J@aa1v}ix`##mlZgwDiYrg* z-}m*+f2tmg@a9DE!|HAC%-N#*|EkwFtJSy8{D*q?KR2#AcjjXK`sUQbe)W29lAiDa z)i`sr{(e*Kl9D?$D)RMo;=vE~_uK)`?@u&@|9Ou`k4sV>)m3+k&U4j1zTB_(gSsnc z-mYIR6n%K|!NiNZb-nBE)Rn)RzTZ3Z#?bCeOkU2R>edvBh3w?=%Y z{=QRGU#xH6J#)GKy7O{<&dz@2Yb`pKPY<1 z-rh5MjIFP@SJ!+~zdWiIwY_)d#w0(kF?(aZX4T!A#)V2(JgsZB{CZtEU&*i6ciqG5 z)0hS~{1%ygaBz*5B71%zuQY(wbnk$sZp&{OPkx<`>HNX9{km6kwwh+=b?vsR=luWYXV-@Er4-@Y7=I`~KE;RKd3Tb5 z2i5);_5WUtguFxpgl^xqoHT#?_5Eq)w~hY$YKeqwp9NgotJ(av=G--A{Yl+HgGRE< zI+{pUw;zc^YozDf`kh|;v2;Y_H}o`T_G)IWQ?^4t-_$>=QoE1ppHcbyReh#2Y3{4F z!p2;Ga`5@Psi$x2nkSPUnM+YPr}YQ*d+0zjuG7M7(vJss>F>vC|4CUgT7srBh9Q4h zAM^!HOoL}V(8085uZAz`H*MoYXg4x?FWd?p-kS#_)XRJvW8L*iZoa)<^hEhL>(GQ*8y3I^|Q!+#njjO-c+K=@g-ydv@_Wimpwmb9V z>Y=aaqsKiugq)a7SOZ;mr}t>Y?W~Y*>f6!(T8D5xvzXvrlR|;F;^d_!@u`bvDccId!ctFbMy;)^rpMEF zU)JBJ^*fz$ws7rx^?IRNd+W?U>e~DD%a`?fxjy}e0}Yn8mS}6-cd4$lDjpRwa!(|O zuKunhmV}*@CMa`^F)fnuC0<=hH(h(LuDDmd2ATG!i>uEUrcninHlH4qR=Zamhg}Dj zoYSEFr3b(w{5Y*(*vIc_^H#O9Y~xaKGrY_d_iCnY)$BYezK<)R^&s};wGSs<1La;? zQ0LmWCyM9SvnpC=Y0SuIzejU%K?|*TyRQGI`ZuSW->__VXwcgS2%qG&W}ABw?8UkI z1v+5MXlmcln9=y#e1o2g+eQ1WndGsD-%fIrd8Nzg%b@97H9~&K_w~)+s-Min?fTV> zwx2e*K+-Qvd}}`NpV+f|2Q7`yLvs3x$PUZ&{WQ78YwjuvdhyyGxcUk@tvabV!eB7`cK2mJ-DOkY4l}Y zhL6Q(yHaglu2%0>Pdqywo_=V+oAvo_QHmzt-z~bnsDJ3aTz9VNfEt3QZr4?C%#X#7 zP-i`4Ofrv^ zEJmw8oq36 zf#5h=j-R&amBM${E}uU!q{DpJApT)RNDdyRmD`iPO+SzutjMOdcWO0$QN1L_LPuK( zeC^EJpd|?0ukN?9gEaH`f0Xp1_Qz>dT5)}P(FP3{v}rE+=jL~fwbEx=J`Q!1f{l@StUhUlP#bK0-*bU19i}PvtVBzBEwPnI6 zS-fy{%w4QI8-Q0ZOW!-EVQ3Hc?;fj}3$IYgFBsl+DCcb^!q%9^Xl_lMY@cY!dux;= zqpU}>DVbkP^FZ^EMY44~jxYJrG1`tNY+o14dq#ijxo-~6CFxf2n64BXh@8hunMX}V zi~L94qjfV@(0r~3Pmb4nY#g*OTI5+76O{6J(uJg`>C3!D18w*AN#ER~-#(8)jmPbE z=%`5KHpy@8la|x#@jc7;%geh_qomu14~gSAK|9ks=IOm@T|i?$u43HQ%gMdSqr4Mx z^Sr%G@toGz)^Per+-F!bmL%5YX3eQ`?{P}u!wdh>O zuAWqXa0FaB*Rb*@Ii#=$Nm>!W(5dxHneKH2>7+3(L=i@*VFZ(_#5 zll%4EPsvhVDs-N8a;;`5bDvk%$ar4#IMwmB$fmB~eawAC(XC6<+(CZjgx4d0n9XGzi4@{nN9^bi8vuIrYUntsS zDc@X-`qF_9jxw3iZ5s3YiYlOP9b=f=V38%#2nokt`)01`!Yz?MUiF2NJ={Xs#qtWn zH8SY#x!&b>bnHMp>7Du~e|&cuO(IyiJL~cGvz5BM??T}Peerfa7pl+B74ccm!c9rMcL{Ts#Q`X*jKewjIA z7sa?Z@tUuGyGFGc6=(^4W3n~iQ{RyS^!t>k&70E*P%R=P zZe?uA0nGU?3Uag75*?x+7z3Z)tiOq%-K;xt&2MHNAIu3JnF}+OHK3+}EL^DT)^m{P zOk3i4>~C4$;ffzh8lDv1AqgUSW* zWmWh)@+6TboVK#xnZ|F0->Od+>#unrpH>$dMBwCtNFctE@sm)2^1+#NlT3eKv$0pb zSP#)M_-_TNVfmuQtJZ}^BQfS6T*zFgc)44@p@NN?TZrLB`DJLgwyeus?6A#=xrxu+ zDg7ZD0I|w2@A?YN`WGVPX3a(skky;AdaZN2`}hGcANW37+YhPGRN&l9ndmftSd zyx+nBv9NIbaZ%n>^lv>EL;e%HTyw0WyRpO2mI#c99T|&9)b#~=zK@eT?-iiJ&77vv4u!u4|K;MOp^Rpi z{{opMGq88&FU0}KGIRJ|&GX+XMmoMbjJsV037f{}n%7OvGv`0WBY|b1QAi64HS6@X z5q?(v?(#|eP9~R;j^la`k*R1i*(UeJubt&AoZrF*N&^ZqI20A%zUy6PUHA# zx}BVxC6;$cy=Y4+XV5yYSDWi-Ba6;QMdF_@&Cu;);twYoN*3UL{U%~M`oxt-6A$T+ zWyYlIVxT*c7o?V+)|Th&Ok<95%*QY^>ZrbpCzRQ2+r+C9b#9%A$F0rJ6%Ar?sgA`r zJq$XdPs5kdi&)QxHD*zt8xz`b&m0vkDehbueTpm5>78ZMVey@I=6nCTzhh0 zdy=1LyOIq}CAN0gS&;C0Ib(zK(cVUTz4$U}Tg5`6OTV0Yd@;2rv>QwlzFe(&CzqLF zGrNu;zcZmcnu5m6F}z^(U5tC%bj-mvvXPADzr!Yfrvo1c@{7U}0#z zbY!UINS%mPm$*XLAHlw(mfT8RYV~2!htv)&D?SqXyV4~37`jMJ51V2IwqDL1t<`r& zfsSiUa3Y!Nd1cR)Bdo^Y$u6$Oifreqx!SKeTvz^&R>Nu6eNa3{pYe#DCMz(m+|_?- z&vy6U?T%ULeD=`N-OKrBxFg~_gJb%T1*b31YoJgLkTh^eDo@_fABE|1Gw_Yv(t9QGN2$uhjRO zKJm6bGdhtRo}Hew?EkIlJ>O6L3q-}ggD%8h-knB;DpfmXZSFnHds_MiEcxYXO)M+k zxBU|Qe7++ulb_846-|S(;_cok8uXvI%x(no$(cV@M0~H}pvm~B4&?nQszU1bN(%o_ z{dbPbm5_lLMC_hb$j_SV4>%w@RN#!rANv%X!V_S<)v?R($0G^8F?zZuSci4j*1BG} zZRE0LI>6odgl{7=&W50o-9Q5$)ip-==s(;uuemjS{tvY_LQ%i2=c@NW znF}-~gN1&da3)wO$VG%wA0u0DbeTtU63?8i=sdSp+iJ{FVyL=W9-4o9g+x9rxw?Ah zul0VvMiTiN=sF}dw^q}<{;~TwTWxfkf0;P<^)#oD;U`5anTEVXkXS8I@l7r@ z+G8nF{UuXxoL&1t6RHDU+C|^u82|m|lncB%&B)$_Jk(~;%VZ_~f7T16ZRd^Z6T9VF z4#T6yv#sNH*<@IBV7biqAF4$&;Wz6F!t1>0?+SUpG0hHt**|e*D_Mn`S^u_vSF`-6 zUQ;3SwB9ux;~m*}RQG84L5&UFpBCpjB5%CAEAX#XpX#PXi}b#C=3ndj-k+j2!fwZJ zsxo43<&PH^Ajiwx)PD7p6&IW6ih&+^GAG+vdAzy@l#5`6H&+*ZZbPh{*~go##g{8% z`R5v|c-4qmLSQlo=gKb4acGd)#Ytwe+S*@g%(9$AyBbqPVvXUb3>iOfJ<}i7uWMAf zysB4{h#dWTcFJ-dMQMD`AjNgRCu?JU_qmE;^HpH=%k@_uM)gCzPwn*a+qVX{qs283 z20m}}dH75vGD6dBLzwH0yVK~7s>W$lMnlrj4-PER z=o|Ovh)8Q@dV+RR(Qjrg6*Ps!s@Q zV3QF+X>`N7n>9QQT}_FKT-J@X#;524C@&SezH7YH@y(6~4gMZ&p=m%ix~+3`KwG{y zs&9zuY5ks=V3C(AA7B6*gwAG7mnPvQ5z7r3Q6Svwd*uDxJ0DAFmY{VS5}t_`Z=rdwo`R*N8VR|9Lg@cFlWh zvcA>}-bLTUqwN?so%HwmeQ3D(7=~7im$J#dr8hCg9Zk5_fGDxUH8maH^B2~6B(1a# z)xOG5-_89(dV4JoK`81(ld&12;)P$WR@kbJ0Pv=KlQFiZ9fXIX^k^^hZ(Re=5z;oa z`LbwbzPy;%rAd3etUlAOoNDqK*%#Rru`iZ>=C2I=`eOTB!D>hEB!&%nmbA&udMAd; zrp$dLqi$^LJRf%)b=CGA%Bc@cYkxn$lNU;#jvm#xCg-6lGu%F@f7jWU!~F2Hd~)l$ zl!5IwwJl{dJpI4jFTvBcvPbDCNPF0xSSKFMcDorLXG9pp1QYFE`z$}*uj&yrPB_^} zI$qna|Fz`M8Cr=TzFwo=E7?q?-C;f&KZg&)YgDMg$E#@fJftpE`0_r1RJ1M?G0Y6wJnpnWyh~!|}58 z_GAB6c4@5ldNrr!7>wpmB+^L~B4Eq7X;ExoArt#iJ-qRlezJJzT!?AD{Fb2x4}BiW2AB*IpLE1 zk_8(S{eE?=NTxN?^3kI??9`}HKY6b1Ok{ZM)gxgqL%vG|JX&8?&uyCyrh(s`q_EFF zjdw}|EIlL=!F!PRG5_C{#5i}Szwd9Y>Ua;sE4Vy)1*%rN66)S`-q2xJSld>O_If8% z_c88^ufg`@ydv|1_PPEU*}bc^${LT2=nVamanK*FIS-fF^$LoG5aqFNpz$jdOwT6L zvqrO{ho#2dY#R^eZ9c4%^|mAHXzS(bra4tB!~?Oyqb)}+q4~S!YcfBwM2BJb;9>fB zy(f0~M`NtryIE=-vN_`RC;IgywvpvXR_M5Mz2p~N`2FOY_5KTE3uj}AWsI!>-?JB+ zI_;;7+-&V0FKHbWJg6A!l`zpR$2=VUa;H z7MbGrle);(n%}PWi`n0%>6UzTwK;Ej>etqXEZcC9)g3E4Y#k7+hQO zO5fpl=p6I8ZGHUntT#F(7HNCGp%FEMOCQ!fLziB!QM1aord12Az?RxPngg4j26;tO zR?hKtt9hSm(a?>GR2RvBXP>ZHw&#z1v^IKmM!NT^iZfB=NVs+**)0icDH*;)OA(#c zJRl=zQE6se5Ju)``_-WvUmj1_i=T#%FGlUy-)l6^c{GODcy*f3r)n&^bkv=ukJL5{ zRMj3v*5uXV^il_}_c^@mRb#s{AVC|4pU4ld=FCNB9Sp6odtX`b_07tXUM$J)N+fN_ zIIGc?bKaHLnBT2Bsn0hRW8Y27GG8#?JCKOY;gS88NY=of3Q|KuZO7!a`MuKV=O*+> zKd_vsj7gUG>-r7i$yuVouAL?B8f`v&P?+m-ntaQp_U4sDc$qarlSES|MnfC4o|8M~ z3wJFoy+g;R-k6ON)rtO~Q~WZo$(hsBdbeNa6woYcc>4Nah#nv6tmd}Tt)u=+eM=wF zjH2OxnXnk@X=@RT-Mljb5w2*xb^mSO%!C@ooFMXL(TD@|D8&1R>ff&LXx7XEy=ry_ zDw&U~+YQI%;OY77mB#Zy4gJyhI^2&mz%4;8v3#*bI1=0z*@QA#1&op29?JD?B>x$!5 zZogmDp(uR1R&A3opECCS>HQZ|9g4N{UVR>S{J!LUpsQuH>p0mQW3g6^tSjKU6_s$U zixStvkL%U{sE`+JHODeu#7n#@RyxvY^@(kb2o8jBoTv1xbLfZ5+PSK2%^9Nui=+x#bkx6#71?`OTI<`1 zLXz=OeWs)72=Z#Km!oqP4s1sVAJo+{HAc8sdj{XHzlW^_*SNOji`=vh-hLl_2rYbB z_r=}?FJ%7`tYB_fl%db)5o?z}^F_tu^m4QAjzuQZdL1YD`rbH2;`YnJENx3HN4&OZ z&}gY{gRZ6>?fQrX8DnbN+p38r1VKg9roXm+*4wBk-n0HGc~+T z@?u`Jt6y{)drPD<^5mmLL48kNA5Qp=cgUvq9eEcXopheG)q8Qa-)nw41`>Pa2`8vA zufE9`W6_r0d}taGs9bH_kO{5c2*ubUP0$d|X?@JtK`FDre zX0&ayr0BX+?XjaGsjmB|UPaDEPKu8DrO%IC(s;F~3okley7a4^m__><&&yN0vJo>I#m9X9u!Z%i7ChFEII*7w_N?z}pp{S4TC>b^oA`fy}MZMB$_3 zw{gsjjT86bqKpmg$vn09X?@VqG7Gd+_CVk{3cY?=|AUXb7umFX*n7B%4e}lap;^6$ zRFap3Dx5aP4#d*r-J=6~?9mpRB@3<~8y)e044jPMFZ9Xk#malt8rzZjg8Vk9x8n@o zLz#C@*Yax4S|akP_crl}&80TkRbCvPcpobjgyfklvAgy#&rcXK6A(xC+6Og|aaZ-< zxu)GKtMg6$f^T^6aKd2s>ov2bS3911PO^WFR(tCiS0B_p%^B~l0+>Jy{Oh_mGvqxf zr3L(g7yMAx-aFRUwO5mzi~>4>(%S0lzFukQ9Pxw?^&@(%C( zhFkvT1k$|IJ`SgyL1WWm#4RP>&*i+en)x|j`oYX;XOf#yS zqNV8SL?q}Pzjus!MEO%+>3$kuKC0l4uIi7LjVH>hw3<48-ZYJ!%JE|JyLN*VlKF0A zla@o3b=?TAUvCsm84Z0oqOjYZ)j%h^D*kS))Ts3B8o1&6XS6(RnhyqGXA+~qi;iDJ zqm$WK*ilu*=A@O~voD6KT}UBzEO)Fq+avDRG4de+R%fpnRHvd~&6&txcB{~a^V1EN z>w7uZyT%ZTj&EJ8>=BRpq+%x@)cCjQRQe@PvFYobSm9*p-4zUf9V zd)X|J6R$7Up7E3H8mH^xEt=(DK2|@EB(q&+8)E@Y>XHtTRHDjCdH~)U^ z(QD3zq3x3oV4b=i1M+z1S>|IIDPjdZY?B9zHHe6hd6@&nJcu;@z{D&3w_@L^iUG;Ij z|6^s>^8QBUHQ~FYkj?{$v&#_`t1*OXJ6OvbLG`qL!LS3s(zne{#fg#f9d!6 z#c7?_OUk;!w?F6C&#I?u2Q7NPfAD_4=ubcUHG-yQzph=sPdhh?`ajnG{EzDQk;%Am zaBurp{n|3S<{?ZnGW8?`Uze3Wa}M=cq~Ub)%D0~v9fQweiDIN;gQBlyYS~VE)S17| zGVtT)ce*E@p3Dhk;u|Z#S(M|Oj#d6sT`l(P)J{?|;sX59WLgt1?aIKj2PgDGO4|Og zS{1Q`6MF{#RAc*N&4l0oRKKZE#3vqo_KIbvB`;fKgg573)_iB561$zR#?GGS^q%=x zgL1@UdF@%@R&B5Oezp^P4NHg5?boBucwxnGrce5Tgp5?~C9-jt4d-7tW38pfDW+dk zEI!Xw%I=Wkd(7__o%pH>j^@HC)*Pjg5i#WVio5ydG^d)Z7Tzo^{bdwr|9! z_lrwr5QqFiO3ixWhL;2OaJ+HebeZ2*O>>8@RiIf*SRJ}uHxr>l;;UTfnt=MRwj-MC?#B0bJAp;@d(F}op|(aB+TFS=!>b5T@u@A8dcPjTCse&Dhfdr*Kn$<7^ zC^;-^cbHX+O=H9LhmYs02e?7Tt$kGAIpg~ulXO3>xiRa`_-+Y5tach7?OTrqFf03Y z@AI@aIZYn*+O>ky%i7+IIyt{ztt&}XY}{sU&@&fbzG^PI$LM~FrQtEkhj|9!G778Z z&uTv5S=kvY3SL;wvF9w(LD2*>`1&co>+6#@)cS6HPnYn!mT}#jvxZu>kac3|I%kO1 z_7Ulzel8aoe_B^=vvI@DZnI$LYGj@n`S3u-Hgl5eu_ZSN(XM|Pz56xQQ|OAu%IOZ! zAPH6D;;LxcXzB+w0y=YF~T!%`}A3aysdFq9~;z!Py@7)e6S;Q}M zSDzG^9>!IhvfLch2hA!Elgxi8THlei=_$~or`+qW8FhDO?0Xj4zcdT9$^1z3&*mv_(2!v+%|wkL%g+EFMqafv#1Pd3LgA={Q5wQ))f0$J3bKE;_t=F0s|E%U;6f9{x3b`HqB-CGX)E^eg zQ}gE7El74Zx*| z%!w>)qbBw<)v>L$+OrK6;XDnGZN%hDwK9?qhE29Uf!I8ZM-qAQRg2|U+aw#;AfSgbDw_6 zi}~q+v35AkuSt!U)?h^%ec4XxtVuS$C)y(F+q%EAHrB&~x~dUOV*dl74%*sQ!JFae z@vYDY#m%ff(JQU1{S{HbpZs!IEW!LO?QXn3Yk)Q&F~6ogU69_(9dZ71$(3s8fd*+t z-hk5;-mEzjX`_$7t+ATl&+3j%|2{LE`6unWcW~{>8oPH}4X?Gnlpa<=tt$;;pTp>T zxAy*ck~}&g_8{wx)(x_4*&G@iJ)<_ad+F<}ileo4G* zYrXaVsGx2A*02Bl+1;;CZPBD6expaT3#}yCp+l_9J|76$ zehNIx=}I(eqJgx5JX_xxe)*QAORL7F|K1T4+X$WSo%!$etx>KiZ=SIs_R@(v7*WTU zqle7o+zz@T2wi`nrKxBN*L>NeqxVX)OC-bmgr~7p_v@Rc4`=9XJPKOTzF%3!P|$O0 z7Pj#|R%(;ZNJh?BaUUz#XRb8W-S(vo4)!n0xerA&y7@D6XvHnO96ajwhh&V27 zoG0^V*8QGcj)O1IrmBjph~zk6rtw@)b2O@2vubWvOY%FRW1ek>4((vE>Uurjs2K?o z#BcMHU1Isp>TFC&LW9wQjyh|-_EL9if<}RJxaXz1dyd-p`*{4>B;(7>%)IU4PbUhUn7C-~*!M*L z%=08XqiCR<2X(Jjk-Ba+Ca2wW4ES*R66Ixe#h1IrX~Y^5pYE8hbx#JMHU89yKQ8kO zG)VsQeWsBiH zANEPj+WdOs6`xqr8}6Q~Imp>^j~DkOYQDTf^%8C-PM)}p>$JbzK`=5q89nqr=x>g6 zHYMY=!H%a0AV#qwuVy!%&6p!vUS@1A zHF=75_FJP8Quwj1L1k=1&^jv~ow<&7jqzNZxHg{7iraEaV;y}|twM>7%Cp9;8&qbF zS!JJ|9AvCwi+B4sUEiN5jC!O~-6jGG;F2* zul31cx#2|0;8?PXhh_op5Gcab*nlT?vP4M-{lO*j` z{p);eW?VZ11EPo)%hPS+`{3EA@@Y(yXH~eOchCQ!o`8P6&`VzT>n?FBx-zrq@7&)T zZFZHevCpqv(mwbhCJ1)b$!bfUcB zZ9qPg&v%#KtxE55P7E41JyVea$RQ_1!~RoQtXVo;Z z0B_ViqD1ytu;b2$kfoxlf2?mu#reB+zd9SO8`YKiug$lr1ub8!cj{$SFp(hr>qqs@ z-7Wof%cGC;)$-lqKZ|T1pQ_1vcEZf^H#m8&efP`5lN>YsnFt=SG%GPKYlrYulG2+eDM?XDr)U-W4sB(Td#-e{Wzsnq)Z!cQw|y zJ#p(K2!y|*G52#H1cF#|@uYjFevarW?R2uUMaa`OuFi48ygJQ$$lop# zNO8&M`Cas?m`nKle$BY70KA?&V%8l_l7N#|v#(>TJ@01f&(*^^7rq-2MQh^25)t{N zuKIUHpBnerV2GysV!3lo$G#3jUR~vTIMUL~IIG^L9||4KE7*N!-6zX35Sz%}+Xq;I zX6qXoQ~2_JY;MKqj;!KJw|e#r(wRSZdeDPn`P;+Rup~scgShSEZG4 z+LUYfW2v`EbxrO==`w=$-Hh`jW%;ZOK9M;P-x~9g^}4HOKh>t^rrv+7d+{grIcuC2 zy}`3|(xQIm_$(F5`P+48$H7`HKW`MLznnPkdAP8_awn1+WLPG($xs!fbEm%hqW)Ru zuof)B{=-;r)xWokv+hZ*V2%cM^_zO8?371?I%TNqLcMQn1sQlRHUTZbbeF1km;ohk z*ZaHm2@1`#f32u_J=v&C@k?v$$nrd{fP$fsw%IVVd)SlvcB6u$+cv-Cp(9_Bjk%4S z=Z1r?*QbY#SfYxXw$bE!tek-t$a_ahmsN$LA|r#e=F}!exK>wNyC-WUQIy0;V3+Ky z2qsF!9G#un3!)il0yYwv8RPor1BBnvE%|o{Ao+1TfQ>?Bb0*PgQ4i6Z#9~{64L&up z`=G8{>yy?vuaFDvpxP#Dz!~$=g{;o=%M35&WY@m$lSOU#H5W>+p!Zr)YvrNZE6w{` zh}!UI<@4m2=2$AR9Gsy$mpCOpUe}ts=IBVx{Ry=%&jf=XSdxx594~g0l?Vs09~WzF z&Lc>dt3~79#YCb<=VEo-XrQRgc(JM(vw^&+^G(W_@09yt+Dxmd0Ej@NMZVvQzxq5{Wv|>lo zo7MKa)sIMvoljy^;tS$9w~KOd7m_+@hlp<1H*#_vA6$-3Rw(0` z@04n5%udtLyY7Y%)KTEcyq+)kAQXwXB(ojgQmunAY1K>(tl0UkdH(8;Q~%<@OS(7l zkx?XTwL~7l6T{l*z3r!H(SwPeMl{x#sD>HmH<(kNEnlh6I`Cc7+8k^$Lohe_)7;5)K6 za+g7UR*+Ge`{YVdkMem>J(H^5dVYP!mzvKb9q8Mj!H#96Lg8p!m+HRO7d?A;HOJ7Q zldpKb=oW)E4_7CAs&8{d4q%OOYR;$i%X~fUj#g!zsP)tup8eNpwr&&^uh)0tRn~)y zxU+sd&Bq>9Yw+8HNC}Fr7ahYVLQSttzwXyP&lAnX`p^Yghv_3xn&Gvzj*D))T71fV z>^!aO3kK?LAmC7pm5Vkfd$>})o8_#C>q}3B2)c9!NScpgUX|GPOQ{N&>v(5pWFYf+O*F2o`D=A=| z&7F#RlI zm^j_F;tZ}dKKU*9+RnbVb9wUw@=Md2I?97Sx`H?>2w!(sDpE2=Z^*-_rS zaB?QCM^8l8rs6&P!wdi4En2&EGr)dt_aS>F4sUQEWO|oI@i#RG!T6pnes0o`er1Ei zP31M((sgBUx6r)|Z)Khb?_cLM1hx15!|G~5oE;u+y2cJA*VDAIHM}2f(h_vv@N<$M z3BtCwTkW!IM6mS$(Q2Or`5galZIApi_dv8^4QN+1{dUbnC^?DMd;J+_U!N$^&R!w% z!Qx-4=Zq~c!gkQuC@H|DhaVFNB1mR_Pj!!f~;zA_Yn3i7X z(LPS^VM?rzWs^O3E(qN9ZN2h3?p4GhHvMna3!bau=~-{N@EtxL-?w?c`p8*ysky#Z ztv@ZS2mSNqoK{DQ#&^H3>xcK=)E$<0H_Ld|tB}`)%I9rSkC%IIYBJkbi?YKk>ZQ^_ z$+`U&Eow9)%}-y`v&#r}ICFZRr{`iW8S^JvjQ*E9bJY7pvn2{Oe_n=r<~DI#AIV*d zaJDS&*8AcL$yJD2L}!uajxVK+^;t}*1%$p+>uh(2YUpw5w|EL&5`D_*L<4@jWwo@w z6AvP*>G1yBPfA4xeZ?aGy?SeH-Ibh8BkMGDbZ8OrKp8JhR44J9&Qz?m{a@?%zf5Dw zUJ++sj;b&5t*cWT@oH$1R950H%^j_}#zceTu_ClF&xn3*JF|@2_Sl2gqAmMcHY~OH zwCb?f>3fOE={+;uGqY(u&0*rF=GC0fcS`V*!pD9+Y;K#w=GdIjO0maB&6+#u>M>if zSbef4J{WaqiM1jHWW!Dw_a48;^{lIR-5k5wX3O>b*U}wJ-t``s^(uM2u2n(DA}20s z&bO^#;~^=*TS0m}$hBr@shann8Dsatk@eUcnPl#J?)vp42*q5kJ@H5ybR8tok@lA5 z?V}}EiARU#vo+Gx;Pm&w(!nGnH@~ZYoF`$yGfN9NIF9?>Q{OTAS&l4pWppGrt@o&m zH(1=LVPrWiC}ZBoxN5$eXg3%6@Tw4cbY zM0R?;L>qg8E);dX{swbeSt43*lr9&~a<)X!JM_7U>rRS4+1-%|I-HvDbG~YDAD_p* zn(Zyj_t<2;<<4cDBaOn7A+zUez6NE@kCUR@O`Ne`ty>ASoA{E^jMYcCp6smtp9AyC zuVzQdwMi=J!NE0De_nIe=ML?z;hjgwUxsSaZSrh=ZdRTSTzWEGEX$iHMD}_oBGZ)= z-M^>->HAx8{k!6Gq$nPq-08|Z`vOGS3?QK2ziId3txmfX)V;Bl_HxTUq! zDTtKN6fEYtrCy0m!?|f?8U4{$W`vtpLfPv7JD;lFVDl>0=ZDg{sz9^fcO6#?FPgGV z?;|myG0P{}bJMNz#k*zE>(=mZlrJSh$udA;(K_(nydEOepK3(a1*mX%v#zwCL*++m zEu1;4(nB?fN)Nj`)JfRQ6T70yfz=(l`e>@e5Nhdnz^ zLxxDurryN)!*<-*!Q#DI($mt^v;;Ybr$G+l{>@)#P|vWwGbYv1qEr2>@9|58iqn9z z6DM^lYF|~bq<&;xr*hm`T+_y9;8nmSbNbZAynR3oUW705Dkz~9a#}G?+T(P&TDdaO zJ8J)*2Yt{Be``*gsvG;b>4vSZDhsz4H{QS)@Az9DnuQuWelbJA2v{?-}BXLj|2C+Y&#B-(9ghaxSP`9-A>Si!W6EB){DLr!kXAk@~+wDMfyvg3lp$FinI z5@j@SG;;Pznq!vFZ`QPyPG`$t#rxH|SxS};8VlAlE3Dd5^RWZb%WNAy;h;5Zeb6LU zEgKhvfOgc=kx<5IBySg$bd6{EIJsudNz_=SdEx*n`OKMZD}Z)+v*@&1X%>HJ8~;>& zK3^KQ*$*^3ao?)AU4KcOxutWm*A})ytLjD$asH9AJML-6W15PoQmS>MQ!}g*oTQ z45^B(Co#B2x^c)}34AlxB+01!W_OG6oJP`Fy2I8U-^4894gKVE{04Xk1?)YkVli>i z`iNh4It>+ywy!;-W{b1W6gTuw!{4p`=_Z_`E7@)yDlHYyW!uRZN8?7aJNHm}Z?8O{ z^*EnBDA4LMe{{83%WRo{av$vwf0ZW3PgmgOX>diT`?@eAFMjEvkYl{e?w0J;VBumx zXe~2EYe2hbvtDu39G*r`g_^_caqxoP^ptqDd0}thTFB6@9h#oc8C^!Q@a%N*O?SdI zG-jgz^EqSTA}O#1>q63blqlqj^FD*~*opx79vqOk#*?}FYy7r1cyLa z=93m@i)kqIS@piBZ^JoSjYem+PlI1VUE&Cd*2YU;(q+zgNNfu}B(}l^$0Eb3=wXLQ zJ`|BSm$^TUP7Pf@R$pSxyD78oVFI?6_o!z)gQP-fVhEXCI-mZ4?&1H=ubu=^u_Ez^ z#DYAZC6#7NDKm?7f;lsvdc`~cBHO$q8Z~Q_e`IC4{xq1%$YF*&O>&BDeig%ly_`~;3PQQ1 zC2}1(jKk|)6496281*)d=lF!n`ke6k;{y#gzwT9~O{yMpet=9UFL317UzSRn8x!Sn z!@-MRmt>HvmZHvRx}s|o=5nxeb*kpn6ebUwt8(6Yp2?ZW zoIQB*vC)w?YmDF2Clt_)pV#+ro!zvpjYC!I=hUEzHs=JM*3{i=YirzN*?q5P&*9KV z<8*ZqKi2nhookM%e9RM*Si6j^PhF2iuwww0AYE-Ua7A^PvGG}kjT}6@RxOJ~;8-aB zcA_qb6KpWl>MWW@eoFN%j==2470chyz7?`1#&JU7_^^!~chfNE1Znw~vu4sjxSUks7l zy81BW@WFJINC6#fE%%c|PZFm%D#N#wUFRFhU9}b+@$7j7LFAl|-kCEM451CL!^ zP)s9#Mg3CaBnIOiyf|5n#!nl?oOV9GXrD+ZZ(kfxd_#mvZbbY{+)s=frW1=3ClISW zU2gESw9~xne2-`iibY7pbj5N+usRQi%G7#<{^Wf+N_d*;v6E4MxZH@!iN_`8l2}+W zjiSJ$gIuLvKn^6ijzq&mg??_iY3k?lBE~8oBsMM{o!pjO3=Jo@O!kulr}gCI#j!=o zNX*Yq^ENrL4sllYMtnU{Ep-LSEL*)s<9pv(6{4c<@&Dvho4vKwYJP6lBI1cw8ohyw zsv%kTWC=wIMRG+#twTCRR#UV&*_G!@<9yCjjVY#02bg8C_EfBh)QWIhgE%F7f;QUD zbt3f7i?VGqE7mT0N{flbCl5{Yiq6aHSVedTxm)@4w0LeZ_ot!F?)}r)iDbjfpV+I& zuKbGmqp^~emfN7EaMCIhC8ljf!G9iF=QE8CYg)uNb|CtnZE)T_zNSi2)*PLxhO7~K zar2DZf#7ykq>2CU=H|8;PnMJ3H>b(V(u|o=Yge@TcF~qBZ8FDd6Rh=Q^ggURgW=av z?Dj@kZiHh$6g- z2fT_0yh!l?v}}(9!19UA@SFLgiF%S}zMmD-mT(@Skk90u=e!NkhItgDKh3X-KJ0kL zW`tv0yDi?(p0WG3ty=4Q9uWzxG+>V-{SO_t)?*|3XKl~D+m5RA>rxMl7SZ-9+F|Z_ zhq&}xjCKq;B5^N6yd&5q_5H8n9rgm6xs$~^Nc%}MC3fS!5f2sAB zaqL&PYWX)dXW?toH4$sYO?ikNLaPs*wpcTRy^~oEF@zbUbCV_4uecUB{hWJJF+cf?)QZotyM*v})=}R|D5^gezM<~$#~IdzrT8( zUFPQKb6yV1QRifO@dy6>$;rx9St!t1ha4y}oh9#}hZlDUN;R_+Bd{ zcJIu;R6G0i>Lg2Jw(I71^=g!t>$^y0PCL{y9I%^uwox{?Jt4C?&_|CJx2#)rU#S_4 zRb9q0j4`LjU7chjKGitM>0I4y)i@zv^xIP#)L~kSRuWlGeav2QF?%fR->d#!MzG;MLWFsOn$4nVr%gC~&mY8rUh_EqK&hm#W&J}i|J2PG! zFsfy{yQQ@EpLDyCf=9FY)5oB=`O~kYV<5YusEUqCE~QUZnp3}tI?aIE#(vu42PHlK zTHkHEvg3315v#a$PJF78lfew$bfTcOZ2cu(@u05NUNWN1Nv98vk;#O5mc?Rp}vxC7@B1HJQ62An(xD~ARGBc{afl+b%|tEwc^^ArcwgE+C< zR?j8QN{dcw-9)cKXnD>+pU{!D6dC1pvS#$m`;&%`_G+uW^cd0Ny0-f5V-LNpooI6? zdVD`tFU@4Vk8=W8&ep=H;<2-cd()G!{rZPVl1KeT=#Y6>>Zy@)K5W#@owE}i>`uoB zSeI z@5(o5toKTLr`Bg6&G@HFOE>gnY~w5KohmAx($9%*kAvb*qp6Rlf8i35_v}dJqdWcY zPFcin$~*XK(%>(HCYGsj(#LNhio1!U>0;3!7WmD{o_AC>r+N49G&Z@|jjo}9NCB_7 zrC{g=?;5L$M2P9wv82_#Gk>bj$>WUNR9bY+cq&(;=*AUuWEO-7KdNev&n@yRk^`y7 zLr>LlkaeB|3ctBNIJUE9^LSt9e3s9%gfJTedU|J%;kEicqj)l5RnP1VA4HFG9h?j? ze|zvWPw~d?Q8nK(ccxYp#!u7$U*OxsnDX=~Bgl?YNZF{EDObUefp%BIxV5TM&4nExY=g)8|UQq$iT)-a-4w7e+3(MS?my)vq^dmcylWG!GwG zfz4Xq$aebOzFKm69@z|sUn`oe5U~sr_I|Y-uSR@sy=OPnll-iMaHqYw`T7%89cMwi z?>Khl`|{DgFWq7OdnG68{-EMQBC=2GwQa;3#UYtXF{3fspFe)|bL3HR=y1Y=@8Bx^y;n4oheuWQVl=(e;rvARNzPx$_&n+O{M4pZbH4hBrHbS) zpGJxnC`mRl8XfojKV1I@1Cbl|yZ&X+`Ob08+5ag1b;qRE=P_)~*mjNY?cmg;ULUVbmSX*_vw&y{Ue2y7KB#r|dD&dQhaNhEw%lxO zzw}A#-`9%kxxQbQvqV2YYwi26sr1g%$-a6%U(=o%iiZc@82ztzn!Gh_G$xHJJIj~F zwI%06%d%!-^udlUdC#)!m5ps5cRb6bEpBus&ta*Xca0+y49i0s#?Ld7j4B>PwDOWx zV}5c_C+fb?7i~YrvCWR-?6A+F0&in8dwlWX8r(mWk;GL;pAxH zzZ4hwg1wJ?V8_@CR@UtClFVUj_Sve)x>nzqn@~Id)$_HMJjm58Epva6{pHuiw+{!J z)|}yG@8|1vO>8wA__(NmWXx?>n6zb!M$m>*L>wC%rabthJJP&REQGq9jJTo_9D&gv9v8PSo_cdO6P!K0aQnU&kz2Wq6#~ z8uFW#f~H92vaJh~wDjDr>DfQvDGBdPWaRF+61hJ~WLJ2MNY#4oZWZP9@{2ul^v;Ny z$S&i`TFTe@?t{ASVOfQ767PEEGiNNe$hbMp3A<;f>U7p*xpoulT1L&tOJ+IojZ|?Z z%9jY;?pZb}HcAWjbcdckC#~D17wNW~zZi@kUnBZ7m_L5f^=fH;?n0kQ_x?nqmd=)@ z`n){h`N)=9Ayil~OTI5Q`&Fk=YN`XIm<~SM9irU|v1o;;s5M zmUq8qhR@Ct`gXZiS!nr4)R{T+OqP7sLoh=uK}IEJ&xdw3Yny6bGnaX^Mp`P!PF5-m zfr9+%THMn&Dsy^6=v|M9ZPBvqNluz-CdJ0>nTbeCpF5(P51TZqoPqI$XkGf3uyrsz;Q@S0ZjbVO4+YA%p+ar z913$r>qY*o&KKd?&vr4Y{73PxJEpN2v4CFRt^dx@J6F*iJov2UFIIT$vWaa~>AgAA zBVw^^?XA);+JipU`)dws&pp;oYt_h3>-<33I*Gvg@}IY-PBN-ow_&fLomGpC=aZ|&2dPja@W^YFIM zt!Oq~%vR57^y~{K#CcXs&i`|6oF|Ytu`ti_bKV$=(CUdx&brI9drtF=E~|7qjdQx3 z@#vWxd0MUroD=Sx%9hZ*rl{_oOg zti^vVDcn2r-=^=fcl4`+yla0tSJ=}vG-1x>4q|clvZ9wQ{-AhGM<%W^a1N^Wj?s79 zL8#&n)yh}JvGh;t9^OBDnccdFQPPO)YF!OGB;tdA%WU~}+y^c2EBP(KebC7dQ(L)1 zc8YFjYmZN2M^@i5vl5;xvkASBdo-{>ibIj-*SHPIbEp#@=H?gFs>ikY*vhzxf+>J_~(34So zeZad=1956A5v(A(#2ukLCu{`wc=e4|)jTA>wK-aGKC|umWxR9ll^CaaZcc`l<%qV; zDL$Za$U;VMY%K{(l&j5-nxbj5W{#qOHrM^&R7>)jf~9nrt=<*3drm&7n1UT(;z8HT);KLQh@*W)ej`uB zjP!o7&x;Flu3s%q-f7F{y25{&EJbAR z-`j(=j#-1$DqXD+u<-GkNx-?nld;geYUr8-yiC69gBk@ykM4D*-ID`6I{+6*c%Cmq z%bl&U^6}{0<>?U7znlK;fYOC zEZ5D^V-7W^)%~9ow)mhZ+@^i3+4YliBAds3YED+?eC%_{A)Apd+VREK($4eG`Ti*0 z#w{cJyil_ktg*n3umrg^r?Chm53UYx=elfu{*(7)$|(dY*_ZsD-*vvO;f?&~iIXyd zyk$N>BL2LiJduXKe6-f?IoZ_3(Ilyk+SJ4UC=ES!l3bdoIjLTb&bCUws)>5JM75KqULFFyRA^Dj1>G78q>Dtc?=Do*y-g1y`C%X zO@5+%PxI@3SNFvhTqy19X(mrfi)Zw?kIdaK-d~#C2a|is#r!%NTQqaD9?PzBZhalJ zKc_cwUh&@fm483r*wS{1Y>zhoKG|r`JebFh`=(~9vp~<(ZDBO9sUS%J=hZxi^Je)q=E8Ttb z;9dKodN2-{Oh4L_O=w?cejS}ZYPi`~&!39L(#CQ0|LHxO%{#-VpTnzZPo`i0y*--E zvA&GnOk@YH$Vzz`tVy23b&|86t(+Itn|U7I3N5*=n@Id-=-FrmI|74u)m#K|ZufOI ztxM~VjuAxvtou2w@A3S-qP7LMYQ)YDSmNQv=STa4!TR(x)F>BlzjW_tEnW<6(st=} zIT}deyXlV8a;L{K_eQpRPg&7VYRrQ|e#_FkdHKAoxmP;p!qU5YzP{K=&__ot&5FHK z>k}(FQ@39-v>A^adf558%cvx&TFcWm|8DcTb?&Qar0+-E_b7jG)6&rd$MFR>@BF#( zz;De5jD8vBJTpV*igQE4o`nRw=W zHT(T}V1RYap%{nA^zaDYKE|3CQMX1rqCcWSsT&#j1#_Z$JF(i%f}fqmGnbiz=$pCL zSpFVevD_0e>S0=wBrA=+>;Opp_-3`#$Y)0`RXV7i7X8BUb#KXTv+zGs_k36M91jIK)50^*Q;V{VN1J(B@w*o* zI=&14q|R=Bp3yeV;)xHUlX7L*K{3w|CsL6}&$cK<&W{|CitL{Y7Kt}R7V^9J52?Vx zWd!56zO{_DI$FM6GDvo%%_9M#D{oDiO8ylJ(zaMHwDWY~l^c07l=Ew4C(+81dd~VX zZsB^|${mMi71MsMOy)k(=ceg#^&>8(qElW!zcx=ru%RJ7Q&~Ft%(w?#W>kII@`_8fg40SW zYWT{V@;mM^665U%BhNZx9@i{;9#=&7Sr5FQn=49E8$l~VwMXqTYf1UH;^U^IoG??V<^ccCmTYSD%JhqQ3dptZ($#v=_zNoRc-rgRm zl7ls3^WqBCs$DPVyVN5!2Sjk=?a{K3uJDR)t2OJ|^$#W*RJ0ji?;lW|H$Me| zWmX^6Yv-``Qd#9%wj>nAtF`OR-Xqqm>&#W+8I?0l`&8QSc$^A0--DuRJ9YU3eacox zt42qv@B8OzEMr7!wn9tS$;S@pUu~A%(!JB9y>qjZxx9pONCG}TeE{Idq1*o;c>M|(%|>l zW!GeHd#Le!)>d{Vkh8}Jyz1HP*OALz+knXYe2-I0Of*mCDH70H%9>8!q+@W)-U{}b zgj?k3kXN7~F?c&=7<{;g~Byf6DSJYfrOpoiXZ6Ktc`thnB-MAz7b zhLfZ&njkym=DI38HJ?^FEkS;AZb$0#Q!hT`b5sNyb^7UwPAID-V(@3aAMwv+A@-+} zo~+-V+3Xfd-E`L7l781lu9x$cynk#UYbn>w_*- z8P3u5>jxU(^#iY_t6G9v2I^>R_4r~+XpD=*JYh~AdhP7zno?6 znK@oW*^^Q(P1Ig1ogSQ*SCaFhMw9tEIDGs_@}_O>nc9`Z5(CPAQJm!-PT%Kw0-u$1 zuj}X%=_LghOXfR5<7# zEg0jj@pas)Ic3+VT{g5ub{?IkjUk_>9ns+Lxms=fkY!B$_*9vr(^JuRpOl~OJ9mJf z_*FlRf^|!$qvrea#+~uwJPzK;^Pp>2n?H@hc@*HMa;_;{w|N>0pVUhIY4mgpr|CVl zqKUg7E&p@sS^eZhOsBGKyL0e*O`|-=;OD$QdDr9i zNTPRp+FjKqDwWfTv)4UU8%{*stG*Jmvj;RWw(I5TT(56^9w?#Fw;uUIWXP2r`AT0o zBfX<>1E14X$ty$jji^NwJNwzWgpLHtJc`foKobMb`s*m=)oDgVy;*j%&@o1%az{@1 z8Z*z6=r+5TT$fC=-{Nmr$9#m2;3jwB_w3c~7~NjQk;sKr)MrA8dc?Ev_jYLmeeq<_ z71`YtWzQPy2sQ1|bS6U4kzT9MFNxF4W4-OkBo3JvdV2n}=8}c%*%bjx6jRjLC=w47 z{T8Pju}j*mBRTl&7bEtx>yIm5sCymTn**t%T=$R>aHVPY%lm;?Pz9PZ8q2FYux?3d-|y4COHxMdz*g?RRvFscdR$0*!r2YwEoPQHc*8q#c}QP zF4?C0SPSh($IW$Qbnh#>oU{ZIW8G7enUFTrkBe>l@9I`rl!FM?48n_iJ{7Rim-+dRRYb>gPBUeo4bo z7S~zYC)wHjVp!Yu*mo(>9gzIvsQZbgd*{`v=cvWd4JDGs)su6q#|}s%V^hGY^oW@c+CjD+Al#ji7&@v zh)i^RW6XW%W5>~*Iy`faw~wbNK@gfaXH#L84*Q3q|52^!IUY{OTGq1EHEJS7B=T(C zW0pulb3-%<%YW!3pz3 zmP3)dJ3=<6Lgbjv7V}E)^KTMyT=M0(Tru)cwf4*A{X=Imqv<=m@94Ci{MPl-xZ56H zN8KVj+Q>PQbV<*+wf1#M!w=8&NcR(Do$B$(ou2ZU^pQ}XJq=oXy`SCGT4dgX%%!Nk zdj=OHq0wxEVq$oHT?pyk!*V2lb7w6&-WQS*~U^@(-%jo~hOz%eKuMUZ$o!_mEv9 zN7Fhc-BItc-i>M*>oYzlgWf4Vnxn55v+W5XPIv#ZcrkQls2abKDEo87pXa-QL_B#~ z_v&{%f8}A#JuEVgEBnQ>7u}QBl-3CR>$?2;$L~1b?mgKb1ip>^2Tz;SNmko!^;}_^Z4^*-cRRWwZt6mDP5l| z2Wy$!?I&gZIzJaJzuey;e#XAWO5)$G`qX*H&R5W8u{~|&hV6wj*yLA{3t-_v?E1e52dcz_beFtm|Nm@7FH;QZj^+UudlTn-wfp+e(%3qPbg#}>Ugp~QR=ak4!(+E90>zW!Q?n93*65s0 z55Ms!;L_}s<0)_7$reh^7I#N2LeqoW@vyaZm@dG9)B`luwd0u4LdYyI67}( z+$tVVg2R_pjxRzk!-?MX?>y(BPRmITM`avNi-q@VKGx@cI_sNVSNU$HoW+tEYe_ao zLH+E)gVj9c$?fZTO^s7x!z7n7iX4gA+QF1LNTlwKx_WIHXqDSjpEs)wXM%VR zvgiJy>q@=9T))0kzkN}!&iS}qbmIHP`o(p<_7C@MWOWX!OC9)ZGbb@LmcEt|vseBL zefO#&wR2GYdZSiFWMhdw535KWa>lZ-eVv8u%CF^mo}<|L2`8*mFmdG78r;O>59*p! z<*M+#U-z_kv-T-?2E3;ITH)8qZ@5+!G@XfxFJZL2m-z^^DH0qSGM@P<6S5z8BiEvK zUgR02@jIeb)>u6pjb=wt_F!sTv_3U|?L)P`#|>3Mn`?L1c(+9^GS0tDmR*)m?lhjl zwpm7ZP=nRM6HV5^{?g4kId`a^G_Scf6Kb{bEg6*5JGJLC&oM0Z)V#*oc@$mIY1Tz< z`y7~8QTfR5+>hy9Q1iWCX7Oe4XmTPVYOhW649&iZ)St|w+4L+n_f(xE)*cje(g^)F z>~#Ft@j$&Ni}C+*z6F`(OUm^=m~xV=(Au9o2^Y0!E`J(Q^xSx-z9o0fD)KT|Hn^Jg z;$6gV)ZQtxUDq;S6)1r>8<~o)LuT!zdmoJCd^#*u zqDNMz%C4vN@6psJ4VbgZS`*Conx%(vea^p{$9t{rr-hq(=h7Qf>ko>aH;QIB_HO+S z)#0UcK!Yg7NNDkMHFi2A(ecD52Cve8bN%DG7yXbOl$-cckZTZp;&ZYI_Ef?xYue#G z=hLa8>R8L5{V%1-_NO_1h5dG8KiuuJ^~})Af&H#j^u-|E;*_+t!mql-{@Ys4s z`B64$y>egSv9>MStEyROSA6&{lt zI_U`juyg9Z6U%)W5ZJ}SUoZEQx?k-*OvEV}4H%#K>P zM;1yx3p!9U6DsEO2|JBlNu9A9(U|2Zme_ubhMXq4q%J9^%d7oMY$aLH)HWw48afhj z8kHnDv8O9Oa8M1TDoUJuQaxluMw4ouR7zx~Uqx5e(Usma{4!W_b;_?S`M>{Ju%3^o zHqMIN6^m(oagtonu#hjGP2Y8H!2BeD;FaTA|IbPTLjFy!u$pLgdNOb$KW3)G2{%Q}(t34=@jng2rjsYb)oReyoxCdUE{~D>2D{C~< z7-gC93r+CR{G`a`Uhd8Tb)^~Kd zLf@aPMepiELwh6H;dgZd4KJT~b7`vGCD*U=Qm4&JvDmwJS*&fWO8o4t=zRv!MBml7 zoBqAp^5dPrAC&Q8p0RZNUAo)$5zwCFNfpOOGe4cvkm>x4=9~&M&SvdBGkVINnRjRS z_^08fwi|SR&L8e-+PPI|Ytwe?ML|?;w_COw+0mHBGvW=YP{~OdPT%3N<8yX>_Lv(n zrhd5X+H&_`tT=0$NJc*;GdxMmVeF9Dw&ycv9Ca>^y;U`xGdQ|t2A=5Hf@%mh7YXmu-{6lqVab^Wals5s#Wx~yps{Bxm7+* z%g~kC_a}K4yd0wQnx2&HgnV*#q%%JJU+$~CR}@^DU6uVMPFws(NwU41v~WNFAtTMX zvNtDAiD}IDL+1X?D9ybZryYy<=vhN=&HBuDp3XhrA9}CW&}Jo2=4U;I?~2aHMbF@) z82HWV=U%-&sF{3P;~u|?W8bdU|Ed06nNiZGpJbdr)ICt*d>($N_P(zX48C5hdPEo{ z64KAD=oa)n@1d`D$MWePo-3bcR>Qs9B2_X2bT2-pectTI1Vs~U?iY0$)wmZIhKxl| z1sB|!R{rj+RnFX!Q{Zc}f!UceT4QNZB>O-11SJ=PIuWRr?#*j+sv(PGzf}D4w*B$J zS$(;Q?iuwmAS9K(?Ox4qpX--w+5Ni5ubWk+p)0$r5@ns=-7U)b1+fHC-KzFbiY}DK zDvr5eJ;NPxm==T4=G>2s&CavsIT3y4!rqzpC+XF9TGw+jkjvTc{&vrX-mGS?n_jON zNKR?&$Vf17t`GlYmck&>0|(OXI``%Tr1mX zR`dIOX8OF$UdMVi!ofKRB{l0R)JcIkCvUinW1fo5e|a)#@X#lcce}=yk&uk0lQj80 zJ5>hujiju(v>u7;vmTpbCpW6Ar+Hj$7x<7Ay*g(mm^jay&&&oJy4zjOjqK;vTdDD- z22JQn&vtOb{QaC=n(g7#r%~GZul-PGMlze*c{xIs3Hd-@MX> z_pAM`&L^LC?2=C_-2C9j@T1MfQX`rrbAw}FO!lQEB3bX8BY{68i(Ghby`_inuIF~! zY^5H;UA<$~-c{ZVY3_(`*6-j4D#)wx;NiS?PrY}C-&@8rhV>I_PcsYQ4+&0Yj9inhXndL+Raxnc zwetKdnkm>Lv{@Uu%eU5^Z`?h&l^I#vx!mtwqlTng7j#_+Dk6V}ozL6jZ%uz=T6^v_ zCi*B<2OSBRSJSi>>oI1`!H-#6!#179pKmm0#{XWJ)@bfk{U_dhI<2E<7YOa#POtKh zHG(H~KP0hNby`k${`=Hc;tfAm>k#)R^?a=TDMIsJ^}?_HeXWF(R#|3Epzxz=fdrf3 zoDd+=|6_dwH-2^^pW^84-j?0H|qyW^)_C~ws>4Mhf>h_sY~ zbJJ&jh1t}rwY=GOJhn}b_R`ccEuSkM6{odv=|DO*{kipZgIZ^JzFl;pOH0{7} z|2(znmtEC(eR_2xq)@fger<1p8Cd^l^f@Pj^!G|bSOr0&^RoomsE0LLw2X5F8x0$m zR&yTs+?!Zx?AGksYUCa?GEUWMLyRw*_+;kPJgIthvd@% zUlvtR9?in)1+{1y7qlE3#*R3f>sqzQF47C1)H`Rv_$4%LUjyNV-qSj|+7FIwvr8zm zzQYUO;6BOA3G}QRdfL1+sVUz#O zk;dil2HvA87&~$)CP7b+h*3DhLdC-NSgl3p)LH-TfA$?iTzmWs%>;Yh2w;_P^I12 z*PM+u`i8^l*4E3r#ydRHfeFoa*N4!rt?Pp&H#K|vHnEAJEGVPLNz=E55}C_0L3|sJ zpf#~qxc2;{H(sB#X(BEIh0=X=QSixnTt+eWcg$N4U89HpR{dxd2Sd3T8dSbszllbV zxj~m!p0^f_hCHbse{C9PEakt}Jo9DPoZz$figJAL`(YNrcs8|mGTF&CKvmi_qvy)9 zCzh7T`+j}KyXWPzA1~i-WL7uYuNGCotciE2yV{AI5*L))5h8GbzSLtoc^qe?V76V!Z#yZPse6BDDi^ zbPiyiX&%2uD-7*)G}a475XnhobB@slW=Vu+=&Qu866?*31Y1Q5&d+&&y+#$PK0NcM zvTTD^crd-vu)#y-a=!?eFwGx_E-3ze3-UPr4C=!fOm{B;`b zNA>#I^zM^dnIBK@{-u8Xs62Aq==ZR)R#RDey;{h%*C(w>%YIPbx#N0iI%P+wD@Ir zrr+68=+CFn#Zo34G`At$Pjh`a|5}Ev{x*4gWMIr?;uxZ!|37>0x?JV4tm$$A```Z6 z|7h=sJ?S-Cme?+@rs?%k_pkq|H?ySJALZk%6#P9fvF>N%8anLgvgaKO7_rg zp!`S&e0?+H+IR68{)?#tXXu}i`diHBYP=;6MfFG4yNj6rQz+eb4FHcxa})3Z&yF4?Z1Ux#pheT59W8HXu$b5b8XFd_oyI(wjapkTG6A} zTJrJl;yJAU`S|o<{QpY)pO}gpy13Vhd(6(qX!-l6&|#Ou@~xN8_u~KDhqV{qT%CCZ zc_-JR{_XQ61>B}Zi`))&IcVVjS4+>2<5Os$NA3&5+vBb-XoWhc5K>j&3O0`dMvq_1 z&LgZCeu6o1HriT`&o4*2+yX{Va0a^8<5_k$J&14i;`^)d>0bE;{z0+t_nF2CxbURv zTcbQ1y`GJ>pky!l>w-4@J8_is{+gqgsNU2tVv{&KE$Q-(dX6p$Erae@E7y(IR4K( zor@Nb+Dql%a+GLS$x5`TxUkC;abi7seGt7M5y%U(!K`slAM1mEtPoZclFX`A9&oC> zcz=tkZvP&w{2KF@lEVr;7jtwWT4z06j3-#HtPuFX`n?eUSk+JmU!aP64*C3GTp>p6 zqXh`G>CV&)Esfjg`&$vUg=O3-yMs_+1R3)_apAd1FbBu&hM zbO_vH1;HbD$CE!r&(LyyMhlimdJm4lzk|_4ejcP9oWGW4dgMes{*(T`5>z}4YLH!K zNEAZRg`idXZ!dmF`yEV%k{7HQRx28nUb!a$osJGhZjdAXp^ww?Lp%ASK50dBYIuu1U&!Z(HjPQ#ZMCP!&3r%y7?WZky+KJPIT4aWi_R%jy z##wPuSJKYwSJjMlb-we;qKK+TFhtr1rJ*gT2QO+zEu3(81bR>Sg6bHFIH9(Cu4~ph zBU#J?c@|JK^VRh|qE8j0%zzP>dDL|=-t>!^6(hfVDnw|g`bYf|kF;D90^^#*VLli$ z@y~f!4a_0@fth=5S#i0jJh@LF`<$ybB6p5Cw0EZcxzKD=fJk08XQzwwjO;=&Dhvh z;yrfmU_+RNVIPujz~#BC3>-#!rN7TSKf`W4uKZY@{aZYDKKS>pJOz3WesVrORa=t6-{MofiB04^Xu$)~eiJ?+0{yk{ zMAiS$K%Ny_ZZ5qpe}N^sf;s%3q?*7#T+Y^ya?_v`%9e+^IGc*R!Cj{M|1<)`pQ z^b=mqN!FYdd6G3JcG5~d$(o;J%~ctpm+Rd{1`~Ss!HyLBw_=zI%PAeD}D&wosrin z7Juq99hR#ACpWQP{9?QY)=OPQ&v@>KH|EnCK9@5cme1+;Gxe&w%-tbv9Hx2ZBoTY1 z8UKI-lT)I4licmRQxJ>}u7p0=D=ShNblxTdOD>W#50R&F4^q`rq6sa=Ze`s%lsY2O zIHZ@_qr@X!S(zG&>Yd!HI)rbI_lZ_V|Du_|

A!?R&p9c%vzMJ+lX@5tD@g4bStp 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 ZeO`8G{IO%;8A$B1EyyYAja$#c#7vD| Oxp+D?-C1t&ct3t)T@eKU diff --git a/reports/vm-prebuilt-inventory-20260325/npm-global.txt b/reports/vm-prebuilt-inventory-20260325/npm-global.txt deleted file mode 100644 index 2bad233c..00000000 --- a/reports/vm-prebuilt-inventory-20260325/npm-global.txt +++ /dev/null @@ -1,22 +0,0 @@ -@mermaid-js -corepack -docx -graphviz -markdownlint-cli -markdownlint-cli2 -markdown-toc -marked -npm -pdfjs-dist -pdf-lib -playwright -pptxgenjs -react -react-dom -react-icons -remark-cli -remark-preset-lint-recommended -sharp -ts-node -tsx -typescript diff --git a/reports/vm-prebuilt-inventory-20260325/pip-dist-info.txt b/reports/vm-prebuilt-inventory-20260325/pip-dist-info.txt deleted file mode 100644 index 9d0b3642..00000000 --- a/reports/vm-prebuilt-inventory-20260325/pip-dist-info.txt +++ /dev/null @@ -1,83 +0,0 @@ -backrefs-6.2 -beautifulsoup4-4.14.3 -blinker-1.9.0 -camelot_py-1.0.9 -charset_normalizer-3.4.6 -click-8.3.1 -contourpy-1.3.3 -cycler-0.12.1 -defusedxml-0.7.1 -deprecated-1.3.1 -docopt-0.6.2 -et_xmlfile-2.0.0 -flask-3.1.3 -flatbuffers-25.12.19 -fonttools-4.62.1 -ghp_import-2.1.0 -grip-4.6.2 -imageio_ffmpeg-0.6.0 -imageio-2.37.3 -img2pdf-0.6.3 -itsdangerous-2.2.0 -joblib-1.5.3 -kiwisolver-1.5.0 -lazy_loader-0.5 -lxml-6.0.2 -magika-0.6.3 -markdown-3.10.2 -markdownify-1.2.2 -markitdown-0.1.5 -marko-2.2.2 -matplotlib-3.10.8 -mergedeep-1.3.4 -mistune-3.2.0 -mkdocs_get_deps-0.2.2 -mkdocs_material_extensions-1.3.1 -mkdocs_material-9.7.6 -mkdocs-1.6.1 -mpmath-1.3.0 -networkx-3.6.1 -numpy-2.4.3 -odfpy-1.4.1 -onnxruntime-1.24.4 -opencv_contrib_python-4.13.0.92 -opencv_python_headless-4.13.0.92 -opencv_python-4.13.0.92 -openpyxl-3.1.5 -paginate-0.5.7 -pandas-3.0.1 -path_and_address-2.0.1 -pathspec-1.0.4 -pdf2image-1.17.0 -pdfkit-1.0.0 -pdfminer_six-20251230 -pdfplumber-0.11.9 -pikepdf-10.5.1 -pillow-12.1.1 -protobuf-7.34.1 -pymdown_extensions-10.21 -pymupdf-1.27.2.2 -pypdf-5.9.0 -pypdfium2-5.6.0 -pytesseract-0.3.13 -python_dateutil-2.9.0.post0 -python_docx-1.2.0 -python_dotenv-1.2.2 -python_pptx-1.0.2 -pyyaml_env_tag-1.1 -reportlab-4.4.10 -scikit_image-0.26.0 -scikit_learn-1.8.0 -scipy-1.17.1 -seaborn-0.13.2 -soupsieve-2.8.3 -sympy-1.14.0 -tabula_py-2.10.0 -tabulate-0.10.0 -threadpoolctl-3.6.0 -tifffile-2026.3.3 -wand-0.7.0 -watchdog-6.0.0 -werkzeug-3.1.7 -wrapt-2.1.2 -xlsxwriter-3.2.9 diff --git a/reports/vm-prebuilt-inventory-20260325/pip-freeze-new.txt b/reports/vm-prebuilt-inventory-20260325/pip-freeze-new.txt deleted file mode 100644 index 270b48a98e0ead5f965e165752f859afb0e08394..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1246 zcmY*Y!A|2)5c3&nKLtdR7T5y^?jUjHgwk%H5upuf3N0TGYmeU}MbS&7%#7{v%=`Yj z#{mzx!5waK&F2DVIOnPG2Pdd8;2BG7@QNNC8jL*3E0}Y|Q$*f^e{1X*ZSll!hb?P% zteHjjEHd>r?vnXK%5Ed0Bx@P$C9_J*19yp4XxIkxDb_?CsHG-Di_bujnu;|-zerVz zF7Xn`QaN>`Ub;Z314q@0s(O~HH{=;Z|G*9ofuerJQl^{(@#V)5Q6UtJWj~%+EexTE zZyQ-wIAyi?$E-SH!Y-1VFI4}*BM&>g|FB5Ioaa0=bl9_lE~@j?tWN%nF|Q53@?vqs zA+1+|u;zp`S9fUNvxhw~eYoO+_S3VvtcLR7x$eV zQoK)?ib$2!JF-tzj<{nf73)C8Ns~FJ>H~XTk#~*LP^XRcX4)&U{Y{KL^R~>N@Q-Wd zUPGMsA1$iQD#i1eI>@^#cQU4L#z2HO@;AZbJrGJ2+u|+vcI&qM-j{jk^lWt-)%D~{ zfy?N^5#KgtG_f)ATs+!-Ph_NX3o7aO&9~TltG68(!(n^-NThvoK)7;r+T-GA6Xk>U zKn&)UAKFwi?>I5-g7&aV8QQZcE(dhigt3YYIq93L{&r~w)~lG^{a|*($CFjG3RfKc E4;t>Xh5!Hn diff --git a/reports/vm-prebuilt-inventory-20260325/rebuild-comparison.md b/reports/vm-prebuilt-inventory-20260325/rebuild-comparison.md deleted file mode 100644 index 41620028..00000000 --- a/reports/vm-prebuilt-inventory-20260325/rebuild-comparison.md +++ /dev/null @@ -1,52 +0,0 @@ -# openagent-prebuilt Rebuild Comparison (2026-03-25) - -## Summary - -- Old prebuilt tar (before clean rebuild): `6,693,079,040` bytes (`6.23 GB`) -- New prebuilt tar (rebuilt from clean `openagent-build` + current setup scripts): `2,019,061,760` bytes (`1.88 GB`) -- Delta: `-4,674,017,280` bytes (`-4.35 GB`, about `-69.8%`) - -## Hash - -- Old SHA256: `61279AF095540D3C1290BDF8B2BA1F4094BD128C347E873BEF0F9D25A56986D6` -- New SHA256: `8D8F7F8718891C8242DEE44409EA92EB57B912FCBA53F427622C2DE70B92A022` - -## Package Count Changes - -- APT packages: - - old: `964` - - new: `386` - - source files: - - `apt-installed.txt` - - `apt-installed-new.txt` -- Python packages: - - old: `83` (from old dist-info snapshot extraction) - - new: `35` (from `pip list --format=freeze` in rebuilt distro) - - source files: - - `pip-dist-info.txt` - - `pip-freeze-new.txt` -- NPM global packages: - - old: `22` - - new: `5` - - source files: - - `npm-global.txt` - - `npm-global-new.txt` - -## New Prebuilt Size Hotspots - -Top large files (`>50 MB`) in rebuilt tar: - -- `opt/pw-browsers/chromium-1208/chrome-linux64/chrome` (`257.28 MB`) -- `opt/pw-browsers/chromium_headless_shell-1208/chrome-headless-shell-linux64/chrome-headless-shell` (`175.05 MB`) -- `usr/bin/node` (`118.90 MB`) -- `usr/lib/x86_64-linux-gnu/libLLVM-15.so.1` (`111.46 MB`) -- `usr/local/bin/uv` (`56.33 MB`) - -## Notes - -- During the first rebuild attempt, `05_pip.sh` did not fail even when pip installs failed. -- Root causes fixed in source: - - `setup.sh`: `pip_install` now conditionally adds `--break-system-packages` only when supported. - - `05_pip.sh`: now uses `set -euo pipefail` to fail fast on pip errors. - - `03_apt.sh`: each `apt_install` call now hard-fails on error (`|| exit 1`). -- After fixes, rebuild completed and dependencies were actually installed. From c7b9224c6d7896ab503ffb54e8b1f0f0d4fe4b8d Mon Sep 17 00:00:00 2001 From: "Anqi (Anthony) Tang" Date: Fri, 27 Mar 2026 17:10:11 +0800 Subject: [PATCH 17/34] fix(tests): update environment tests to expect ValueError on empty datetime The environment.py revert restored the ValueError behavior for empty datetime, but the PR's tests still expected the removed graceful fallback. Update test_empty_datetime and test_pads_missing_parts to assert ValueError is raised. --- .../unit_tests/harness/test_environment.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/libs/hexagent/tests/unit_tests/harness/test_environment.py b/libs/hexagent/tests/unit_tests/harness/test_environment.py index a48fa284..9681f0a6 100644 --- a/libs/hexagent/tests/unit_tests/harness/test_environment.py +++ b/libs/hexagent/tests/unit_tests/harness/test_environment.py @@ -6,6 +6,8 @@ from datetime import UTC, datetime from unittest.mock import AsyncMock +import pytest + from hexagent.harness.environment import EnvironmentResolver from hexagent.types import CLIResult @@ -81,20 +83,20 @@ async def test_datetime_without_timezone_fallback(self) -> None: assert env.today_date.year == 2026 assert env.today_date.tzinfo is None - async def test_empty_datetime_fallback_now(self) -> None: + async def test_empty_datetime_raises(self) -> None: + """Empty datetime must raise — it indicates a broken shell probe.""" computer = _mock_computer(_make_stdout(date="")) - env = await EnvironmentResolver(computer).resolve() - assert isinstance(env.today_date, datetime) + with pytest.raises(ValueError, match="empty datetime"): + await EnvironmentResolver(computer).resolve() - async def test_pads_missing_parts(self) -> None: - """When stdout has fewer delimiters, missing fields are padded.""" + async def test_pads_missing_parts_raises(self) -> None: + """When stdout has fewer delimiters, missing date field raises.""" # Only cwd and git — missing platform, shell, os_version, date stdout = f"/home/user\n{_DELIM}\ntrue" computer = _mock_computer(stdout) - # Date will be empty -> fallback to current time. - env = await EnvironmentResolver(computer).resolve() - assert isinstance(env.today_date, datetime) + with pytest.raises(ValueError, match="empty datetime"): + await EnvironmentResolver(computer).resolve() async def test_darwin_platform(self) -> None: computer = _mock_computer(_make_stdout(platform="darwin", os_version="Darwin 25.3.0")) From 02d8b252cf5da3284897af4aabfa42c591792736 Mon Sep 17 00:00:00 2001 From: "Anqi (Anthony) Tang" Date: Fri, 27 Mar 2026 17:28:43 +0800 Subject: [PATCH 18/34] security: fix CodeQL alerts for clear-text key storage and exception exposure - OnboardingWizard.tsx: exclude API keys (apiKey, sumApiKey, searchKey, fetchKey, e2bKey) from localStorage draft persistence; only non-sensitive form state (provider IDs, model IDs, display names, URLs) is saved - routes/setup.py: sanitize _run_wrapper exception handler to emit a generic error message to the SSE stream instead of raw str(exc) which could expose stack traces or internal paths; full details are still logged server-side via logger.exception() --- .../backend/hexagent_api/routes/setup.py | 36 +++++++++---------- .../src/components/OnboardingWizard.tsx | 20 ----------- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/libs/hexagent_demo/backend/hexagent_api/routes/setup.py b/libs/hexagent_demo/backend/hexagent_api/routes/setup.py index b09d0c59..034db6f5 100644 --- a/libs/hexagent_demo/backend/hexagent_api/routes/setup.py +++ b/libs/hexagent_demo/backend/hexagent_api/routes/setup.py @@ -179,11 +179,11 @@ def _lima_status() -> dict[str, object]: # WSL (Windows) # --------------------------------------------------------------------------- -_WSL_INSTANCE = "openagent" +_WSL_INSTANCE = "hexagent" _WSL_EXPORT_SOURCE = "Ubuntu" _WSL_PREBUILT_CANDIDATES = ( - "openagent-prebuilt.tar", - "openagent.tar", + "hexagent-prebuilt.tar", + "hexagent.tar", ) @@ -372,7 +372,7 @@ async def _wsl_probe_start() -> tuple[bool, str]: # ``wsl -l -v`` uses the Windows display language for the STATE column. -# Cowork only needs the ``openagent`` distro to exist; WSL starts it on demand. +# Cowork only needs the ``hexagent`` distro to exist; WSL starts it on demand. _WSL_COWORK_READY_STATES = frozenset( { "Running", @@ -855,11 +855,11 @@ async def _run_wrapper(self, **kwargs: object) -> None: self._status = "error" self._error = "Cancelled" self._emit("error", {"message": "Cancelled"}) - except Exception as exc: + except Exception: logger.exception("%s failed", self.__class__.__name__) self._status = "error" - self._error = str(exc) - self._emit("error", {"message": str(exc)}) + self._error = "Internal error" + self._emit("error", {"message": "An internal error occurred — check server logs for details."}) finally: self._new_event.set() @@ -1049,7 +1049,7 @@ async def _run_wsl(self) -> None: else: err = _combine_wsl_output(stdout_b, stderr_b) if _looks_like_missing_wsl_disk(err): - self._emit("progress", {"step": "creating", "message": "Detected broken WSL distro disk. Recreating OpenAgent distro..."}) + self._emit("progress", {"step": "creating", "message": "Detected broken WSL distro disk. Recreating HexAgent distro..."}) proc_unreg = await asyncio.create_subprocess_exec( wsl_exe, "--unregister", _WSL_INSTANCE, stdout=asyncio.subprocess.PIPE, @@ -1059,7 +1059,7 @@ async def _run_wsl(self) -> None: u_out_b, u_err_b = await self._communicate_with_heartbeat( proc_unreg, step="creating", - message="Removing broken OpenAgent WSL distro...", + message="Removing broken HexAgent WSL distro...", ) if proc_unreg.returncode != 0: u_err = _combine_wsl_output(u_out_b, u_err_b) @@ -1077,9 +1077,9 @@ async def _run_wsl(self) -> None: prebuilt_tar = _wsl_prebuilt_tar_path() import_dir = data_dir() / "wsl" / _WSL_INSTANCE / "disk" - # Distro does not exist: prefer bundled prebuilt OpenAgent rootfs. + # Distro does not exist: prefer bundled prebuilt HexAgent rootfs. if prebuilt_tar is not None: - self._emit("progress", {"step": "creating", "message": "Importing bundled OpenAgent VM image..."}) + self._emit("progress", {"step": "creating", "message": "Importing bundled HexAgent VM image..."}) if import_dir.exists(): shutil.rmtree(import_dir, ignore_errors=True) import_dir.mkdir(parents=True, exist_ok=True) @@ -1093,7 +1093,7 @@ async def _run_wsl(self) -> None: _, err_b = await self._communicate_with_heartbeat( proc_import, step="creating", - message="Importing bundled OpenAgent VM image...", + message="Importing bundled HexAgent VM image...", progress_info=lambda: f"(image ~{(prebuilt_tar.stat().st_size / (1024 * 1024)):.1f} MB)", ) if proc_import.returncode != 0: @@ -1103,7 +1103,7 @@ async def _run_wsl(self) -> None: self._error = f"exit {proc_import.returncode}" return - self._emit("progress", {"step": "starting", "message": "Starting imported OpenAgent WSL distro..."}) + self._emit("progress", {"step": "starting", "message": "Starting imported HexAgent WSL distro..."}) proc_start = await asyncio.create_subprocess_exec( wsl_exe, "-d", _WSL_INSTANCE, "--", "echo", "ok", stdout=asyncio.subprocess.PIPE, @@ -1113,7 +1113,7 @@ async def _run_wsl(self) -> None: out_b, err_b = await self._communicate_with_heartbeat( proc_start, step="starting", - message="Starting imported OpenAgent WSL distro...", + message="Starting imported HexAgent WSL distro...", ) if proc_start.returncode == 0: self._emit("done", {"message": "WSL distro imported from bundled image and started successfully"}) @@ -1187,7 +1187,7 @@ async def _run_wsl(self) -> None: self._error = f"exit {proc_export.returncode}" return - self._emit("progress", {"step": "creating", "message": "Importing OpenAgent WSL distro..."}) + self._emit("progress", {"step": "creating", "message": "Importing HexAgent WSL distro..."}) proc_import = await asyncio.create_subprocess_exec( wsl_exe, "--import", _WSL_INSTANCE, str(import_dir), str(export_tar), "--version", "2", stdout=asyncio.subprocess.PIPE, @@ -1197,7 +1197,7 @@ async def _run_wsl(self) -> None: _, err_b = await self._communicate_with_heartbeat( proc_import, step="creating", - message="Importing OpenAgent WSL distro...", + message="Importing HexAgent WSL distro...", ) if proc_import.returncode != 0: err = _decode_wsl_output(err_b or b"").strip() @@ -1206,7 +1206,7 @@ async def _run_wsl(self) -> None: self._error = f"exit {proc_import.returncode}" return - self._emit("progress", {"step": "starting", "message": "Starting OpenAgent WSL distro..."}) + self._emit("progress", {"step": "starting", "message": "Starting HexAgent WSL distro..."}) proc_start = await asyncio.create_subprocess_exec( wsl_exe, "-d", _WSL_INSTANCE, "--", "echo", "ok", stdout=asyncio.subprocess.PIPE, @@ -1216,7 +1216,7 @@ async def _run_wsl(self) -> None: out_b, err_b = await self._communicate_with_heartbeat( proc_start, step="starting", - message="Starting OpenAgent WSL distro...", + message="Starting HexAgent WSL distro...", ) if proc_start.returncode == 0: self._emit("done", {"message": "WSL distro created and started successfully"}) diff --git a/libs/hexagent_demo/frontend/src/components/OnboardingWizard.tsx b/libs/hexagent_demo/frontend/src/components/OnboardingWizard.tsx index 94298b13..e7aeebd8 100644 --- a/libs/hexagent_demo/frontend/src/components/OnboardingWizard.tsx +++ b/libs/hexagent_demo/frontend/src/components/OnboardingWizard.tsx @@ -89,21 +89,16 @@ const ONBOARDING_DRAFT_KEY = "hexagent-onboarding-draft-v1"; interface OnboardingDraft { step?: Step; selectedProviderId?: string; - apiKey?: string; modelId?: string; displayName?: string; baseUrl?: string; sumProviderId?: string; - sumApiKey?: string; sumModelId?: string; sumDisplayName?: string; sumBaseUrl?: string; sumSameAsMain?: boolean; searchProvider?: string; - searchKey?: string; fetchProvider?: string; - fetchKey?: string; - e2bKey?: string; vmSkipped?: boolean; } @@ -197,21 +192,16 @@ export default function OnboardingWizard({ open, onComplete, settings, onSetting if (draft) { if (draft.step && STEPS.includes(draft.step)) setStep(draft.step); if (draft.selectedProviderId) setSelectedProvider(PROVIDERS.find((p) => p.id === draft.selectedProviderId) ?? null); - if (typeof draft.apiKey === "string") setApiKey(draft.apiKey); if (typeof draft.modelId === "string") setModelId(draft.modelId); if (typeof draft.displayName === "string") setDisplayName(draft.displayName); if (typeof draft.baseUrl === "string") setBaseUrl(draft.baseUrl); if (draft.sumProviderId) setSumProvider(PROVIDERS.find((p) => p.id === draft.sumProviderId) ?? null); - if (typeof draft.sumApiKey === "string") setSumApiKey(draft.sumApiKey); if (typeof draft.sumModelId === "string") setSumModelId(draft.sumModelId); if (typeof draft.sumDisplayName === "string") setSumDisplayName(draft.sumDisplayName); if (typeof draft.sumBaseUrl === "string") setSumBaseUrl(draft.sumBaseUrl); if (typeof draft.sumSameAsMain === "boolean") setSumSameAsMain(draft.sumSameAsMain); if (typeof draft.searchProvider === "string") setSearchProvider(draft.searchProvider); - if (typeof draft.searchKey === "string") setSearchKey(draft.searchKey); if (typeof draft.fetchProvider === "string") setFetchProvider(draft.fetchProvider); - if (typeof draft.fetchKey === "string") setFetchKey(draft.fetchKey); - if (typeof draft.e2bKey === "string") setE2bKey(draft.e2bKey); if (typeof draft.vmSkipped === "boolean") setVmSkipped(draft.vmSkipped); } @@ -223,42 +213,32 @@ export default function OnboardingWizard({ open, onComplete, settings, onSetting saveOnboardingDraft({ step, selectedProviderId: selectedProvider?.id, - apiKey, modelId, displayName, baseUrl, sumProviderId: sumProvider?.id, - sumApiKey, sumModelId, sumDisplayName, sumBaseUrl, sumSameAsMain, searchProvider, - searchKey, fetchProvider, - fetchKey, - e2bKey, vmSkipped, }); }, [ open, step, selectedProvider, - apiKey, modelId, displayName, baseUrl, sumProvider, - sumApiKey, sumModelId, sumDisplayName, sumBaseUrl, sumSameAsMain, searchProvider, - searchKey, fetchProvider, - fetchKey, - e2bKey, vmSkipped, ]); From 3b6d55aab94c78c43bb8e47b4ddce72d13171b30 Mon Sep 17 00:00:00 2001 From: "Anqi (Anthony) Tang" Date: Fri, 27 Mar 2026 17:54:45 +0800 Subject: [PATCH 19/34] fix(wsl): eliminate redundant wsl.exe resolution and preserve exception chain _check_wsl_prerequisites() now returns the validated wsl.exe path so WslVM.__init__ can use it directly, removing the redundant second call to _resolve_wsl_exe() and the assert that would be silently stripped under python -O in packaged builds. In agent_manager, the catch-all cowork setup error now raises from exc instead of from None so unexpected failures carry a full traceback for debugging. Known operational states (distro not found, VM not running) correctly keep from None since their rewrites are intentional. --- libs/hexagent/hexagent/computer/local/_wsl.py | 18 ++++++++++-------- .../backend/hexagent_api/agent_manager.py | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/libs/hexagent/hexagent/computer/local/_wsl.py b/libs/hexagent/hexagent/computer/local/_wsl.py index 7a6aa956..b62909a3 100644 --- a/libs/hexagent/hexagent/computer/local/_wsl.py +++ b/libs/hexagent/hexagent/computer/local/_wsl.py @@ -100,8 +100,8 @@ def _ensure_proactor_event_loop() -> None: asyncio.set_event_loop_policy(proactor_cls()) # type: ignore[deprecated,unused-ignore] -def _check_wsl_prerequisites() -> None: - """Verify we are on Windows with WSL available. +def _check_wsl_prerequisites() -> str: + """Verify we are on Windows with WSL available and return the wsl.exe path. Also ensures the ``ProactorEventLoop`` policy is active. On Windows, uvicorn (and some other frameworks) force ``SelectorEventLoop``, which @@ -110,6 +110,9 @@ def _check_wsl_prerequisites() -> None: instantiates ``WslVM`` gets it automatically, regardless of the application entry point. + Returns: + Absolute path to ``wsl.exe``. + Raises: UnsupportedPlatformError: If not on Windows. MissingDependencyError: If ``wsl.exe`` is not found. @@ -117,13 +120,15 @@ def _check_wsl_prerequisites() -> None: if _PLATFORM != "win32": msg = f"WSL is a Windows subsystem — it cannot run on {_PLATFORM}" raise UnsupportedPlatformError(msg) - if _resolve_wsl_exe() is None: + wsl_exe = _resolve_wsl_exe() + if wsl_exe is None: msg = "wsl.exe not found. Install WSL2: https://learn.microsoft.com/windows/wsl/install" raise MissingDependencyError(msg) # Ensure ProactorEventLoop is used so create_subprocess_exec works. # SelectorEventLoop (uvicorn's default on Windows) does not support it. _ensure_proactor_event_loop() + return wsl_exe class WslVM: @@ -134,10 +139,7 @@ class WslVM: """ def __init__(self, instance: str) -> None: - _check_wsl_prerequisites() - wsl_exe = _resolve_wsl_exe() - assert wsl_exe is not None # noqa: S101 - self._wsl_exe = wsl_exe + self._wsl_exe = _check_wsl_prerequisites() self._instance = instance self._unc_prefix: str | None = None # cached after first successful probe @@ -577,7 +579,7 @@ async def _apply_bind_mounts(self) -> None: # Windows ACLs allow writes. chown maps ownership to the session Linux user so # mkdir/Write behave consistently. sess = _session_user_from_guest_mount_path(m.guest_path) - skip_chown = os.environ.get("OPENAGENT_WSL_SKIP_SESSION_MOUNT_CHOWN", "").strip().lower() in ( + skip_chown = os.environ.get("HEXAGENT_WSL_SKIP_SESSION_MOUNT_CHOWN", "").strip().lower() in ( "1", "true", "yes", diff --git a/libs/hexagent_demo/backend/hexagent_api/agent_manager.py b/libs/hexagent_demo/backend/hexagent_api/agent_manager.py index 20a3164e..31384445 100644 --- a/libs/hexagent_demo/backend/hexagent_api/agent_manager.py +++ b/libs/hexagent_demo/backend/hexagent_api/agent_manager.py @@ -74,7 +74,7 @@ async def _verify_session_dir_writable(self, session_name: str, guest_dir: str) if vm is None: return - probe = f"{guest_dir.rstrip('/')}/.openagent_write_probe_{os.getpid()}" + probe = f"{guest_dir.rstrip('/')}/.hexagent_write_probe_{os.getpid()}" cmd = ( f"test -d {shlex.quote(guest_dir)} && " f"test -w {shlex.quote(guest_dir)} && " @@ -210,7 +210,7 @@ async def _ensure_computer( "VM is not running. " "Please set it up in Settings \u2192 Sandbox." ) from None - raise RuntimeError(f"Cowork session setup failed: {detail}") from None + raise RuntimeError(f"Cowork session setup failed: {detail}") from exc actual_name = computer.session_name self._computers[actual_name] = computer From d17cf8d824aeb69548d0d347c76a614e8ab27fef Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Fri, 27 Mar 2026 18:52:54 +0800 Subject: [PATCH 20/34] security(codeql): avoid exposing exception details in setup routes --- .../hexagent_demo/backend/hexagent_api/routes/setup.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libs/hexagent_demo/backend/hexagent_api/routes/setup.py b/libs/hexagent_demo/backend/hexagent_api/routes/setup.py index 034db6f5..61d9a89d 100644 --- a/libs/hexagent_demo/backend/hexagent_api/routes/setup.py +++ b/libs/hexagent_demo/backend/hexagent_api/routes/setup.py @@ -266,7 +266,8 @@ def _probe_wsl2_readiness() -> tuple[bool, str | None]: timeout=8, ) except Exception as exc: # pragma: no cover - defensive - return False, str(exc) + logger.warning("WSL readiness probe failed", exc_info=exc) + return False, "Failed to probe WSL runtime" combined = _combine_wsl_output(proc.stdout, proc.stderr).strip() reason = _wsl2_blocker_reason(combined) @@ -456,7 +457,8 @@ def _wsl_status() -> dict[str, object]: timeout=8, ) except Exception as exc: # pragma: no cover - defensive - last_err = str(exc) + logger.warning("WSL status probe failed for args=%s", args, exc_info=exc) + last_err = "Failed to probe WSL runtime" continue if proc.returncode == 0: @@ -592,9 +594,9 @@ def _extract() -> None: _ensure_managed_lima_on_path() yield sse("done", {"message": f"Lima v{version} installed successfully", "path": str(_lima_bin())}) - except Exception as exc: + except Exception: logger.exception("Lima installation failed") - yield sse("error", {"message": str(exc)}) + yield sse("error", {"message": "Lima installation failed. Check server logs for details."}) finally: shutil.rmtree(tmp_dir, ignore_errors=True) From d8261268690c3965a853ea9149b40efe5f113560 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Sat, 28 Mar 2026 07:41:41 +0800 Subject: [PATCH 21/34] fix(cowork): harden WSL shell decoding and environment probing --- libs/hexagent/hexagent/computer/local/_wsl.py | 15 ++- libs/hexagent/hexagent/harness/environment.py | 94 ++++++++++++++----- .../hexagent/tools/ui/present_to_user.py | 7 +- .../unit_tests/harness/test_environment.py | 59 +++++++++--- .../tools/ui/test_present_to_user.py | 5 + 5 files changed, 140 insertions(+), 40 deletions(-) diff --git a/libs/hexagent/hexagent/computer/local/_wsl.py b/libs/hexagent/hexagent/computer/local/_wsl.py index b62909a3..4f2dc500 100644 --- a/libs/hexagent/hexagent/computer/local/_wsl.py +++ b/libs/hexagent/hexagent/computer/local/_wsl.py @@ -45,6 +45,13 @@ _PLATFORM = sys.platform +def _decode_wsl_output(raw: bytes) -> str: + """Decode WSL output that may be UTF-16-LE on some Windows builds.""" + if raw[:2] == b"\xff\xfe" or b"\x00" in raw: + return raw.decode("utf-16-le", errors="replace").replace("\x00", "") + return raw.decode("utf-8", errors="replace") + + def _resolve_wsl_exe() -> str | None: """Return a usable ``wsl.exe`` path. @@ -453,8 +460,8 @@ async def shell( msg = f"timed out after {timeout}s" raise WslError(msg) from None - stdout = stdout_bytes.decode("utf-8", errors="replace").removesuffix("\n") - stderr = stderr_bytes.decode("utf-8", errors="replace").removesuffix("\n") + stdout = _decode_wsl_output(stdout_bytes).removesuffix("\n") + stderr = _decode_wsl_output(stderr_bytes).removesuffix("\n") rc: int = process.returncode if process.returncode is not None else -1 return CLIResult( @@ -530,11 +537,11 @@ async def _run_wsl(self, *cmd: str, timeout: float = 300) -> str: # noqa: ASYNC raise WslError(msg) from None if proc.returncode != 0: - stderr = stderr_bytes.decode("utf-8", errors="replace").strip() + stderr = _decode_wsl_output(stderr_bytes).strip() msg = f"wsl.exe failed (exit {proc.returncode}): {stderr}" raise WslError(msg) - return stdout_bytes.decode("utf-8", errors="replace") + return _decode_wsl_output(stdout_bytes) async def _apply_bind_mounts(self) -> None: """Apply all bind mounts from ``mounts.json`` inside the distro. diff --git a/libs/hexagent/hexagent/harness/environment.py b/libs/hexagent/hexagent/harness/environment.py index 9aa63886..7d6fbc6e 100644 --- a/libs/hexagent/hexagent/harness/environment.py +++ b/libs/hexagent/hexagent/harness/environment.py @@ -6,6 +6,8 @@ from __future__ import annotations +import logging +import shlex from datetime import datetime from typing import TYPE_CHECKING @@ -14,6 +16,8 @@ if TYPE_CHECKING: from hexagent.computer.base import Computer +logger = logging.getLogger(__name__) + class EnvironmentResolver: """Detects runtime environment properties via a Computer. @@ -38,6 +42,48 @@ def __init__(self, computer: Computer) -> None: """ self._computer = computer + async def _probe_datetime(self) -> datetime: + """Best-effort datetime probe that never raises. + + Returns: + Timezone-aware datetime when possible; falls back to UTC now. + """ + # Primary probe: timezone-aware ISO-8601 from shell date. + probe = await self._computer.run("date '+%Y-%m-%dT%H:%M:%S%z'") + raw = (probe.stdout or "").strip() + if raw: + try: + return datetime.strptime(raw, "%Y-%m-%dT%H:%M:%S%z") + except ValueError: + try: + return datetime.strptime(raw[:19], "%Y-%m-%dT%H:%M:%S") # noqa: DTZ007 + except ValueError: + logger.warning("Unparseable environment datetime probe: %r", raw) + + # Secondary probe: Python inside guest (if available). + py_probe = await self._computer.run( + "python3 -c \"from datetime import datetime as d; print(d.now().astimezone().strftime('%Y-%m-%dT%H:%M:%S%z'))\"" + ) + py_raw = (py_probe.stdout or "").strip() + if py_raw: + try: + return datetime.strptime(py_raw, "%Y-%m-%dT%H:%M:%S%z") + except ValueError: + try: + return datetime.strptime(py_raw[:19], "%Y-%m-%dT%H:%M:%S") # noqa: DTZ007 + except ValueError: + logger.warning("Unparseable python datetime probe: %r", py_raw) + + logger.warning( + "Environment datetime probes failed; falling back to UTC now. " + "date.stdout=%r date.stderr=%r python3.stdout=%r python3.stderr=%r", + probe.stdout, + probe.stderr, + py_probe.stdout, + py_probe.stderr, + ) + return datetime.now().astimezone() + async def resolve(self) -> EnvironmentContext: """Detect environment properties from the computer. @@ -46,19 +92,19 @@ async def resolve(self) -> EnvironmentContext: """ # Single batched command: 6 values separated by a unique delimiter. delimiter = "___ENV___" + qd = shlex.quote(delimiter) cmd = ( - f'printf "%s\\n" ' - f'"$(pwd)" ' - f'"{delimiter}" ' - f'"$(git rev-parse --is-inside-work-tree 2>/dev/null || echo false)" ' - f'"{delimiter}" ' - f'"$(uname -s | tr "[:upper:]" "[:lower:]")" ' - f'"{delimiter}" ' - f'"$(basename "$SHELL")" ' - f'"{delimiter}" ' - f'"$(uname -sr)" ' - f'"{delimiter}" ' - f"\"$(date '+%Y-%m-%dT%H:%M:%S%z')\"" + "pwd; " + f"printf '%s\\n' {qd}; " + "(git rev-parse --is-inside-work-tree 2>/dev/null || echo false); " + f"printf '%s\\n' {qd}; " + "uname -s | tr '[:upper:]' '[:lower:]'; " + f"printf '%s\\n' {qd}; " + "basename \"${SHELL:-bash}\"; " + f"printf '%s\\n' {qd}; " + "uname -sr; " + f"printf '%s\\n' {qd}; " + "date '+%Y-%m-%dT%H:%M:%S%z'" ) result = await self._computer.run(cmd) parts = result.stdout.strip().split(delimiter) @@ -69,16 +115,22 @@ async def resolve(self) -> EnvironmentContext: while len(values) < _EXPECTED_PARTS: values.append("") - # Parse into a timezone-aware datetime. - # Shell outputs ISO 8601 with numeric offset, e.g. "2026-02-14T10:30:00-0800". + # Parse into a datetime. Shell usually outputs timezone-aware ISO 8601, + # e.g. "2026-02-14T10:30:00-0800". If missing, probe separately. raw_dt = values[5] - if not raw_dt: - msg = f"Environment shell returned empty datetime (raw output: {result.stdout!r})" - raise ValueError(msg) - try: - now = datetime.strptime(raw_dt, "%Y-%m-%dT%H:%M:%S%z") - except ValueError: - now = datetime.strptime(raw_dt[:19], "%Y-%m-%dT%H:%M:%S") # noqa: DTZ007 + if raw_dt: + try: + now = datetime.strptime(raw_dt, "%Y-%m-%dT%H:%M:%S%z") + except ValueError: + now = datetime.strptime(raw_dt[:19], "%Y-%m-%dT%H:%M:%S") # noqa: DTZ007 + else: + logger.warning( + "Environment shell returned empty datetime; falling back probe. stdout=%r stderr=%r exit=%s", + result.stdout, + result.stderr, + result.exit_code, + ) + now = await self._probe_datetime() return EnvironmentContext( working_dir=values[0], diff --git a/libs/hexagent/hexagent/tools/ui/present_to_user.py b/libs/hexagent/hexagent/tools/ui/present_to_user.py index b9b5df39..7efeb826 100644 --- a/libs/hexagent/hexagent/tools/ui/present_to_user.py +++ b/libs/hexagent/hexagent/tools/ui/present_to_user.py @@ -184,6 +184,11 @@ def _build_case_block() -> str: done """.format(case_arms=_build_case_block()) # noqa: UP032 — can't use f-string; bash ${} conflicts +# WSL/bash is sensitive to CRLF in inline scripts (can break function +# definitions/quoting with opaque parse errors). Normalize to LF at runtime so +# behavior is stable regardless of host checkout EOL settings. +_SCRIPT_BODY_LF = _SCRIPT_BODY.replace("\r\n", "\n").replace("\r", "\n") + def _build_command(filepaths: list[str], output_dir: str) -> str: """Build a bash command that processes all file paths. @@ -200,7 +205,7 @@ def _build_command(filepaths: list[str], output_dir: str) -> str: A shell command string safe for ``Computer.run()``. """ quoted_args = " ".join(shlex.quote(p) for p in [output_dir, *filepaths]) - return f"bash -c {shlex.quote(_SCRIPT_BODY)} _ {quoted_args}" + return f"bash -c {shlex.quote(_SCRIPT_BODY_LF)} _ {quoted_args}" class PresentToUserTool(BaseAgentTool[PresentToUserToolParams]): diff --git a/libs/hexagent/tests/unit_tests/harness/test_environment.py b/libs/hexagent/tests/unit_tests/harness/test_environment.py index 9681f0a6..e41f9b32 100644 --- a/libs/hexagent/tests/unit_tests/harness/test_environment.py +++ b/libs/hexagent/tests/unit_tests/harness/test_environment.py @@ -1,4 +1,4 @@ -# ruff: noqa: PLR2004 +# ruff: noqa: PLR2004 """Tests for EnvironmentResolver.""" from __future__ import annotations @@ -6,8 +6,6 @@ from datetime import UTC, datetime from unittest.mock import AsyncMock -import pytest - from hexagent.harness.environment import EnvironmentResolver from hexagent.types import CLIResult @@ -36,6 +34,12 @@ def _mock_computer(stdout: str) -> AsyncMock: return computer +def _mock_computer_sequence(results: list[CLIResult]) -> AsyncMock: + computer = AsyncMock() + computer.run = AsyncMock(side_effect=results) + return computer + + # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- @@ -83,20 +87,47 @@ async def test_datetime_without_timezone_fallback(self) -> None: assert env.today_date.year == 2026 assert env.today_date.tzinfo is None - async def test_empty_datetime_raises(self) -> None: - """Empty datetime must raise — it indicates a broken shell probe.""" - computer = _mock_computer(_make_stdout(date="")) - with pytest.raises(ValueError, match="empty datetime"): - await EnvironmentResolver(computer).resolve() + async def test_empty_datetime_falls_back_to_secondary_probe(self) -> None: + """Empty datetime in batched output falls back to a secondary date probe.""" + computer = _mock_computer_sequence( + [ + CLIResult(stdout=_make_stdout(date=""), stderr="", exit_code=0), + CLIResult(stdout="2026-03-13T10:30:00+0000", stderr="", exit_code=0), + ] + ) + env = await EnvironmentResolver(computer).resolve() + + assert env.today_date.tzinfo is not None + assert env.today_date == datetime(2026, 3, 13, 10, 30, 0, tzinfo=UTC) - async def test_pads_missing_parts_raises(self) -> None: - """When stdout has fewer delimiters, missing date field raises.""" - # Only cwd and git — missing platform, shell, os_version, date + async def test_pads_missing_parts_falls_back_to_secondary_probe(self) -> None: + """When stdout has fewer delimiters, resolver still recovers via date probe.""" + # Only cwd and git; missing platform, shell, os_version, date stdout = f"/home/user\n{_DELIM}\ntrue" - computer = _mock_computer(stdout) + computer = _mock_computer_sequence( + [ + CLIResult(stdout=stdout, stderr="", exit_code=0), + CLIResult(stdout="2026-03-13T10:30:00+0000", stderr="", exit_code=0), + ] + ) + env = await EnvironmentResolver(computer).resolve() + + assert env.today_date.tzinfo is not None + assert env.today_date.year == 2026 + + async def test_all_datetime_probes_fail_uses_local_time(self) -> None: + """If both date probes fail, resolver should still return a usable context.""" + computer = _mock_computer_sequence( + [ + CLIResult(stdout=_make_stdout(date=""), stderr="", exit_code=0), + CLIResult(stdout="", stderr="date: command not found", exit_code=127), + CLIResult(stdout="", stderr="python3: command not found", exit_code=127), + ] + ) + env = await EnvironmentResolver(computer).resolve() - with pytest.raises(ValueError, match="empty datetime"): - await EnvironmentResolver(computer).resolve() + assert env.today_date.tzinfo is not None + assert env.today_date.year >= 2020 async def test_darwin_platform(self) -> None: computer = _mock_computer(_make_stdout(platform="darwin", os_version="Darwin 25.3.0")) diff --git a/libs/hexagent/tests/unit_tests/tools/ui/test_present_to_user.py b/libs/hexagent/tests/unit_tests/tools/ui/test_present_to_user.py index b8f21263..3e68db1f 100644 --- a/libs/hexagent/tests/unit_tests/tools/ui/test_present_to_user.py +++ b/libs/hexagent/tests/unit_tests/tools/ui/test_present_to_user.py @@ -160,6 +160,11 @@ def test_quotes_special_characters(self) -> None: cmd = _build_command(["/path/with spaces/file.txt"], "/out") assert "'/path/with spaces/file.txt'" in cmd + def test_embedded_script_normalized_to_lf(self) -> None: + """Command string should not carry CR characters into bash -c payload.""" + cmd = _build_command(["/a.txt"], "/out") + assert "\r" not in cmd + # --------------------------------------------------------------------------- # _EXT_MIME_MAP / generated script tests From 32b2fd05754f9bc3ef879a4cad646ceac1af45c5 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Sat, 28 Mar 2026 07:41:52 +0800 Subject: [PATCH 22/34] feat(setup): improve WSL instance/provision reliability on Windows --- .../backend/hexagent_api/routes/setup.py | 221 +++++++++++++----- libs/hexagent_demo/frontend/src/App.tsx | 2 +- libs/hexagent_demo/frontend/src/vmSetup.tsx | 13 ++ 3 files changed, 175 insertions(+), 61 deletions(-) diff --git a/libs/hexagent_demo/backend/hexagent_api/routes/setup.py b/libs/hexagent_demo/backend/hexagent_api/routes/setup.py index 61d9a89d..4a2137d0 100644 --- a/libs/hexagent_demo/backend/hexagent_api/routes/setup.py +++ b/libs/hexagent_demo/backend/hexagent_api/routes/setup.py @@ -184,6 +184,7 @@ def _lima_status() -> dict[str, object]: _WSL_PREBUILT_CANDIDATES = ( "hexagent-prebuilt.tar", "hexagent.tar", + "ubuntu-base-24.04-amd64.tar.gz", ) @@ -222,6 +223,18 @@ def _combine_wsl_output(stdout_b: bytes | None, stderr_b: bytes | None) -> str: return err or out +def _looks_like_wsl_usage(msg: str) -> bool: + """Return True when output is the generic WSL usage/help banner.""" + text = msg.strip() + low = text.lower() + return ( + "usage: wsl" in low + or "usage: wsl.exe" in low + or "用法: wsl" in text + or "用法: wsl.exe" in text + ) + + def _looks_like_missing_wsl_disk(msg: str) -> bool: text = msg.lower() return ( @@ -333,12 +346,23 @@ async def _wsl_instance_status() -> str | None: def _wsl_prebuilt_tar_path() -> Path | None: - """Return bundled prebuilt WSL rootfs tar if present.""" - prebuilt_dir = vm_setup_dir().parent / "wsl" / "prebuilt" - for name in _WSL_PREBUILT_CANDIDATES: - candidate = prebuilt_dir / name - if candidate.is_file(): - return candidate + """Return an offline WSL rootfs archive if present. + + Search order: + 1. Backend-bundled VM assets (PyInstaller ``sandbox/vm/wsl/prebuilt``) + 2. Electron extraResources path from ``HEXAGENT_WSL_OFFLINE_DIR`` (if set) + """ + candidate_dirs: list[Path] = [vm_setup_dir().parent / "wsl" / "prebuilt"] + + offline_dir = os.environ.get("HEXAGENT_WSL_OFFLINE_DIR", "").strip() + if offline_dir: + candidate_dirs.append(Path(offline_dir)) + + for prebuilt_dir in candidate_dirs: + for name in _WSL_PREBUILT_CANDIDATES: + candidate = prebuilt_dir / name + if candidate.is_file(): + return candidate return None @@ -469,7 +493,9 @@ def _wsl_status() -> dict[str, object]: "installed": False, "path": wsl, "managed": False, - "reason": last_err or "WSL runtime is not available", + # Some Windows builds print only usage text for unsupported probes. + # Treat that as "not installed yet" (pending) instead of hard error. + **({} if _looks_like_wsl_usage(last_err) else {"reason": last_err or "WSL runtime is not available"}), } @@ -664,6 +690,22 @@ def _vm_status() -> dict[str, object]: return {"supported": False, "backend": None, "installed": False, "reason": f"No VM backend for {sys.platform}"} +def _runtime_vm_backend() -> str: + """Resolve the active VM backend for branch dispatch. + + Prefer the backend reported by ``_vm_status()`` so behavior stays aligned + with the setup API surface. Fall back to platform defaults defensively. + """ + backend = str(_vm_status().get("backend") or "") + if backend in {"wsl", "lima"}: + return backend + if sys.platform == "win32": + return "wsl" + if sys.platform == "darwin": + return "lima" + return "" + + # --------------------------------------------------------------------------- # Endpoints — generic /vm, frontend doesn't need to know Lima vs WSL # --------------------------------------------------------------------------- @@ -947,10 +989,59 @@ async def _communicate_with_heartbeat( self._emit("progress", {"step": step, "message": f"{message} (elapsed {elapsed}s){extra}"}) async def _run(self, **kwargs: object) -> None: - if sys.platform == "win32": + backend = _runtime_vm_backend() + if backend == "wsl": await self._run_wsl() return - await self._run_lima() + if backend == "lima": + await self._run_lima() + return + self._emit("error", {"message": f"VM build is not supported on backend: {backend or sys.platform}"}) + self._status = "error" + self._error = "Unsupported backend" + + async def _start_wsl_instance( + self, + wsl_exe: str, + *, + step: str, + message: str, + retries_on_missing_disk: int = 0, + ) -> tuple[bool, str]: + """Start hexagent distro and optionally retry transient missing-disk errors.""" + attempts = max(1, retries_on_missing_disk + 1) + for attempt in range(1, attempts + 1): + proc = await asyncio.create_subprocess_exec( + wsl_exe, "-d", _WSL_INSTANCE, "--", "echo", "ok", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + self._process = proc + out_b, err_b = await self._communicate_with_heartbeat( + proc, + step=step, + message=message, + ) + if proc.returncode == 0: + return True, "" + + err = _combine_wsl_output(out_b, err_b) + is_missing_disk = _looks_like_missing_wsl_disk(err) + if is_missing_disk and attempt < attempts: + wait_s = min(2 * attempt, 5) + self._emit( + "progress", + { + "step": step, + "message": f"WSL disk not ready yet, retrying start in {wait_s}s " + f"({attempt}/{attempts - 1})...", + }, + ) + await asyncio.sleep(wait_s) + continue + + return False, err or f"WSL start failed (exit {proc.returncode})" + return False, "WSL start failed" async def _run_lima(self) -> None: # Ensure limactl has the virtualization entitlement before any VM @@ -1033,23 +1124,17 @@ async def _run_wsl(self) -> None: if _wsl_state_equals(status, _WSL_STOPPED_STATES): self._emit("progress", {"step": "starting", "message": "Starting existing WSL distro..."}) - proc = await asyncio.create_subprocess_exec( - wsl_exe, "-d", _WSL_INSTANCE, "--", "echo", "ok", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - self._process = proc - stdout_b, stderr_b = await self._communicate_with_heartbeat( - proc, + ok, err = await self._start_wsl_instance( + wsl_exe, step="starting", message="Starting existing WSL distro...", + retries_on_missing_disk=1, ) - if proc.returncode == 0: + if ok: self._emit("done", {"message": "WSL distro started successfully"}) self._status = "done" return else: - err = _combine_wsl_output(stdout_b, stderr_b) if _looks_like_missing_wsl_disk(err): self._emit("progress", {"step": "creating", "message": "Detected broken WSL distro disk. Recreating HexAgent distro..."}) proc_unreg = await asyncio.create_subprocess_exec( @@ -1106,25 +1191,19 @@ async def _run_wsl(self) -> None: return self._emit("progress", {"step": "starting", "message": "Starting imported HexAgent WSL distro..."}) - proc_start = await asyncio.create_subprocess_exec( - wsl_exe, "-d", _WSL_INSTANCE, "--", "echo", "ok", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - self._process = proc_start - out_b, err_b = await self._communicate_with_heartbeat( - proc_start, + ok, err = await self._start_wsl_instance( + wsl_exe, step="starting", message="Starting imported HexAgent WSL distro...", + retries_on_missing_disk=3, ) - if proc_start.returncode == 0: + if ok: self._emit("done", {"message": "WSL distro imported from bundled image and started successfully"}) self._status = "done" else: - err = _combine_wsl_output(out_b, err_b) - self._emit("error", {"message": err or f"WSL start failed (exit {proc_start.returncode})"}) + self._emit("error", {"message": err}) self._status = "error" - self._error = f"exit {proc_start.returncode}" + self._error = err return # Fallback: bootstrap from Ubuntu export. @@ -1209,25 +1288,19 @@ async def _run_wsl(self) -> None: return self._emit("progress", {"step": "starting", "message": "Starting HexAgent WSL distro..."}) - proc_start = await asyncio.create_subprocess_exec( - wsl_exe, "-d", _WSL_INSTANCE, "--", "echo", "ok", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - self._process = proc_start - out_b, err_b = await self._communicate_with_heartbeat( - proc_start, + ok, err = await self._start_wsl_instance( + wsl_exe, step="starting", message="Starting HexAgent WSL distro...", + retries_on_missing_disk=3, ) - if proc_start.returncode == 0: + if ok: self._emit("done", {"message": "WSL distro created and started successfully"}) self._status = "done" else: - err = _combine_wsl_output(out_b, err_b) - self._emit("error", {"message": err or f"WSL start failed (exit {proc_start.returncode})"}) + self._emit("error", {"message": err}) self._status = "error" - self._error = f"exit {proc_start.returncode}" + self._error = err async def _stream_stderr(self, proc: asyncio.subprocess.Process) -> str: """Read limactl stderr line-by-line and emit progress events. @@ -1260,8 +1333,8 @@ async def _stream_stderr(self, proc: asyncio.subprocess.Process) -> str: # Provision Manager — runs setup.sh inside the VM # --------------------------------------------------------------------------- -_SETUP_MARKER_DIR = "/var/lib/hexagent/setup" -_SETUP_LOG_DIR = "/var/log/hexagent/setup" +_SETUP_MARKER_DIRS = ("/var/lib/hexagent/setup", "/var/lib/openagent/setup") +_SETUP_LOG_DIRS = ("/var/log/hexagent/setup", "/var/log/openagent/setup") _SETUP_VM_DIR = "/tmp/hexagent-setup" # Step IDs that setup.sh discovers (must match filenames in steps/) @@ -1281,10 +1354,16 @@ class _ProvisionManager(_ProcessManager): """Manages setup.sh execution inside the Lima VM.""" async def _run(self, **kwargs: object) -> None: - if sys.platform == "win32": + backend = _runtime_vm_backend() + if backend == "wsl": await self._run_wsl(**kwargs) return - await self._run_lima(**kwargs) + if backend == "lima": + await self._run_lima(**kwargs) + return + self._emit("error", {"message": f"VM provisioning is not supported on backend: {backend or sys.platform}"}) + self._status = "error" + self._error = "Unsupported backend" async def _run_lima(self, **kwargs: object) -> None: force = bool(kwargs.get("force", False)) @@ -1418,7 +1497,9 @@ async def _run_wsl(self, **kwargs: object) -> None: setup_vm_dir_quoted = shlex.quote(_SETUP_VM_DIR) rc, _, err = await _wsl_shell( f"rm -rf {setup_vm_dir_quoted} && mkdir -p {setup_vm_dir_quoted} && " - f"cp -r {setup_wsl_quoted}/. {setup_vm_dir_quoted}/", + f"cp -r {setup_wsl_quoted}/. {setup_vm_dir_quoted}/ && " + f"find {setup_vm_dir_quoted} -type f -name '*.sh' -exec sed -i 's/\\r$//' {{}} + && " + f"find {setup_vm_dir_quoted} -type f -name '*.sh' -exec chmod +x {{}} +", timeout=60, user="root", ) @@ -1488,19 +1569,27 @@ def _handle_setup_line(self, line: str) -> None: async def check_markers(self) -> dict[str, object]: """Read VM-side marker files to determine provision state.""" - if sys.platform == "win32": + backend = _runtime_vm_backend() + if backend == "wsl": instance_status = await _wsl_instance_status() shell = lambda cmd: _wsl_shell(cmd, user="root") if not _wsl_distro_ready_for_cowork(instance_status): return {"provisioned": False, "steps_done": [], "total_steps": len(_PROVISION_STEPS)} - else: + elif backend == "lima": instance_status = await _lima_instance_status() shell = _lima_shell if instance_status != "Running": return {"provisioned": False, "steps_done": [], "total_steps": len(_PROVISION_STEPS)} + else: + return {"provisioned": False, "steps_done": [], "total_steps": len(_PROVISION_STEPS)} - rc, stdout, _ = await shell(f"ls {_SETUP_MARKER_DIR}/*.done 2>/dev/null || true") - if rc != 0 or not stdout.strip(): + stdout = "" + for marker_dir in _SETUP_MARKER_DIRS: + rc, out, _ = await shell(f"ls {marker_dir}/*.done 2>/dev/null || true") + if rc == 0 and out.strip(): + stdout = out + break + if not stdout.strip(): return {"provisioned": False, "steps_done": [], "total_steps": len(_PROVISION_STEPS)} done_files = stdout.strip().splitlines() @@ -1518,12 +1607,21 @@ async def check_markers(self) -> dict[str, object]: async def get_log(self) -> str: """Fetch the latest setup log from the VM.""" - shell = (lambda cmd, timeout=15: _wsl_shell(cmd, timeout=timeout, user="root")) if sys.platform == "win32" else _lima_shell - rc, stdout, _ = await shell( - f"ls -t {_SETUP_LOG_DIR}/setup-*.log 2>/dev/null | head -1 | xargs cat 2>/dev/null | tail -500", - timeout=15, - ) - return stdout if rc == 0 else "" + backend = _runtime_vm_backend() + if backend == "wsl": + shell = lambda cmd, timeout=15: _wsl_shell(cmd, timeout=timeout, user="root") + elif backend == "lima": + shell = _lima_shell + else: + return "" + for log_dir in _SETUP_LOG_DIRS: + rc, stdout, _ = await shell( + f"ls -t {log_dir}/setup-*.log 2>/dev/null | head -1 | xargs cat 2>/dev/null | tail -500", + timeout=15, + ) + if rc == 0 and stdout.strip(): + return stdout + return "" # --------------------------------------------------------------------------- @@ -1573,10 +1671,13 @@ async def get_build_status() -> dict[str, object]: mgr = _get_build_manager() result = dict(mgr.status_dict()) if mgr._status in ("idle", "done", "error"): - if sys.platform == "win32": + backend = _runtime_vm_backend() + if backend == "wsl": result["vm_state"] = await _wsl_instance_status() - else: + elif backend == "lima": result["vm_state"] = await _lima_instance_status() + else: + result["vm_state"] = None return result diff --git a/libs/hexagent_demo/frontend/src/App.tsx b/libs/hexagent_demo/frontend/src/App.tsx index e3c7d180..dd9bcb0d 100644 --- a/libs/hexagent_demo/frontend/src/App.tsx +++ b/libs/hexagent_demo/frontend/src/App.tsx @@ -307,7 +307,7 @@ function App() { abortMapRef.current.set(conversationId, controller); }, - [dispatch, state.conversations, state.selectedModelId, state.isStreaming] + [dispatch, state.conversations, state.selectedModelId] ); const handleNewConversation = useCallback(() => { diff --git a/libs/hexagent_demo/frontend/src/vmSetup.tsx b/libs/hexagent_demo/frontend/src/vmSetup.tsx index ea3797c6..d3a22479 100644 --- a/libs/hexagent_demo/frontend/src/vmSetup.tsx +++ b/libs/hexagent_demo/frontend/src/vmSetup.tsx @@ -109,6 +109,7 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { const [provStepMsg, setProvStepMsg] = useState>({}); const [provLog, setProvLog] = useState(null); const autoBootstrapTriggeredRef = useRef(false); + const autoProvisionTriggeredRef = useRef(false); const [autoBootstrapping, setAutoBootstrapping] = useState(false); // SSE abort controllers (kept alive across renders, never aborted on unmount) @@ -550,6 +551,18 @@ export function VMSetupProvider({ children }: { children: ReactNode }) { } }, [autoBootstrapping, phase1, phase2]); + // Windows-first run: once VM instance is ready, auto start dependency provision + // in background so users don't need to click "Install in background" manually. + useEffect(() => { + if (!IS_WINDOWS) return; + if (autoProvisionTriggeredRef.current) return; + if (phase1 !== "done" || phase2 !== "done") return; + if (phase3 !== "pending") return; + autoProvisionTriggeredRef.current = true; + notify("VM instance is ready. Starting system dependency installation in background...", "info"); + doStartProvision(false); + }, [phase1, phase2, phase3]); // eslint-disable-line react-hooks/exhaustive-deps + const value: VMSetupContextValue = { vmStatus, autoBootstrapping, From 34d8d4417b29aee668bfb89fb0978da5c538f5e0 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Sat, 28 Mar 2026 07:42:16 +0800 Subject: [PATCH 23/34] feat(electron): bundle offline WSL assets and build pipeline --- .gitattributes | 7 + libs/hexagent_demo/electron/main.js | 4 + libs/hexagent_demo/electron/package.json | 6 + .../electron/resources/wsl/.gitkeep | 1 + .../wsl/ubuntu-base-24.04-amd64.tar.gz | 3 + .../resources/wsl/wsl.2.6.3.0.x64.msi | 3 + .../electron/scripts/build-all.ps1 | 19 ++- .../scripts/prepare-wsl-offline-assets.ps1 | 127 ++++++++++++++++++ .../electron/wsl.2.6.3.0.x64.msi | 3 + 9 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 libs/hexagent_demo/electron/resources/wsl/.gitkeep create mode 100644 libs/hexagent_demo/electron/resources/wsl/ubuntu-base-24.04-amd64.tar.gz create mode 100644 libs/hexagent_demo/electron/resources/wsl/wsl.2.6.3.0.x64.msi create mode 100644 libs/hexagent_demo/electron/scripts/prepare-wsl-offline-assets.ps1 create mode 100644 libs/hexagent_demo/electron/wsl.2.6.3.0.x64.msi diff --git a/.gitattributes b/.gitattributes index c3482579..245dd5cd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,4 +2,11 @@ libs/openagent/sandbox/vm/setup/*.sh text eol=lf libs/openagent/sandbox/vm/setup/steps/*.sh text eol=lf libs/hexagent/sandbox/vm/setup/*.sh text eol=lf libs/hexagent/sandbox/vm/setup/steps/*.sh text eol=lf +libs/openagent/sandbox/vm/setup_lite/*.sh text eol=lf +libs/openagent/sandbox/vm/setup_lite/steps/*.sh text eol=lf +libs/hexagent/sandbox/vm/setup_lite/*.sh text eol=lf +libs/hexagent/sandbox/vm/setup_lite/steps/*.sh text eol=lf libs/openagent/sandbox/vm/wsl/prebuilt/openagent-prebuilt.tar filter=lfs diff=lfs merge=lfs -text +libs/hexagent_demo/electron/resources/wsl/*.msi filter=lfs diff=lfs merge=lfs -text +libs/hexagent_demo/electron/*.msi filter=lfs diff=lfs merge=lfs -text +libs/hexagent_demo/electron/resources/wsl/ubuntu-base-24.04-amd64.tar.gz filter=lfs diff=lfs merge=lfs -text diff --git a/libs/hexagent_demo/electron/main.js b/libs/hexagent_demo/electron/main.js index b65a9061..b49a2638 100644 --- a/libs/hexagent_demo/electron/main.js +++ b/libs/hexagent_demo/electron/main.js @@ -107,6 +107,9 @@ function waitForHealth(port, retries = 30, interval = 500) { async function spawnBackend() { const port = IS_DEV ? 8000 : await findFreePort(); backendPort = port; + const wslOfflineDir = IS_DEV + ? path.join(__dirname, "resources", "wsl") + : path.join(process.resourcesPath, "wsl"); if (IS_DEV) { const backendDir = path.join(__dirname, "..", "backend"); @@ -167,6 +170,7 @@ async function spawnBackend() { HOST: "127.0.0.1", PORT: String(port), HEXAGENT_DATA_DIR: userDataDir, + HEXAGENT_WSL_OFFLINE_DIR: wslOfflineDir, }, }); } diff --git a/libs/hexagent_demo/electron/package.json b/libs/hexagent_demo/electron/package.json index 75fd69b7..2cd16416 100644 --- a/libs/hexagent_demo/electron/package.json +++ b/libs/hexagent_demo/electron/package.json @@ -64,6 +64,12 @@ }, "afterPack": "./scripts/afterPack.js", "win": { + "extraResources": [ + { + "from": "resources/wsl/", + "to": "wsl" + } + ], "target": [ { "target": "nsis", diff --git a/libs/hexagent_demo/electron/resources/wsl/.gitkeep b/libs/hexagent_demo/electron/resources/wsl/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/libs/hexagent_demo/electron/resources/wsl/.gitkeep @@ -0,0 +1 @@ + diff --git a/libs/hexagent_demo/electron/resources/wsl/ubuntu-base-24.04-amd64.tar.gz b/libs/hexagent_demo/electron/resources/wsl/ubuntu-base-24.04-amd64.tar.gz new file mode 100644 index 00000000..24e69628 --- /dev/null +++ b/libs/hexagent_demo/electron/resources/wsl/ubuntu-base-24.04-amd64.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1e67ef7b17a6300e136118bd1dc04725009cb376c1aad10abcf8cd453628d58 +size 29989394 diff --git a/libs/hexagent_demo/electron/resources/wsl/wsl.2.6.3.0.x64.msi b/libs/hexagent_demo/electron/resources/wsl/wsl.2.6.3.0.x64.msi new file mode 100644 index 00000000..3a090704 --- /dev/null +++ b/libs/hexagent_demo/electron/resources/wsl/wsl.2.6.3.0.x64.msi @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:562c79aba6ce9b6e9170f069d31e3717f10d76dd8bfbee39b07eae0ca4a02ca0 +size 247123968 diff --git a/libs/hexagent_demo/electron/scripts/build-all.ps1 b/libs/hexagent_demo/electron/scripts/build-all.ps1 index d26a5905..3c3f9e01 100644 --- a/libs/hexagent_demo/electron/scripts/build-all.ps1 +++ b/libs/hexagent_demo/electron/scripts/build-all.ps1 @@ -3,7 +3,8 @@ $ErrorActionPreference = 'Stop' $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $ElectronDir = Resolve-Path "$ScriptDir\.." $Target = if ($args.Count -gt 0) { $args[0] } else { 'win' } -$EmbedWslPrebuilt = ($env:OPENAGENT_EMBED_WSL_PREBUILT -eq "1") +$EmbedWslPrebuilt = ($env:HEXAGENT_EMBED_WSL_PREBUILT -eq "1" -or $env:OPENAGENT_EMBED_WSL_PREBUILT -eq "1") +$PrepareOfflineWsl = ($env:HEXAGENT_PREPARE_OFFLINE_WSL -ne "0") Write-Host '=========================================' Write-Host ' HexAgent Desktop - Build ('$Target')' @@ -25,10 +26,18 @@ Write-Host '' Write-Host '[2/3] Skipping electron dependencies (already installed)...' Set-Location $ElectronDir -if ($Target -eq 'win' -and $EmbedWslPrebuilt) { - Write-Host '' - Write-Host '[2.2/3] Exporting prebuilt WSL VM image for offline-ready package...' - & "$ScriptDir\prepare-wsl-prebuilt.ps1" +if ($Target -eq 'win') { + if ($PrepareOfflineWsl) { + Write-Host '' + Write-Host '[2.1/3] Preparing offline WSL installer assets...' + & "$ScriptDir\prepare-wsl-offline-assets.ps1" + } + + if ($EmbedWslPrebuilt) { + Write-Host '' + Write-Host '[2.2/3] Exporting prebuilt WSL VM image for offline-ready package...' + & "$ScriptDir\prepare-wsl-prebuilt.ps1" + } } Write-Host '' diff --git a/libs/hexagent_demo/electron/scripts/prepare-wsl-offline-assets.ps1 b/libs/hexagent_demo/electron/scripts/prepare-wsl-offline-assets.ps1 new file mode 100644 index 00000000..1346a101 --- /dev/null +++ b/libs/hexagent_demo/electron/scripts/prepare-wsl-offline-assets.ps1 @@ -0,0 +1,127 @@ +$ErrorActionPreference = "Stop" + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$ElectronDir = Resolve-Path "$ScriptDir\.." +$OfflineDir = Join-Path $ElectronDir "resources\wsl" + +$WslMsiName = "wsl.2.6.3.0.x64.msi" +$UbuntuRootfsName = "ubuntu-base-24.04-amd64.tar.gz" +$UseCnMirrors = ($env:HEXAGENT_USE_CN_MIRRORS -ne "0") + +if ($env:OS -ne "Windows_NT") { + Write-Host "Skipping offline WSL asset preparation: non-Windows environment." + exit 0 +} + +New-Item -ItemType Directory -Force -Path $OfflineDir | Out-Null + +function Ensure-DownloadedFile { + param( + [Parameter(Mandatory = $true)][string]$Name, + [Parameter(Mandatory = $true)][string[]]$Urls, + [long]$MinBytes = 1024, + [long]$MaxBytes = 0, + [string]$Kind = "generic" + ) + + $target = Join-Path $OfflineDir $Name + function Test-AssetValidity { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string]$AssetKind, + [long]$AssetMinBytes = 1024, + [long]$AssetMaxBytes = 0 + ) + if (-not (Test-Path $Path)) { return $false } + $item = Get-Item $Path -ErrorAction SilentlyContinue + if (-not $item) { return $false } + if ($item.Length -lt $AssetMinBytes) { return $false } + if ($AssetMaxBytes -gt 0 -and $item.Length -gt $AssetMaxBytes) { return $false } + + try { + $fs = [System.IO.File]::OpenRead($Path) + try { + $header = New-Object byte[] 4 + [void]$fs.Read($header, 0, 4) + } finally { + $fs.Dispose() + } + + if ($AssetKind -eq "msi") { + # MSI is a CFB container: D0 CF 11 E0 + return ($header[0] -eq 0xD0 -and $header[1] -eq 0xCF -and $header[2] -eq 0x11 -and $header[3] -eq 0xE0) + } + if ($AssetKind -eq "tar_gz") { + # Gzip magic: 1F 8B + return ($header[0] -eq 0x1F -and $header[1] -eq 0x8B) + } + return $true + } catch { + return $false + } + } + + if (Test-Path $target) { + if (Test-AssetValidity -Path $target -AssetKind $Kind -AssetMinBytes $MinBytes -AssetMaxBytes $MaxBytes) { + $sizeMb = [math]::Round(((Get-Item $target).Length / 1MB), 1) + Write-Host "==> Reusing cached offline asset: $Name (${sizeMb} MB)" + return + } + Write-Host "==> Cached file is invalid, redownloading: $Name" + Remove-Item -Force $target + } + + $lastError = $null + foreach ($url in $Urls) { + if (-not $url) { continue } + Write-Host "==> Downloading $Name from $url ..." + try { + Invoke-WebRequest -Uri $url -OutFile $target + if (Test-AssetValidity -Path $target -AssetKind $Kind -AssetMinBytes $MinBytes -AssetMaxBytes $MaxBytes) { + $sizeMb = [math]::Round(((Get-Item $target).Length / 1MB), 1) + Write-Host "==> Ready: $Name (${sizeMb} MB)" + return + } + Write-Host "==> Downloaded file failed validation, trying next mirror..." + if (Test-Path $target) { Remove-Item -Force $target } + } catch { + $lastError = $_ + Write-Host "==> Download failed from $url, trying next mirror..." + if (Test-Path $target) { Remove-Item -Force $target } + } + } + + if (-not (Test-Path $target)) { + if ($lastError) { + throw $lastError + } + throw "All download URLs failed for $Name" + } +} + +$wslMsiUrls = @() +$rootfsUrls = @() + +if ($env:HEXAGENT_WSL_MSI_URL) { + $wslMsiUrls += $env:HEXAGENT_WSL_MSI_URL +} +if ($env:HEXAGENT_UBUNTU_ROOTFS_URL) { + $rootfsUrls += $env:HEXAGENT_UBUNTU_ROOTFS_URL +} + +if ($UseCnMirrors) { + # Optional acceleration mirror for GitHub download. + $wslMsiUrls += "https://gh.llkk.cc/https://github.com/microsoft/WSL/releases/download/2.6.3/$WslMsiName" + # Smaller Ubuntu base rootfs (~28MB) for offline package size control. + $rootfsUrls += "https://mirrors.ustc.edu.cn/ubuntu-cdimage/ubuntu-base/releases/24.04/release/ubuntu-base-24.04.4-base-amd64.tar.gz" + $rootfsUrls += "https://mirror.sjtu.edu.cn/ubuntu-cdimage/ubuntu-base/releases/24.04/release/ubuntu-base-24.04.4-base-amd64.tar.gz" +} + +# Official fallback URLs. +$wslMsiUrls += "https://github.com/microsoft/WSL/releases/download/2.6.3/$WslMsiName" +$rootfsUrls += "https://cdimage.ubuntu.com/ubuntu-base/releases/24.04/release/ubuntu-base-24.04.4-base-amd64.tar.gz" + +Ensure-DownloadedFile -Name $WslMsiName -Urls $wslMsiUrls -Kind "msi" -MinBytes 10485760 +Ensure-DownloadedFile -Name $UbuntuRootfsName -Urls $rootfsUrls -Kind "tar_gz" -MinBytes 20971520 -MaxBytes 83886080 + +Write-Host "==> Offline WSL assets are ready in: $OfflineDir" diff --git a/libs/hexagent_demo/electron/wsl.2.6.3.0.x64.msi b/libs/hexagent_demo/electron/wsl.2.6.3.0.x64.msi new file mode 100644 index 00000000..3a090704 --- /dev/null +++ b/libs/hexagent_demo/electron/wsl.2.6.3.0.x64.msi @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:562c79aba6ce9b6e9170f069d31e3717f10d76dd8bfbee39b07eae0ca4a02ca0 +size 247123968 From d4ede0c024d71460c1435b51b0497f9c12e03aa0 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Sat, 28 Mar 2026 08:04:30 +0800 Subject: [PATCH 24/34] fix(build): reuse hexagent-prebuilt tar during offline packaging --- .../electron/scripts/prepare-wsl-prebuilt.ps1 | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/libs/hexagent_demo/electron/scripts/prepare-wsl-prebuilt.ps1 b/libs/hexagent_demo/electron/scripts/prepare-wsl-prebuilt.ps1 index 33396e6e..011d7395 100644 --- a/libs/hexagent_demo/electron/scripts/prepare-wsl-prebuilt.ps1 +++ b/libs/hexagent_demo/electron/scripts/prepare-wsl-prebuilt.ps1 @@ -3,14 +3,29 @@ $ErrorActionPreference = "Stop" $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $HexagentRoot = Resolve-Path "$ScriptDir\..\..\.." $PrebuiltDir = Join-Path $HexagentRoot "hexagent\sandbox\vm\wsl\prebuilt" -$PrebuiltTar = Join-Path $PrebuiltDir "openagent-prebuilt.tar" -$DistroName = "openagent" +$PrebuiltTar = Join-Path $PrebuiltDir "hexagent-prebuilt.tar" +$LegacyPrebuiltTar = Join-Path $PrebuiltDir "openagent-prebuilt.tar" +$DistroName = if ($env:HEXAGENT_WSL_DISTRO) { $env:HEXAGENT_WSL_DISTRO } else { "hexagent" } +$ForceRebuild = ($env:HEXAGENT_FORCE_REBUILD_WSL_PREBUILT -eq "1") if ($env:OS -ne "Windows_NT") { Write-Host "Skipping WSL prebuilt export: non-Windows environment." exit 0 } +New-Item -ItemType Directory -Force -Path $PrebuiltDir | Out-Null + +if ((-not (Test-Path $PrebuiltTar)) -and (Test-Path $LegacyPrebuiltTar)) { + Write-Host "==> Found legacy prebuilt tar name, renaming to hexagent-prebuilt.tar ..." + Move-Item -Force $LegacyPrebuiltTar $PrebuiltTar +} + +if ((Test-Path $PrebuiltTar) -and (-not $ForceRebuild)) { + $sizeMb = [math]::Round(((Get-Item $PrebuiltTar).Length / 1MB), 1) + Write-Host "==> Reusing existing WSL prebuilt image: $PrebuiltTar (${sizeMb} MB)" + exit 0 +} + if (-not (Get-Command wsl -ErrorAction SilentlyContinue)) { throw "wsl command not found. Install WSL first." } @@ -18,10 +33,9 @@ if (-not (Get-Command wsl -ErrorAction SilentlyContinue)) { Write-Host "==> Ensuring distro '$DistroName' can start..." & wsl -d $DistroName -- echo ok | Out-Null if ($LASTEXITCODE -ne 0) { - throw "WSL distro '$DistroName' is not available/runnable. Please initialize VM Instance first." + throw "WSL distro '$DistroName' is not available/runnable. Please initialize VM Instance first, or provide an existing hexagent-prebuilt.tar." } -New-Item -ItemType Directory -Force -Path $PrebuiltDir | Out-Null if (Test-Path $PrebuiltTar) { Remove-Item -Force $PrebuiltTar } From a3e0ad5cb9c88dbd05df7a995a8cefb1fe7e2696 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Sat, 28 Mar 2026 08:26:41 +0800 Subject: [PATCH 25/34] chore(branding): rename desktop app to ClawWork and update Windows icon --- libs/hexagent_demo/electron/package.json | 4 ++-- .../hexagent_demo/electron/resources/icon.ico | Bin 36624 -> 23006 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/hexagent_demo/electron/package.json b/libs/hexagent_demo/electron/package.json index 2cd16416..d03b6f53 100644 --- a/libs/hexagent_demo/electron/package.json +++ b/libs/hexagent_demo/electron/package.json @@ -1,7 +1,7 @@ { "name": "hexagent", "version": "0.0.1", - "description": "HexAgent Desktop App", + "description": "ClawWork Desktop App", "main": "main.js", "scripts": { "dev": "ELECTRON_DEV=1 electron .", @@ -22,7 +22,7 @@ }, "build": { "appId": "com.hexagent.app", - "productName": "HexAgent", + "productName": "ClawWork", "directories": { "buildResources": "resources", "output": "dist" diff --git a/libs/hexagent_demo/electron/resources/icon.ico b/libs/hexagent_demo/electron/resources/icon.ico index af153efbeef697b1cbd8a283328a7ca6c970ae6f..0caa30a6251e4898f5ef9d22809d3a50c0b03b81 100644 GIT binary patch literal 23006 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 literal 36624 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

$? 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 From 4c483a59c04ab6e83edf0b4ffab86bc529934a7a Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Sat, 28 Mar 2026 08:49:39 +0800 Subject: [PATCH 26/34] fix(branding): point builder icon path to buildResources and regenerate ICO --- libs/hexagent_demo/electron/package.json | 4 ++-- .../hexagent_demo/electron/resources/icon.ico | Bin 23006 -> 52011 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/hexagent_demo/electron/package.json b/libs/hexagent_demo/electron/package.json index d03b6f53..fb679f00 100644 --- a/libs/hexagent_demo/electron/package.json +++ b/libs/hexagent_demo/electron/package.json @@ -57,7 +57,7 @@ ] } ], - "icon": "resources/icon.icns", + "icon": "icon.icns", "category": "public.app-category.developer-tools", "identity": null, "signIgnore": [] @@ -78,7 +78,7 @@ ] } ], - "icon": "resources/icon.ico", + "icon": "icon.ico", "signAndEditExecutable": false }, "nsis": { diff --git a/libs/hexagent_demo/electron/resources/icon.ico b/libs/hexagent_demo/electron/resources/icon.ico index 0caa30a6251e4898f5ef9d22809d3a50c0b03b81..2b3b0647ed5a4b304ecae53a1895413201d1110a 100644 GIT binary patch literal 52011 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⩔7#$43+`=M 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 From 089bd4ad276c9a360a45b30302d0dd6ec1c1c08d Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Sat, 28 Mar 2026 09:05:22 +0800 Subject: [PATCH 27/34] fix(windows-branding): force ClawWork icon for desktop shortcut and window --- libs/hexagent_demo/electron/main.js | 5 +++++ libs/hexagent_demo/electron/package.json | 4 ++++ libs/hexagent_demo/electron/resources/installer.nsh | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/libs/hexagent_demo/electron/main.js b/libs/hexagent_demo/electron/main.js index dd5191e6..c31b4f6a 100644 --- a/libs/hexagent_demo/electron/main.js +++ b/libs/hexagent_demo/electron/main.js @@ -419,9 +419,14 @@ try { // ── Window ─────────────────────────────────────────────────────────────────── function createWindow() { + const winIconPath = IS_DEV + ? path.join(__dirname, "resources", "icon.ico") + : path.join(process.resourcesPath, "app-icon.ico"); + mainWindow = new BrowserWindow({ width: 1200, height: 800, + icon: fs.existsSync(winIconPath) ? winIconPath : undefined, webPreferences: { preload: path.join(__dirname, "preload.js"), contextIsolation: true, diff --git a/libs/hexagent_demo/electron/package.json b/libs/hexagent_demo/electron/package.json index fb679f00..322157a4 100644 --- a/libs/hexagent_demo/electron/package.json +++ b/libs/hexagent_demo/electron/package.json @@ -68,6 +68,10 @@ { "from": "resources/wsl/", "to": "wsl" + }, + { + "from": "resources/icon.ico", + "to": "app-icon.ico" } ], "target": [ diff --git a/libs/hexagent_demo/electron/resources/installer.nsh b/libs/hexagent_demo/electron/resources/installer.nsh index 7609d096..6a228a6f 100644 --- a/libs/hexagent_demo/electron/resources/installer.nsh +++ b/libs/hexagent_demo/electron/resources/installer.nsh @@ -1,3 +1,9 @@ +!macro customInstall + ; Force desktop shortcut to use bundled ClawWork icon, independent of EXE icon resource. + Delete "$DESKTOP\ClawWork.lnk" + CreateShortCut "$DESKTOP\ClawWork.lnk" "$INSTDIR\ClawWork.exe" "" "$INSTDIR\resources\app-icon.ico" 0 +!macroend + !macro customUnInstall RMDir /r "$PROFILE\.hexagent" !macroend From 9137f499042b62c91b02f332c66e911b308979c3 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Sat, 28 Mar 2026 09:59:54 +0800 Subject: [PATCH 28/34] style(lint): apply ruff formatting for environment probe changes --- libs/hexagent/hexagent/harness/environment.py | 5 ++--- libs/hexagent/tests/unit_tests/harness/test_environment.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/libs/hexagent/hexagent/harness/environment.py b/libs/hexagent/hexagent/harness/environment.py index 7d6fbc6e..b6f6f578 100644 --- a/libs/hexagent/hexagent/harness/environment.py +++ b/libs/hexagent/hexagent/harness/environment.py @@ -75,8 +75,7 @@ async def _probe_datetime(self) -> datetime: logger.warning("Unparseable python datetime probe: %r", py_raw) logger.warning( - "Environment datetime probes failed; falling back to UTC now. " - "date.stdout=%r date.stderr=%r python3.stdout=%r python3.stderr=%r", + "Environment datetime probes failed; falling back to UTC now. date.stdout=%r date.stderr=%r python3.stdout=%r python3.stderr=%r", probe.stdout, probe.stderr, py_probe.stdout, @@ -100,7 +99,7 @@ async def resolve(self) -> EnvironmentContext: f"printf '%s\\n' {qd}; " "uname -s | tr '[:upper:]' '[:lower:]'; " f"printf '%s\\n' {qd}; " - "basename \"${SHELL:-bash}\"; " + 'basename "${SHELL:-bash}"; ' f"printf '%s\\n' {qd}; " "uname -sr; " f"printf '%s\\n' {qd}; " diff --git a/libs/hexagent/tests/unit_tests/harness/test_environment.py b/libs/hexagent/tests/unit_tests/harness/test_environment.py index e41f9b32..f93ac826 100644 --- a/libs/hexagent/tests/unit_tests/harness/test_environment.py +++ b/libs/hexagent/tests/unit_tests/harness/test_environment.py @@ -1,4 +1,4 @@ -# ruff: noqa: PLR2004 +# ruff: noqa: PLR2004 """Tests for EnvironmentResolver.""" from __future__ import annotations From d90d3eb8470b471269f8880176c4877bdc64c829 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Sat, 28 Mar 2026 16:38:05 +0800 Subject: [PATCH 29/34] fix mcp httpx timeout --- libs/hexagent/hexagent/mcp/_client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/hexagent/hexagent/mcp/_client.py b/libs/hexagent/hexagent/mcp/_client.py index 8536e912..e21e93b7 100644 --- a/libs/hexagent/hexagent/mcp/_client.py +++ b/libs/hexagent/hexagent/mcp/_client.py @@ -166,7 +166,10 @@ async def _open_transport(self) -> tuple[Any, Any]: if transport_type == "http": http_cfg = cast("McpHttpServerConfig", config) http_client = await self._exit_stack.enter_async_context( - httpx.AsyncClient(headers=dict(http_cfg.get("headers", {}))), + httpx.AsyncClient( + headers=dict(http_cfg.get("headers", {})), + timeout=httpx.Timeout(300, connect=10), + ), ) read_stream, write_stream, _ = await self._exit_stack.enter_async_context( streamable_http_client(http_cfg["url"], http_client=http_client), From 48155ab7d61df30367db433bf79f99f4259963f4 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Sat, 28 Mar 2026 16:42:49 +0800 Subject: [PATCH 30/34] fix wsl io decoding and vm file staging --- libs/hexagent/hexagent/computer/local/_wsl.py | 35 ++++++++++++-- libs/hexagent/hexagent/computer/local/vm.py | 36 ++++++++++---- .../hexagent/computer/local/vm_win.py | 36 ++++++++++---- .../hexagent/tools/ui/present_to_user.py | 17 ++++--- .../tests/unit_tests/computer/test_vm.py | 4 +- .../tests/unit_tests/computer/test_wsl.py | 48 +++++++++++++++++-- .../tools/ui/test_present_to_user.py | 10 ++++ 7 files changed, 150 insertions(+), 36 deletions(-) diff --git a/libs/hexagent/hexagent/computer/local/_wsl.py b/libs/hexagent/hexagent/computer/local/_wsl.py index 4f2dc500..1a3cdb1a 100644 --- a/libs/hexagent/hexagent/computer/local/_wsl.py +++ b/libs/hexagent/hexagent/computer/local/_wsl.py @@ -46,10 +46,37 @@ def _decode_wsl_output(raw: bytes) -> str: - """Decode WSL output that may be UTF-16-LE on some Windows builds.""" - if raw[:2] == b"\xff\xfe" or b"\x00" in raw: - return raw.decode("utf-16-le", errors="replace").replace("\x00", "") - return raw.decode("utf-8", errors="replace") + """Decode WSL output that may mix UTF-16-LE and UTF-8 bytes. + + Some Windows builds emit UTF-16-LE diagnostics from ``wsl.exe`` and then + append plain UTF-8 stderr from the invoked shell in the same stream. + """ + if not raw: + return "" + + # Handle BOM-prefixed UTF-16-LE while preserving the remaining bytes for + # mixed-stream recovery below. + if raw.startswith(b"\xff\xfe"): + raw = raw[2:] + + # Fast path: regular UTF-8 output. + if b"\x00" not in raw: + return raw.decode("utf-8", errors="replace") + + # Mixed-path: decode the UTF-16-LE prefix up to the last NUL byte, then + # decode any trailing bytes as UTF-8 (common bash stderr tail). + last_nul = raw.rfind(b"\x00") + split = last_nul + 1 + if split % 2 != 0: + split += 1 + + head = raw[:split] + tail = raw[split:] + + text = head.decode("utf-16-le", errors="replace").replace("\x00", "") + if tail: + text += tail.decode("utf-8", errors="replace") + return text def _resolve_wsl_exe() -> str | None: diff --git a/libs/hexagent/hexagent/computer/local/vm.py b/libs/hexagent/hexagent/computer/local/vm.py index 7ba91d52..b5f3d032 100644 --- a/libs/hexagent/hexagent/computer/local/vm.py +++ b/libs/hexagent/hexagent/computer/local/vm.py @@ -15,7 +15,7 @@ import shlex import sys import uuid -from pathlib import Path +from pathlib import Path, PurePosixPath from typing import TYPE_CHECKING import petname @@ -127,20 +127,33 @@ async def upload(self, src: str, dst: str) -> None: msg = f"Source is not a file: {src}" raise CLIError(msg) - dst_parent = str(Path(dst).parent) + # Destination path is always POSIX inside the guest. + dst_parent = str(PurePosixPath(dst).parent) + tmp = f"/tmp/.upload-{uuid.uuid4().hex}" # noqa: S108 + sudo_prefix = "" try: - await self._vm.shell(f"sudo mkdir -p {shlex.quote(dst_parent)}") + sudo_probe = await self._vm.shell("command -v sudo >/dev/null 2>&1") + sudo_prefix = "sudo " if sudo_probe.exit_code == 0 else "" + mk_result = await self._vm.shell(f"{sudo_prefix}mkdir -p {shlex.quote(dst_parent)}") + if mk_result.exit_code != 0: + msg = mk_result.stderr or mk_result.stdout or f"Failed to create upload directory: {dst_parent}" + raise CLIError(msg) # Copy to /tmp first (always writable), then sudo mv into place. # This works regardless of destination directory ownership. - tmp = f"/tmp/.upload-{uuid.uuid4().hex}" # noqa: S108 await self._vm.copy(src, tmp, host_to_guest=True) - await self._vm.shell( - f"sudo mv {tmp} {shlex.quote(dst)} && " - f"sudo chown {self._session_name}:{self._session_name} {shlex.quote(dst)} && " - f"sudo chmod 644 {shlex.quote(dst)}" + stage_result = await self._vm.shell( + f"{sudo_prefix}mv {tmp} {shlex.quote(dst)} && " + f"{sudo_prefix}chown {self._session_name}:{self._session_name} {shlex.quote(dst)} && " + f"{sudo_prefix}chmod 644 {shlex.quote(dst)}" ) + if stage_result.exit_code != 0: + msg = stage_result.stderr or stage_result.stdout or f"Failed to stage uploaded file: {dst}" + raise CLIError(msg) except VMError as e: raise CLIError(str(e)) from e + finally: + # Best-effort cleanup when stage command failed before move. + await self._vm.shell(f"{sudo_prefix}rm -f {tmp}") async def download(self, src: str, dst: str) -> None: """Transfer a file from the VM session to the host. @@ -154,8 +167,11 @@ async def download(self, src: str, dst: str) -> None: self._check_active() Path(dst).parent.mkdir(parents=True, exist_ok=True) tmp = f"/tmp/.download-{uuid.uuid4().hex}" # noqa: S108 + sudo_prefix = "" try: - result = await self._vm.shell(f"sudo cp {shlex.quote(src)} {tmp} && sudo chmod 644 {tmp}") + sudo_probe = await self._vm.shell("command -v sudo >/dev/null 2>&1") + sudo_prefix = "sudo " if sudo_probe.exit_code == 0 else "" + result = await self._vm.shell(f"{sudo_prefix}cp {shlex.quote(src)} {tmp} && {sudo_prefix}chmod 644 {tmp}") if result.exit_code != 0: msg = result.stderr or result.stdout or f"Failed to stage {src} for download" raise CLIError(msg) @@ -164,7 +180,7 @@ async def download(self, src: str, dst: str) -> None: raise CLIError(str(e)) from e finally: # Best-effort cleanup of the temp file inside the guest. - await self._vm.shell(f"sudo rm -f {tmp}") + await self._vm.shell(f"{sudo_prefix}rm -f {tmp}") def _check_active(self) -> None: """Raise if handle is inactive.""" diff --git a/libs/hexagent/hexagent/computer/local/vm_win.py b/libs/hexagent/hexagent/computer/local/vm_win.py index 890433a2..3516c382 100644 --- a/libs/hexagent/hexagent/computer/local/vm_win.py +++ b/libs/hexagent/hexagent/computer/local/vm_win.py @@ -30,7 +30,7 @@ import os import shlex import uuid -from pathlib import Path +from pathlib import Path, PurePosixPath from typing import TYPE_CHECKING import petname @@ -154,20 +154,33 @@ async def upload(self, src: str, dst: str) -> None: msg = f"Source is not a file: {src}" raise CLIError(msg) - dst_parent = str(Path(dst).parent) + # Destination path is always a Linux path; keep POSIX semantics on Windows. + dst_parent = str(PurePosixPath(dst).parent) + tmp = f"/tmp/.upload-{uuid.uuid4().hex}" # noqa: S108 + sudo_prefix = "" try: - await self._vm.shell(f"sudo mkdir -p {shlex.quote(dst_parent)}") + sudo_probe = await self._vm.shell("command -v sudo >/dev/null 2>&1") + sudo_prefix = "sudo " if sudo_probe.exit_code == 0 else "" + mk_result = await self._vm.shell(f"{sudo_prefix}mkdir -p {shlex.quote(dst_parent)}") + if mk_result.exit_code != 0: + msg = mk_result.stderr or mk_result.stdout or f"Failed to create upload directory: {dst_parent}" + raise CLIError(msg) # Copy to /tmp first (always writable), then sudo mv into place. # This works regardless of destination directory ownership. - tmp = f"/tmp/.upload-{uuid.uuid4().hex}" # noqa: S108 await self._vm.copy(src, tmp, host_to_guest=True) - await self._vm.shell( - f"sudo mv {tmp} {shlex.quote(dst)} && " - f"sudo chown {self._session_name}:{self._session_name} {shlex.quote(dst)} && " - f"sudo chmod 644 {shlex.quote(dst)}" + stage_result = await self._vm.shell( + f"{sudo_prefix}mv {tmp} {shlex.quote(dst)} && " + f"{sudo_prefix}chown {self._session_name}:{self._session_name} {shlex.quote(dst)} && " + f"{sudo_prefix}chmod 644 {shlex.quote(dst)}" ) + if stage_result.exit_code != 0: + msg = stage_result.stderr or stage_result.stdout or f"Failed to stage uploaded file: {dst}" + raise CLIError(msg) except VMError as e: raise CLIError(str(e)) from e + finally: + # Best-effort cleanup when stage command failed before move. + await self._vm.shell(f"{sudo_prefix}rm -f {tmp}") async def download(self, src: str, dst: str) -> None: """Transfer a file from the WSL session to the host. @@ -179,8 +192,11 @@ async def download(self, src: str, dst: str) -> None: self._check_active() Path(dst).parent.mkdir(parents=True, exist_ok=True) tmp = f"/tmp/.download-{uuid.uuid4().hex}" # noqa: S108 + sudo_prefix = "" try: - result = await self._vm.shell(f"sudo cp {shlex.quote(src)} {tmp} && sudo chmod 644 {tmp}") + sudo_probe = await self._vm.shell("command -v sudo >/dev/null 2>&1") + sudo_prefix = "sudo " if sudo_probe.exit_code == 0 else "" + result = await self._vm.shell(f"{sudo_prefix}cp {shlex.quote(src)} {tmp} && {sudo_prefix}chmod 644 {tmp}") if result.exit_code != 0: msg = result.stderr or result.stdout or f"Failed to stage {src} for download" raise CLIError(msg) @@ -189,7 +205,7 @@ async def download(self, src: str, dst: str) -> None: raise CLIError(str(e)) from e finally: # Best-effort cleanup of the temp file inside the guest. - await self._vm.shell(f"sudo rm -f {tmp}") + await self._vm.shell(f"{sudo_prefix}rm -f {tmp}") def _check_active(self) -> None: """Raise if handle is inactive.""" diff --git a/libs/hexagent/hexagent/tools/ui/present_to_user.py b/libs/hexagent/hexagent/tools/ui/present_to_user.py index 7efeb826..c533bd15 100644 --- a/libs/hexagent/hexagent/tools/ui/present_to_user.py +++ b/libs/hexagent/hexagent/tools/ui/present_to_user.py @@ -125,11 +125,10 @@ def _build_case_block() -> str: return "\n".join(arms) -# The bash script body template. ``{case_arms}`` is replaced at import -# time with the generated case block. $1 is the output directory; -# $2.. are file paths. +# The bash script body template. ``{case_arms}`` is replaced at import +# time with the generated case block. ``OUTPUT_DIR`` is injected by +# ``_build_command`` and file paths are passed via ``$@``. _SCRIPT_BODY = r""" -OUTPUT_DIR="$1"; shift mkdir -p "$OUTPUT_DIR" REAL_OUT="$(realpath "$OUTPUT_DIR")" @@ -204,8 +203,14 @@ def _build_command(filepaths: list[str], output_dir: str) -> str: Returns: A shell command string safe for ``Computer.run()``. """ - quoted_args = " ".join(shlex.quote(p) for p in [output_dir, *filepaths]) - return f"bash -c {shlex.quote(_SCRIPT_BODY_LF)} _ {quoted_args}" + quoted_file_args = " ".join(shlex.quote(p) for p in filepaths) + set_args = f"set -- {quoted_file_args}" if quoted_file_args else "set --" + script = f"OUTPUT_DIR={shlex.quote(output_dir)}\n{set_args}\n{_SCRIPT_BODY_LF}" + # WSL can evaluate one outer shell layer before the intended ``bash -c`` + # command, which would eagerly expand ``$...`` and break the script. + # Pre-escape dollars so expansion happens only in the inner bash. + script_for_outer = script.replace("$", r"\$") + return f"bash -c {shlex.quote(script_for_outer)}" class PresentToUserTool(BaseAgentTool[PresentToUserToolParams]): diff --git a/libs/hexagent/tests/unit_tests/computer/test_vm.py b/libs/hexagent/tests/unit_tests/computer/test_vm.py index 90e0a7eb..eb0f83c1 100644 --- a/libs/hexagent/tests/unit_tests/computer/test_vm.py +++ b/libs/hexagent/tests/unit_tests/computer/test_vm.py @@ -216,7 +216,7 @@ async def test_upload_copies_via_tmp_then_moves(self, tmp_path: Path) -> None: assert copy_call.kwargs.get("host_to_guest") is True # Should sudo mv from tmp to destination, chown to session user, and chmod 644 - mv_call = vm.shell.call_args_list[1] + mv_call = next(c for c in vm.shell.call_args_list if " mv " in c.args[0]) assert "sudo mv" in mv_call.args[0] assert "/remote/file.txt" in mv_call.args[0] assert "chown test-session:test-session" in mv_call.args[0] @@ -232,7 +232,7 @@ async def test_upload_creates_parent_dir_on_guest(self, tmp_path: Path) -> None: await computer.upload(str(src), "/remote/deep/file.txt") - mkdir_call = vm.shell.call_args_list[0] + mkdir_call = next(c for c in vm.shell.call_args_list if "mkdir -p" in c.args[0]) assert "sudo mkdir -p" in mkdir_call.args[0] assert "/remote/deep" in mkdir_call.args[0] diff --git a/libs/hexagent/tests/unit_tests/computer/test_wsl.py b/libs/hexagent/tests/unit_tests/computer/test_wsl.py index f2ed9766..c13bcd79 100644 --- a/libs/hexagent/tests/unit_tests/computer/test_wsl.py +++ b/libs/hexagent/tests/unit_tests/computer/test_wsl.py @@ -1,7 +1,7 @@ # ruff: noqa: PLR2004 S108 ARG005 UP012 """Tests for WslVM and _VMSessionComputer (Windows variant). -All tests mock the WSL backend — no wsl.exe or WSL2 required. +All tests mock the WSL backend - no wsl.exe or WSL2 required. """ from __future__ import annotations @@ -19,6 +19,7 @@ from hexagent.computer.local._types import ResolvedMount from hexagent.computer.local._wsl import ( WslVM, + _decode_wsl_output, _parse_status_output, _session_user_from_guest_mount_path, _win_path_to_wsl, @@ -243,12 +244,26 @@ async def test_upload_copies_via_tmp_then_moves(self, tmp_path: Path) -> None: assert copy_call.args[1].startswith("/tmp/.upload-") assert copy_call.kwargs.get("host_to_guest") is True - mv_call = vm.shell.call_args_list[1] + mv_call = next(c for c in vm.shell.call_args_list if " mv " in c.args[0]) assert "sudo mv" in mv_call.args[0] assert "/remote/file.txt" in mv_call.args[0] assert "chown test-session:test-session" in mv_call.args[0] assert "chmod 644" in mv_call.args[0] + async def test_upload_uses_posix_parent_for_session_paths(self, tmp_path: Path) -> None: + vm = _mock_vm() + vm.copy = AsyncMock() + computer = _make_computer(vm) + + src = tmp_path / "file.txt" + src.write_text("data") + + await computer.upload(str(src), "/sessions/alice/mnt/uploads/file.txt") + + mkdir_call = next(c for c in vm.shell.call_args_list if "mkdir -p" in c.args[0]) + assert "/sessions/alice/mnt/uploads" in mkdir_call.args[0] + assert "\\sessions\\alice\\mnt\\uploads" not in mkdir_call.args[0] + async def test_upload_missing_src_raises_file_not_found(self, tmp_path: Path) -> None: vm = _mock_vm() computer = _make_computer(vm) @@ -288,7 +303,7 @@ async def test_download_stages_via_tmp(self, tmp_path: Path) -> None: await computer.download("/remote/file.txt", str(dst)) # First shell call: sudo cp to tmp + chmod - stage_call = vm.shell.call_args_list[0] + stage_call = next(c for c in vm.shell.call_args_list if " cp " in c.args[0]) assert "sudo cp" in stage_call.args[0] assert "chmod 644" in stage_call.args[0] @@ -340,7 +355,7 @@ def test_satisfies_computer_protocol(self) -> None: # =========================================================================== -# WslVM — pure logic only (no subprocess) +# WslVM - pure logic only (no subprocess) # =========================================================================== @@ -419,6 +434,30 @@ async def test_start_does_not_retry_on_non_transient_failure(self) -> None: mock_apply.assert_not_awaited() +# =========================================================================== +# WSL output decoding +# =========================================================================== + + +class TestDecodeWslOutput: + """Tests for mixed-encoding stderr decoding.""" + + def test_utf8_plain(self) -> None: + assert _decode_wsl_output("hello".encode("utf-8")) == "hello" + + def test_utf16le_with_bom(self) -> None: + raw = b"\xff\xfe" + "warning: test".encode("utf-16-le") + assert "warning: test" in _decode_wsl_output(raw) + + def test_mixed_utf16le_prefix_and_utf8_tail(self) -> None: + prefix = "wsl: localhost proxy config detected but not mirrored to WSL.\r\n".encode("utf-16-le") + tail = b"/bin/bash: line 1: _mime_by_ext: command not found\n" + text = _decode_wsl_output(prefix + tail) + + assert "localhost proxy config detected" in text + assert "_mime_by_ext: command not found" in text + + # =========================================================================== # Status output parsing # =========================================================================== @@ -821,3 +860,4 @@ async def test_empty_mounts_is_noop(self) -> None: ): await vm._apply_bind_mounts() mock_shell.assert_not_awaited() + diff --git a/libs/hexagent/tests/unit_tests/tools/ui/test_present_to_user.py b/libs/hexagent/tests/unit_tests/tools/ui/test_present_to_user.py index 3e68db1f..8938bcce 100644 --- a/libs/hexagent/tests/unit_tests/tools/ui/test_present_to_user.py +++ b/libs/hexagent/tests/unit_tests/tools/ui/test_present_to_user.py @@ -165,6 +165,16 @@ def test_embedded_script_normalized_to_lf(self) -> None: cmd = _build_command(["/a.txt"], "/out") assert "\r" not in cmd + def test_uses_inner_bash_c_without_positional_arg_shim(self) -> None: + cmd = _build_command(["/a.txt"], "/out") + assert "bash -c" in cmd + assert " _ " not in cmd + assert "OUTPUT_DIR=/out" in cmd + + def test_escapes_dollar_for_wsl_outer_shell(self) -> None: + cmd = _build_command(["/a.txt"], "/out") + assert r"\$OUTPUT_DIR" in cmd + # --------------------------------------------------------------------------- # _EXT_MIME_MAP / generated script tests From 6c17a92fc5f1619786fcda2c2eaabcd2bdcf5715 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Sat, 28 Mar 2026 16:43:00 +0800 Subject: [PATCH 31/34] improve windows setup flow and desktop restart UX --- .../backend/hexagent_api/routes/setup.py | 72 +++++++++++++++++-- libs/hexagent_demo/electron/main.js | 49 +++++++++++++ libs/hexagent_demo/electron/preload.js | 1 + .../electron/scripts/build-backend.ps1 | 52 +++++++++----- .../electron/scripts/build-backend.sh | 18 +++++ .../src/components/RestartRequiredModal.tsx | 36 ++++++++-- .../frontend/src/components/SettingsModal.tsx | 2 +- .../frontend/src/components/WelcomeScreen.tsx | 2 +- libs/hexagent_demo/frontend/src/electron.d.ts | 4 ++ .../frontend/src/locales/en/misc.json | 7 +- .../frontend/src/locales/zh-CN/misc.json | 7 +- 11 files changed, 217 insertions(+), 33 deletions(-) diff --git a/libs/hexagent_demo/backend/hexagent_api/routes/setup.py b/libs/hexagent_demo/backend/hexagent_api/routes/setup.py index 4a2137d0..3fc6d878 100644 --- a/libs/hexagent_demo/backend/hexagent_api/routes/setup.py +++ b/libs/hexagent_demo/backend/hexagent_api/routes/setup.py @@ -244,6 +244,15 @@ def _looks_like_missing_wsl_disk(msg: str) -> bool: ) +def _looks_like_wsl_localhost_proxy_warning(msg: str) -> bool: + """Return True for known non-fatal WSL localhost-proxy warning text.""" + text = (msg or "").lower() + return ( + ("localhost" in text and "proxy" in text and "wsl" in text and "nat" in text) + or ("localhost 代理" in (msg or "") and "未镜像到 wsl" in (msg or "")) + ) + + def _wsl2_blocker_reason(text: str) -> str | None: """Return a friendly reason when host cannot run WSL2.""" t = (text or "").lower() @@ -396,6 +405,23 @@ async def _wsl_probe_start() -> tuple[bool, str]: return False, _combine_wsl_output(stdout_b, stderr_b) +async def _wait_for_wsl_vhdx(import_dir: Path, timeout_s: float = 45.0) -> Path | None: + """Wait until WSL import materializes ``ext4.vhdx`` under ``import_dir``. + + On some Windows hosts `wsl --import` returns before the VHDX file is fully + visible to subsequent `wsl -d` start attempts, which can cause transient + `MountDisk ... ERROR_PATH_NOT_FOUND`. + """ + target = import_dir / "ext4.vhdx" + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout_s + while loop.time() < deadline: + if target.is_file(): + return target + await asyncio.sleep(0.5) + return None + + # ``wsl -l -v`` uses the Windows display language for the STATE column. # Cowork only needs the ``hexagent`` distro to exist; WSL starts it on demand. _WSL_COWORK_READY_STATES = frozenset( @@ -1190,12 +1216,14 @@ async def _run_wsl(self) -> None: self._error = f"exit {proc_import.returncode}" return + self._emit("progress", {"step": "starting", "message": "Finalizing imported WSL disk..."}) + await _wait_for_wsl_vhdx(import_dir) self._emit("progress", {"step": "starting", "message": "Starting imported HexAgent WSL distro..."}) ok, err = await self._start_wsl_instance( wsl_exe, step="starting", message="Starting imported HexAgent WSL distro...", - retries_on_missing_disk=3, + retries_on_missing_disk=6, ) if ok: self._emit("done", {"message": "WSL distro imported from bundled image and started successfully"}) @@ -1287,12 +1315,14 @@ async def _run_wsl(self) -> None: self._error = f"exit {proc_import.returncode}" return + self._emit("progress", {"step": "starting", "message": "Finalizing imported WSL disk..."}) + await _wait_for_wsl_vhdx(import_dir) self._emit("progress", {"step": "starting", "message": "Starting HexAgent WSL distro..."}) ok, err = await self._start_wsl_instance( wsl_exe, step="starting", message="Starting HexAgent WSL distro...", - retries_on_missing_disk=3, + retries_on_missing_disk=6, ) if ok: self._emit("done", {"message": "WSL distro created and started successfully"}) @@ -1504,10 +1534,40 @@ async def _run_wsl(self, **kwargs: object) -> None: user="root", ) if rc != 0: - self._emit("error", {"message": f"Failed to stage setup files in WSL: {err}"}) - self._status = "error" - self._error = "Stage failed" - return + # Some WSL builds emit a localhost-proxy warning under NAT mode, + # and may still finish staging. Verify before failing hard. + if _looks_like_wsl_localhost_proxy_warning(err): + verify_rc, _, verify_err = await _wsl_shell( + f"test -f {setup_vm_dir_quoted}/setup.sh && test -d {setup_vm_dir_quoted}/steps", + timeout=15, + user="root", + ) + if verify_rc == 0: + self._emit( + "progress", + { + "step": "copying", + "message": "WSL reported localhost proxy warning, but setup files were staged successfully. Continuing...", + }, + ) + else: + self._emit( + "error", + { + "message": ( + f"Failed to stage setup files in WSL: {err}" + + (f"\nVerification error: {verify_err}" if verify_err else "") + ) + }, + ) + self._status = "error" + self._error = "Stage failed" + return + else: + self._emit("error", {"message": f"Failed to stage setup files in WSL: {err}"}) + self._status = "error" + self._error = "Stage failed" + return self._emit("progress", {"step": "starting", "message": "Starting provisioning..."}) cmd = f"bash {_SETUP_VM_DIR}/setup.sh" diff --git a/libs/hexagent_demo/electron/main.js b/libs/hexagent_demo/electron/main.js index c31b4f6a..fa2bca61 100644 --- a/libs/hexagent_demo/electron/main.js +++ b/libs/hexagent_demo/electron/main.js @@ -416,6 +416,55 @@ try { }; }); +ipcMain.handle("restart-windows-now", async () => { + if (process.platform !== "win32") { + return { ok: false, message: "This action is only available on Windows." }; + } + + // First try a normal restart request. + let res = await runCommand("shutdown.exe", ["/r", "/t", "0"]); + if (res.code === 0) { + return { ok: true, message: "Windows restart has been triggered." }; + } + + // Fallback with elevation prompt when policy/permissions block direct call. + const psScript = ` +$ErrorActionPreference = 'Stop' +try { + Start-Process -FilePath shutdown.exe -ArgumentList @('/r','/t','0') -Verb RunAs + exit 0 +} catch { + $msg = $_.Exception.Message + if ([string]::IsNullOrWhiteSpace($msg)) { $msg = "Unknown restart failure." } + Write-Output ("RESTART_ERR:" + $msg) + exit 1 +} +`.trim(); + + res = await runCommand("powershell.exe", [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + psScript, + ]); + + if (res.code === 0) { + return { ok: true, message: "Windows restart has been triggered." }; + } + + const combined = `${res.stderr || ""}\n${res.stdout || ""}`.trim(); + const restartErr = (combined.match(/RESTART_ERR:(.*)/) || [null, ""])[1]?.trim(); + const cancelled = /canceled|cancelled|拒绝|已取消|denied/i.test(combined); + if (cancelled) { + return { ok: false, message: "Restart was cancelled." }; + } + return { + ok: false, + message: restartErr || combined || `Failed to trigger restart (exit ${res.code}).`, + }; +}); + // ── Window ─────────────────────────────────────────────────────────────────── function createWindow() { diff --git a/libs/hexagent_demo/electron/preload.js b/libs/hexagent_demo/electron/preload.js index 279d4ef0..4c2a8b6a 100644 --- a/libs/hexagent_demo/electron/preload.js +++ b/libs/hexagent_demo/electron/preload.js @@ -7,4 +7,5 @@ contextBridge.exposeInMainWorld("electronAPI", { platform: process.platform, checkWslPrerequisites: () => ipcRenderer.invoke("check-wsl-prerequisites"), installWslRuntime: () => ipcRenderer.invoke("install-wsl-runtime"), + restartWindowsNow: () => ipcRenderer.invoke("restart-windows-now"), }); diff --git a/libs/hexagent_demo/electron/scripts/build-backend.ps1 b/libs/hexagent_demo/electron/scripts/build-backend.ps1 index 16cbd8e2..f6f0dc05 100644 --- a/libs/hexagent_demo/electron/scripts/build-backend.ps1 +++ b/libs/hexagent_demo/electron/scripts/build-backend.ps1 @@ -3,6 +3,16 @@ $ErrorActionPreference = "Stop" $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $ElectronDir = Resolve-Path "$ScriptDir\.." $BackendDir = Resolve-Path "$ElectronDir\..\backend" +$ConfigSource = Join-Path $BackendDir "config.json" +$TempConfigCreated = $false + +if (-not (Test-Path $ConfigSource)) { + # Keep packaging resilient in CI/local envs where config.json is not present. + # Electron will still seed userData/config.json from this bundled default. + $ConfigSource = Join-Path $BackendDir ".packaged-default-config.json" + Set-Content -Path $ConfigSource -Value "{}" -Encoding UTF8 + $TempConfigCreated = $true +} Write-Host "==> Installing PyInstaller..." Set-Location $BackendDir @@ -31,6 +41,7 @@ $pyinstallerArgs = @( "--collect-data", "hexagent", "--add-data", "../../hexagent/sandbox/vm;sandbox/vm", "--add-data", "skills;skills", + "--add-data", "$ConfigSource;.", "hexagent_api/server.py" ) @@ -39,23 +50,30 @@ if (-not (Test-Path "$BackendDir\skills")) { New-Item -ItemType Directory -Path "$BackendDir\skills" | Out-Null } -if (Get-Command uv -ErrorAction SilentlyContinue) { - uv pip install pyinstaller - Write-Host "==> Building backend with PyInstaller (uv)..." - uv run pyinstaller @pyinstallerArgs -} else { - $venvPython = Join-Path $BackendDir ".venv\Scripts\python.exe" - if (-not (Test-Path $venvPython)) { - throw "uv not found and backend venv python missing: $venvPython" +try { + if (Get-Command uv -ErrorAction SilentlyContinue) { + uv pip install pyinstaller + Write-Host "==> Building backend with PyInstaller (uv)..." + uv run pyinstaller @pyinstallerArgs + } else { + $venvPython = Join-Path $BackendDir ".venv\Scripts\python.exe" + if (-not (Test-Path $venvPython)) { + throw "uv not found and backend venv python missing: $venvPython" + } + Write-Host "==> uv not found, using backend venv python fallback..." + & $venvPython -m PyInstaller @pyinstallerArgs } - Write-Host "==> uv not found, using backend venv python fallback..." - & $venvPython -m PyInstaller @pyinstallerArgs -} -Write-Host "==> Copying dist to electron/backend_dist..." -if (Test-Path "$ElectronDir\backend_dist") { - Remove-Item -Recurse -Force "$ElectronDir\backend_dist" -} -Copy-Item -Recurse "$BackendDir\dist\hexagent_api_server" "$ElectronDir\backend_dist" + Write-Host "==> Copying dist to electron/backend_dist..." + if (Test-Path "$ElectronDir\backend_dist") { + Remove-Item -Recurse -Force "$ElectronDir\backend_dist" + } + Copy-Item -Recurse "$BackendDir\dist\hexagent_api_server" "$ElectronDir\backend_dist" -Write-Host "==> Backend build complete." + Write-Host "==> Backend build complete." +} +finally { + if ($TempConfigCreated -and (Test-Path $ConfigSource)) { + Remove-Item -Force $ConfigSource + } +} diff --git a/libs/hexagent_demo/electron/scripts/build-backend.sh b/libs/hexagent_demo/electron/scripts/build-backend.sh index 10661e62..697df61a 100755 --- a/libs/hexagent_demo/electron/scripts/build-backend.sh +++ b/libs/hexagent_demo/electron/scripts/build-backend.sh @@ -12,6 +12,23 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ELECTRON_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" BACKEND_DIR="$(cd "$ELECTRON_DIR/../backend" && pwd)" TARGET_ARCH="${1:-}" +CONFIG_SOURCE="$BACKEND_DIR/config.json" +TEMP_CONFIG_CREATED=0 + +if [ ! -f "$CONFIG_SOURCE" ]; then + # Keep packaging resilient in CI/local envs where config.json is not present. + # Electron will still seed userData/config.json from this bundled default. + CONFIG_SOURCE="$BACKEND_DIR/.packaged-default-config.json" + printf '{}\n' > "$CONFIG_SOURCE" + TEMP_CONFIG_CREATED=1 +fi + +cleanup_temp_config() { + if [ "$TEMP_CONFIG_CREATED" = "1" ] && [ -f "$CONFIG_SOURCE" ]; then + rm -f "$CONFIG_SOURCE" + fi +} +trap cleanup_temp_config EXIT # ── PyInstaller flags (shared) ── PYINSTALLER_ARGS=( @@ -40,6 +57,7 @@ PYINSTALLER_ARGS=( --collect-data hexagent --add-data "skills:skills" --add-data "../../hexagent/sandbox/vm:sandbox/vm" + --add-data "$CONFIG_SOURCE:." hexagent_api/server.py ) diff --git a/libs/hexagent_demo/frontend/src/components/RestartRequiredModal.tsx b/libs/hexagent_demo/frontend/src/components/RestartRequiredModal.tsx index 0cfc1f25..e2d42789 100644 --- a/libs/hexagent_demo/frontend/src/components/RestartRequiredModal.tsx +++ b/libs/hexagent_demo/frontend/src/components/RestartRequiredModal.tsx @@ -1,6 +1,7 @@ -import { AlertTriangle, RefreshCw, Settings } from "lucide-react"; +import { AlertTriangle, Loader2, Settings } from "lucide-react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { useVMSetup } from "../vmSetup"; +import { useAppContext } from "../store"; interface RestartRequiredModalProps { open: boolean; @@ -14,7 +15,30 @@ export default function RestartRequiredModal({ onOpenSettings, }: RestartRequiredModalProps) { const { t } = useTranslation("misc"); - const vm = useVMSetup(); + const { dispatch } = useAppContext(); + const [restarting, setRestarting] = useState(false); + + const handleRestartNow = async () => { + if (restarting) return; + const confirmed = window.confirm(t("restartRequired.confirmRestartNow")); + if (!confirmed) return; + + setRestarting(true); + try { + const api = window.electronAPI?.restartWindowsNow; + if (!api) { + throw new Error(t("restartRequired.restartNotSupported")); + } + const res = await api(); + if (!res?.ok) { + throw new Error(res?.message || t("restartRequired.restartFailed")); + } + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : t("restartRequired.restartFailed"); + dispatch({ type: "SHOW_NOTIFICATION", payload: { message: msg, type: "error" } }); + setRestarting(false); + } + }; if (!open) return null; @@ -39,9 +63,9 @@ export default function RestartRequiredModal({ {t("restartRequired.openSandboxSettings")} -

diff --git a/libs/hexagent_demo/frontend/src/components/SettingsModal.tsx b/libs/hexagent_demo/frontend/src/components/SettingsModal.tsx index 59253023..bcd765db 100644 --- a/libs/hexagent_demo/frontend/src/components/SettingsModal.tsx +++ b/libs/hexagent_demo/frontend/src/components/SettingsModal.tsx @@ -13,7 +13,7 @@ import type { PhaseStatus } from "../vmSetup"; /** Available languages. Add new entries here to support more languages. */ const LANGUAGES = [ { code: "en", label: "English" }, - { code: "zh-CN", label: "绠€浣撲腑鏂? }, + { code: "zh-CN", label: "Chinese (Simplified)" }, ] as const; interface SettingsModalProps { diff --git a/libs/hexagent_demo/frontend/src/components/WelcomeScreen.tsx b/libs/hexagent_demo/frontend/src/components/WelcomeScreen.tsx index 91797da3..a35c4648 100644 --- a/libs/hexagent_demo/frontend/src/components/WelcomeScreen.tsx +++ b/libs/hexagent_demo/frontend/src/components/WelcomeScreen.tsx @@ -386,7 +386,7 @@ export default function WelcomeScreen({ onSubmit, mode, onOpenSettings }: Welcom )} {sandboxBlocked && (
- {missingE2bKey ? t("e2bKeyRequired") : t("vmSetupRequired")} 鈥攞" "} + {missingE2bKey ? t("e2bKeyRequired") : t("vmSetupRequired")}{" - "} diff --git a/libs/hexagent_demo/frontend/src/electron.d.ts b/libs/hexagent_demo/frontend/src/electron.d.ts index ed453916..c665b9f3 100644 --- a/libs/hexagent_demo/frontend/src/electron.d.ts +++ b/libs/hexagent_demo/frontend/src/electron.d.ts @@ -28,6 +28,10 @@ declare global { stdout?: string; stderr?: string; }>; + restartWindowsNow?: () => Promise<{ + ok: boolean; + message?: string; + }>; }; } } diff --git a/libs/hexagent_demo/frontend/src/locales/en/misc.json b/libs/hexagent_demo/frontend/src/locales/en/misc.json index 1453eb3b..45bdbc4f 100644 --- a/libs/hexagent_demo/frontend/src/locales/en/misc.json +++ b/libs/hexagent_demo/frontend/src/locales/en/misc.json @@ -22,7 +22,12 @@ "wslComplete": "WSL runtime installation is complete, but Windows must restart before OpenAgent can continue.", "pleaseRestart": "Please restart your computer now, otherwise VM/Cowork features will not work.", "openSandboxSettings": "Open Sandbox Settings", - "recheck": "I have restarted, Re-check" + "recheck": "I have restarted, Re-check", + "restartNow": "Restart Now", + "restarting": "Restarting...", + "confirmRestartNow": "Restart Windows now? Unsaved work in other apps may be lost.", + "restartNotSupported": "Immediate restart is only available in the desktop app on Windows.", + "restartFailed": "Failed to trigger Windows restart." }, "vmSetup": { "settingUp": "Setting up VM", diff --git a/libs/hexagent_demo/frontend/src/locales/zh-CN/misc.json b/libs/hexagent_demo/frontend/src/locales/zh-CN/misc.json index ab5e58e8..f4e26a88 100644 --- a/libs/hexagent_demo/frontend/src/locales/zh-CN/misc.json +++ b/libs/hexagent_demo/frontend/src/locales/zh-CN/misc.json @@ -22,7 +22,12 @@ "wslComplete": "WSL 运行时安装已完成,但 Windows 需要重启才能继续使用 OpenAgent。", "pleaseRestart": "请立即重启计算机,否则虚拟机/协作功能将无法使用。", "openSandboxSettings": "打开沙盒设置", - "recheck": "已重启,重新检查" + "recheck": "已重启,重新检查", + "restartNow": "立即重启", + "restarting": "正在重启...", + "confirmRestartNow": "确认立即重启 Windows 吗?其他应用中未保存的内容可能会丢失。", + "restartNotSupported": "仅 Windows 桌面客户端支持立即重启。", + "restartFailed": "触发 Windows 重启失败。" }, "vmSetup": { "settingUp": "正在配置虚拟机", From 61f0196dedb7691a679d509e7d567a3948625739 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Sat, 28 Mar 2026 16:43:09 +0800 Subject: [PATCH 32/34] add email and pptx skill bundles --- .../skills/email-mail-master/LICENSE.txt | 24 + .../backend/skills/email-mail-master/SKILL.md | 58 + .../skills/email-mail-master/_meta.json | 6 + .../scripts/email_manager.py | 557 +++ .../skills/email-mail-master/scripts/mail.py | 221 + .../scripts/requirements.txt | 2 + .../skills/pptx-plus-linux/LICENSE.txt | 14 + .../backend/skills/pptx-plus-linux/editing.md | 205 + .../backend/skills/pptx-plus-linux/examin.md | 117 + .../skills/pptx-plus-linux/pptxgenjs.md | 422 ++ .../references/generate_area_chart.md | 27 + .../references/generate_bar_chart.md | 27 + .../references/generate_boxplot_chart.md | 25 + .../references/generate_column_chart.md | 27 + .../references/generate_district_map.md | 28 + .../references/generate_dual_axes_chart.md | 25 + .../references/generate_fishbone_diagram.md | 20 + .../references/generate_flow_diagram.md | 22 + .../references/generate_funnel_chart.md | 23 + .../references/generate_histogram_chart.md | 26 + .../references/generate_line_chart.md | 26 + .../references/generate_liquid_chart.md | 24 + .../references/generate_mind_map.md | 20 + .../references/generate_network_graph.md | 22 + .../references/generate_organization_chart.md | 21 + .../references/generate_path_map.md | 20 + .../references/generate_pie_chart.md | 24 + .../references/generate_pin_map.md | 23 + .../references/generate_radar_chart.md | 24 + .../references/generate_sankey_chart.md | 24 + .../references/generate_scatter_chart.md | 25 + .../references/generate_spreadsheet.md | 24 + .../references/generate_treemap_chart.md | 23 + .../references/generate_venn_chart.md | 23 + .../references/generate_violin_chart.md | 25 + .../references/generate_word_cloud_chart.md | 23 + .../pptx-plus-linux/scripts/__init__.py | 0 .../pptx-plus-linux/scripts/add_slide.py | 195 + .../skills/pptx-plus-linux/scripts/clean.py | 286 ++ .../pptx-plus-linux/scripts/generate.js | 173 + .../scripts/office/helpers/__init__.py | 0 .../scripts/office/helpers/merge_runs.py | 199 + .../office/helpers/simplify_redlines.py | 197 + .../pptx-plus-linux/scripts/office/pack.py | 159 + .../schemas/ISO-IEC29500-4_2016/dml-chart.xsd | 1499 ++++++ .../ISO-IEC29500-4_2016/dml-chartDrawing.xsd | 146 + .../ISO-IEC29500-4_2016/dml-diagram.xsd | 1085 ++++ .../ISO-IEC29500-4_2016/dml-lockedCanvas.xsd | 11 + .../schemas/ISO-IEC29500-4_2016/dml-main.xsd | 3081 ++++++++++++ .../ISO-IEC29500-4_2016/dml-picture.xsd | 23 + .../dml-spreadsheetDrawing.xsd | 185 + .../dml-wordprocessingDrawing.xsd | 287 ++ .../schemas/ISO-IEC29500-4_2016/pml.xsd | 1676 +++++++ .../shared-additionalCharacteristics.xsd | 28 + .../shared-bibliography.xsd | 144 + .../shared-commonSimpleTypes.xsd | 174 + .../shared-customXmlDataProperties.xsd | 25 + .../shared-customXmlSchemaProperties.xsd | 18 + .../shared-documentPropertiesCustom.xsd | 59 + .../shared-documentPropertiesExtended.xsd | 56 + .../shared-documentPropertiesVariantTypes.xsd | 195 + .../ISO-IEC29500-4_2016/shared-math.xsd | 582 +++ .../shared-relationshipReference.xsd | 25 + .../schemas/ISO-IEC29500-4_2016/sml.xsd | 4439 +++++++++++++++++ .../schemas/ISO-IEC29500-4_2016/vml-main.xsd | 570 +++ .../ISO-IEC29500-4_2016/vml-officeDrawing.xsd | 509 ++ .../vml-presentationDrawing.xsd | 12 + .../vml-spreadsheetDrawing.xsd | 108 + .../vml-wordprocessingDrawing.xsd | 96 + .../schemas/ISO-IEC29500-4_2016/wml.xsd | 3646 ++++++++++++++ .../schemas/ISO-IEC29500-4_2016/xml.xsd | 116 + .../ecma/fouth-edition/opc-contentTypes.xsd | 42 + .../ecma/fouth-edition/opc-coreProperties.xsd | 50 + .../schemas/ecma/fouth-edition/opc-digSig.xsd | 49 + .../ecma/fouth-edition/opc-relationships.xsd | 33 + .../scripts/office/schemas/mce/mc.xsd | 75 + .../office/schemas/microsoft/wml-2010.xsd | 560 +++ .../office/schemas/microsoft/wml-2012.xsd | 67 + .../office/schemas/microsoft/wml-2018.xsd | 14 + .../office/schemas/microsoft/wml-cex-2018.xsd | 20 + .../office/schemas/microsoft/wml-cid-2016.xsd | 13 + .../microsoft/wml-sdtdatahash-2020.xsd | 4 + .../schemas/microsoft/wml-symex-2015.xsd | 8 + .../pptx-plus-linux/scripts/office/soffice.py | 184 + .../pptx-plus-linux/scripts/office/unpack.py | 132 + .../scripts/office/validate.py | 111 + .../scripts/office/validators/__init__.py | 15 + .../scripts/office/validators/base.py | 847 ++++ .../scripts/office/validators/docx.py | 446 ++ .../scripts/office/validators/pptx.py | 275 + .../scripts/office/validators/redlining.py | 247 + .../pptx-plus-linux/scripts/ppt_to_pic.py | 153 + .../pptx-plus-linux/scripts/thumbnail.py | 289 ++ .../pptx-plus-linux/scripts/vision_qwen.py | 211 + .../pptx-plus-linux/scripts/web_search.py | 208 + .../backend/skills/pptx-plus-linux/skill.md | 427 ++ 96 files changed, 26731 insertions(+) create mode 100644 libs/hexagent_demo/backend/skills/email-mail-master/LICENSE.txt create mode 100644 libs/hexagent_demo/backend/skills/email-mail-master/SKILL.md create mode 100644 libs/hexagent_demo/backend/skills/email-mail-master/_meta.json create mode 100644 libs/hexagent_demo/backend/skills/email-mail-master/scripts/email_manager.py create mode 100644 libs/hexagent_demo/backend/skills/email-mail-master/scripts/mail.py create mode 100644 libs/hexagent_demo/backend/skills/email-mail-master/scripts/requirements.txt create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/LICENSE.txt create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/editing.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/examin.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/pptxgenjs.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_area_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_bar_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_boxplot_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_column_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_district_map.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_dual_axes_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_fishbone_diagram.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_flow_diagram.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_funnel_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_histogram_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_line_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_liquid_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_mind_map.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_network_graph.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_organization_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_path_map.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_pie_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_pin_map.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_radar_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_sankey_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_scatter_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_spreadsheet.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_treemap_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_venn_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_violin_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_word_cloud_chart.md create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/__init__.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/add_slide.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/clean.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/generate.js create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/helpers/__init__.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/helpers/merge_runs.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/helpers/simplify_redlines.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/pack.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/mce/mc.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-2010.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-2012.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-2018.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-cex-2018.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-cid-2016.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-symex-2015.xsd create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/soffice.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/unpack.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validate.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/__init__.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/base.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/docx.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/pptx.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/redlining.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/ppt_to_pic.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/thumbnail.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/vision_qwen.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/web_search.py create mode 100644 libs/hexagent_demo/backend/skills/pptx-plus-linux/skill.md diff --git a/libs/hexagent_demo/backend/skills/email-mail-master/LICENSE.txt b/libs/hexagent_demo/backend/skills/email-mail-master/LICENSE.txt new file mode 100644 index 00000000..6d0c1da7 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/email-mail-master/LICENSE.txt @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) 2026 Mail-Master + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + diff --git a/libs/hexagent_demo/backend/skills/email-mail-master/SKILL.md b/libs/hexagent_demo/backend/skills/email-mail-master/SKILL.md new file mode 100644 index 00000000..2970d846 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/email-mail-master/SKILL.md @@ -0,0 +1,58 @@ +--- +name: email-mail-master 万能邮箱助手 +description: 通过阿里云邮箱、QQ邮箱或163邮箱等发送和接收邮件。支持发送普通邮件、带附件邮件、接收邮件、检查新邮件。当用户要求发送邮件、查看邮件、检查新邮件时使用。 + +--- + +# 邮件管理 + +通过阿里云邮箱、QQ邮箱或163邮箱等发送和接收邮件。 + +## 配置 + +编辑 `skills/email/scripts/config.json`,填写邮箱地址和授权码(非登录密码)。 + +授权码获取: +- QQ 邮箱:设置 > 账户 > 开启 IMAP/SMTP > 生成授权码 +- 163 邮箱:设置 > POP3/SMTP/IMAP > 开启服务 > 设置授权密码 + +可通过 `default_mailbox` 字段设置默认邮箱(`"qq"` 或 `"163"`)。 + +## 命令行调用 + +```bash +# 发送邮件 +python3 skills/email/scripts/mail.py send --to user@example.com --subject "主题" --content "内容" + +# 发送带附件 +python3 skills/email/scripts/mail.py send --to user@example.com --subject "报告" --content "请查收" --attach report.pdf + +# 接收最新邮件 +python3 skills/email/scripts/mail.py receive --limit 5 + +# 接收邮件(JSON 输出,推荐 AI 使用) +python3 skills/email/scripts/mail.py receive --limit 5 --json + +# 检查新邮件(最近 N 天) +python3 skills/email/scripts/mail.py check-new --since 1 + +# 检查新邮件(JSON 输出) +python3 skills/email/scripts/mail.py check-new --since 1 --json + +# 删除邮件(移到已删除文件夹,QQ邮箱可恢复) +python3 skills/email/scripts/mail.py delete --ids 123 + +# 批量删除 +python3 skills/email/scripts/mail.py delete --ids 123 124 125 + +# 彻底删除(不可恢复) +python3 skills/email/scripts/mail.py delete --ids 123 --permanent + +# 指定邮箱类型 +python3 skills/email/scripts/mail.py --mailbox 163 send --to user@example.com --subject "测试" +``` + +## 删除邮件说明 + +- QQ 邮箱(IMAP):默认移到「已删除」文件夹,可以从已删除中恢复。加 `--permanent` 彻底删除。 +- 163 邮箱(POP3):POP3 协议不支持文件夹操作,删除始终是永久的,不可恢复。 diff --git a/libs/hexagent_demo/backend/skills/email-mail-master/_meta.json b/libs/hexagent_demo/backend/skills/email-mail-master/_meta.json new file mode 100644 index 00000000..f1c5341d --- /dev/null +++ b/libs/hexagent_demo/backend/skills/email-mail-master/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn7bb6tkjndgyzp6fh2vgvagq982ee7c", + "slug": "email-mail-master", + "version": "1.0.0", + "publishedAt": 1772888109581 +} \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/email-mail-master/scripts/email_manager.py b/libs/hexagent_demo/backend/skills/email-mail-master/scripts/email_manager.py new file mode 100644 index 00000000..1b6bcd95 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/email-mail-master/scripts/email_manager.py @@ -0,0 +1,557 @@ +"""邮箱管理核心模块""" +import imaplib +import poplib +import smtplib +import email +from email.header import decode_header, Header +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from datetime import datetime, timedelta +import json +import os +from typing import List, Dict, Optional + + +class EmailManager: + """邮箱管理器基类""" + + def __init__(self, email_address: str, password: str, + imap_server: str, imap_port: int, + smtp_server: str, smtp_port: int): + self.email_address = email_address + self.password = password + self.imap_server = imap_server + self.imap_port = imap_port + self.smtp_server = smtp_server + self.smtp_port = smtp_port + + def decode_str(self, s): + """解码邮件头部字符串""" + if s is None: + return "" + + value, charset = decode_header(s)[0] + if charset: + try: + value = value.decode(charset) + except: + try: + value = value.decode('utf-8', errors='ignore') + except: + value = str(value) + elif isinstance(value, bytes): + try: + value = value.decode('utf-8', errors='ignore') + except: + value = str(value) + return value + + def get_email_content(self, msg): + """获取邮件正文内容(仅纯文本,HTML 转纯文本)""" + import re + from html import unescape + + content = "" + + if msg.is_multipart(): + # 优先查找 text/plain 部分 + for part in msg.walk(): + content_type = part.get_content_type() + if content_type == 'text/plain': + try: + payload = part.get_payload(decode=True) + charset = part.get_content_charset() or 'utf-8' + content = payload.decode(charset, errors='ignore') + break + except: + pass + + # 如果没有纯文本,尝试从 HTML 提取 + if not content: + for part in msg.walk(): + content_type = part.get_content_type() + if content_type == 'text/html': + try: + payload = part.get_payload(decode=True) + charset = part.get_content_charset() or 'utf-8' + html_content = payload.decode(charset, errors='ignore') + content = self._html_to_text(html_content) + break + except: + pass + else: + # 单部分邮件 + try: + payload = msg.get_payload(decode=True) + charset = msg.get_content_charset() or 'utf-8' + raw_content = payload.decode(charset, errors='ignore') + + # 根据 Content-Type 处理 + if msg.get_content_type() == 'text/html': + content = self._html_to_text(raw_content) + else: + content = raw_content + except: + pass + + return content.strip() + + def _html_to_text(self, html_content: str) -> str: + """将 HTML 转换为纯文本""" + import re + from html import unescape + + # 移除 script 和 style 标签及其内容 + text = re.sub(r']*>.*?', '', html_content, flags=re.DOTALL | re.IGNORECASE) + text = re.sub(r']*>.*?', '', text, flags=re.DOTALL | re.IGNORECASE) + + # 移除所有 HTML 标签 + text = re.sub(r'<[^>]+>', ' ', text) + + # 解码 HTML 实体 + text = unescape(text) + + # 清理多余空白 + text = re.sub(r'\s+', ' ', text) + text = re.sub(r'\n\s*\n', '\n', text) + + return text.strip() + + def receive_emails(self, mailbox: str = 'INBOX', limit: int = 10) -> List[Dict]: + """接收邮件""" + try: + mail = imaplib.IMAP4_SSL(self.imap_server, self.imap_port) + mail.login(self.email_address, self.password) + mail.select(mailbox) + + status, messages = mail.search(None, 'ALL') + email_ids = messages[0].split() + + emails = [] + for email_id in email_ids[-limit:]: + status, msg_data = mail.fetch(email_id, '(RFC822)') + + for response_part in msg_data: + if isinstance(response_part, tuple): + msg = email.message_from_bytes(response_part[1]) + + subject = self.decode_str(msg.get('Subject', '')) + from_ = self.decode_str(msg.get('From', '')) + date = msg.get('Date', '') + content = self.get_email_content(msg) + + emails.append({ + 'id': email_id.decode(), + 'subject': subject, + 'from': from_, + 'date': date, + 'content': content[:200] + '...' if len(content) > 200 else content + }) + + mail.close() + mail.logout() + + return emails + + except Exception as e: + raise Exception(f"接收邮件失败: {str(e)}") + + def receive_emails_since(self, since_date: datetime, mailbox: str = 'INBOX') -> List[Dict]: + """接收指定日期之后的邮件""" + try: + from email.utils import parsedate_to_datetime + + mail = imaplib.IMAP4_SSL(self.imap_server, self.imap_port) + mail.login(self.email_address, self.password) + mail.select(mailbox) + + # IMAP SINCE 精度只到天,需要客户端二次过滤 + date_str = since_date.strftime('%d-%b-%Y') + status, messages = mail.search(None, f'(SINCE {date_str})') + email_ids = messages[0].split() + + emails = [] + for email_id in email_ids: + status, msg_data = mail.fetch(email_id, '(RFC822)') + + for response_part in msg_data: + if isinstance(response_part, tuple): + msg = email.message_from_bytes(response_part[1]) + + # 客户端精确过滤:解析邮件日期,跳过早于 since_date 的 + raw_date = msg.get('Date', '') + try: + email_dt = parsedate_to_datetime(raw_date) + # 统一为 naive datetime 比较(去掉时区信息) + if email_dt.tzinfo: + email_dt = email_dt.replace(tzinfo=None) + if email_dt < since_date: + continue + except Exception: + pass # 无法解析日期的邮件仍然保留 + + subject = self.decode_str(msg.get('Subject', '')) + from_ = self.decode_str(msg.get('From', '')) + content = self.get_email_content(msg) + + emails.append({ + 'id': email_id.decode(), + 'subject': subject, + 'from': from_, + 'date': raw_date, + 'content': content[:200] + '...' if len(content) > 200 else content + }) + + mail.close() + mail.logout() + + return emails + + except Exception as e: + raise Exception(f"接收邮件失败: {str(e)}") + + def send_email(self, to_addr: str, subject: str, content: str, + content_type: str = 'plain', attachments: List[str] = None) -> str: + """发送邮件 + + Args: + to_addr: 收件人邮箱 + subject: 邮件主题 + content: 邮件内容 + content_type: 内容类型 ('plain' 或 'html') + attachments: 附件文件路径列表 + + Returns: + 发送结果消息 + """ + try: + from email.mime.application import MIMEApplication + import os + + message = MIMEMultipart() + message['From'] = Header(self.email_address) + message['To'] = Header(to_addr) + message['Subject'] = Header(subject, 'utf-8') + + # 添加邮件正文 + message.attach(MIMEText(content, content_type, 'utf-8')) + + # 添加附件 + if attachments: + for file_path in attachments: + if not os.path.exists(file_path): + raise FileNotFoundError(f"附件文件不存在: {file_path}") + + with open(file_path, 'rb') as f: + attachment = MIMEApplication(f.read()) + filename = os.path.basename(file_path) + attachment.add_header( + 'Content-Disposition', + 'attachment', + filename=('utf-8', '', filename) + ) + message.attach(attachment) + + server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) + server.login(self.email_address, self.password) + server.sendmail(self.email_address, [to_addr], message.as_string()) + server.quit() + + result = "邮件发送成功!" + if attachments: + result += f" (包含 {len(attachments)} 个附件)" + return result + + except Exception as e: + raise Exception(f"发送邮件失败: {str(e)}") + + def delete_email(self, email_id: str, mailbox: str = 'INBOX', permanent: bool = False) -> str: + """删除邮件(IMAP) + + 默认移到「已删除」文件夹(可在30天内恢复),permanent=True 则彻底删除(不可恢复)。 + + Args: + email_id: 邮件 ID(receive_emails 返回的 id 字段) + mailbox: 邮箱文件夹,默认 INBOX + permanent: 是否彻底删除(expunge) + - False: 移到"已删除"文件夹(可恢复) + - True: 彻底删除(不可恢复) + + Returns: + 操作结果消息 + + 注意: + - QQ邮箱: permanent=False 时邮件移到"已删除"文件夹,可在30天内恢复 + - 163邮箱: 使用POP3协议,删除操作始终是永久的 + """ + try: + mail = imaplib.IMAP4_SSL(self.imap_server, self.imap_port) + mail.login(self.email_address, self.password) + mail.select(mailbox) + + # 标记为删除 + mail.store(email_id.encode(), '+FLAGS', '\\Deleted') + + if permanent: + mail.expunge() + result = f"✓ 邮件 {email_id} 已彻底删除(不可恢复)" + else: + result = f"✓ 邮件 {email_id} 已移到已删除文件夹(可在30天内从已删除文件夹恢复)" + + mail.close() + mail.logout() + return result + + except Exception as e: + raise Exception(f"删除邮件失败: {str(e)}") + + def delete_emails_batch(self, email_ids: List[str], mailbox: str = 'INBOX', permanent: bool = False) -> str: + """批量删除邮件(IMAP) + + Args: + email_ids: 邮件 ID 列表 + mailbox: 邮箱文件夹,默认 INBOX + permanent: 是否彻底删除 + - False: 标记为删除,移到"已删除"文件夹(可恢复) + - True: 彻底删除,立即从服务器移除(不可恢复) + + Returns: + 操作结果消息 + + 注意: + - QQ邮箱: permanent=False 时邮件移到"已删除"文件夹,可在30天内恢复 + - 163邮箱: 使用POP3协议,删除操作始终是永久的 + """ + try: + mail = imaplib.IMAP4_SSL(self.imap_server, self.imap_port) + mail.login(self.email_address, self.password) + mail.select(mailbox) + + # 批量标记为删除 + for eid in email_ids: + mail.store(eid.encode(), '+FLAGS', '\\Deleted') + + if permanent: + mail.expunge() + action = "彻底删除(不可恢复)" + else: + action = "移到已删除文件夹(可在30天内恢复)" + + mail.close() + mail.logout() + + return f"✓ 已{action} {len(email_ids)} 封邮件" + + except Exception as e: + raise Exception(f"批量删除邮件失败: {str(e)}") + + +class QQEmailManager(EmailManager): + """QQ 邮箱管理器 - 使用 IMAP""" + + def __init__(self, email_address: str, auth_code: str): + super().__init__( + email_address=email_address, + password=auth_code, + imap_server='imap.qq.com', + imap_port=993, + smtp_server='smtp.qq.com', + smtp_port=465 + ) + + +class Email163Manager(EmailManager): + """163 邮箱管理器 - 使用 POP3(因为 IMAP 有安全限制)""" + + def __init__(self, email_address: str, auth_password: str): + super().__init__( + email_address=email_address, + password=auth_password, + imap_server='pop.163.com', # 使用 POP3 + imap_port=995, + smtp_server='smtp.163.com', + smtp_port=465 + ) + self.pop_server = 'pop.163.com' + self.pop_port = 995 + + def receive_emails(self, mailbox: str = 'INBOX', limit: int = 10) -> List[Dict]: + """接收邮件 - 使用 POP3""" + try: + # 连接 POP3 + pop = poplib.POP3_SSL(self.pop_server, self.pop_port) + pop.user(self.email_address) + pop.pass_(self.password) + + # 获取邮件数量 + num_messages = len(pop.list()[1]) + + emails = [] + # 获取最新的 limit 封邮件 + start = max(1, num_messages - limit + 1) + for i in range(start, num_messages + 1): + try: + response, lines, octets = pop.retr(i) + + # 解析邮件 + msg_content = b'\r\n'.join(lines) + msg = email.message_from_bytes(msg_content) + + subject = self.decode_str(msg.get('Subject', '')) + from_ = self.decode_str(msg.get('From', '')) + date = msg.get('Date', '') + content = self.get_email_content(msg) + + emails.append({ + 'id': str(i), + 'subject': subject, + 'from': from_, + 'date': date, + 'content': content + }) + except Exception as e: + # 跳过无法解析的邮件 + continue + + pop.quit() + return emails + + except Exception as e: + raise Exception(f"接收邮件失败: {str(e)}") + + def receive_emails_since(self, since_date: datetime, mailbox: str = 'INBOX') -> List[Dict]: + """接收指定日期之后的邮件 - 使用 POP3""" + try: + # 连接 POP3 + pop = poplib.POP3_SSL(self.pop_server, self.pop_port) + pop.user(self.email_address) + pop.pass_(self.password) + + # 获取邮件数量 + num_messages = len(pop.list()[1]) + + emails = [] + # 从最新的邮件开始检查 + for i in range(num_messages, 0, -1): + try: + response, lines, octets = pop.retr(i) + + # 解析邮件 + msg_content = b'\r\n'.join(lines) + msg = email.message_from_bytes(msg_content) + + # 解析日期 + date_str = msg.get('Date', '') + try: + from email.utils import parsedate_to_datetime + email_date = parsedate_to_datetime(date_str) + + # 如果邮件日期早于指定日期,停止检查 + if email_date < since_date: + break + except: + # 如果无法解析日期,跳过 + continue + + subject = self.decode_str(msg.get('Subject', '')) + from_ = self.decode_str(msg.get('From', '')) + content = self.get_email_content(msg) + + emails.append({ + 'id': str(i), + 'subject': subject, + 'from': from_, + 'date': date_str, + 'content': content + }) + except Exception as e: + # 跳过无法解析的邮件 + continue + + pop.quit() + # 反转列表,使最新的邮件在最后 + return list(reversed(emails)) + + except Exception as e: + raise Exception(f"接收邮件失败: {str(e)}") + + def delete_email(self, email_id: str, mailbox: str = 'INBOX', permanent: bool = False) -> str: + """删除邮件(POP3) + + 注意:POP3 协议的删除始终是永久的,不可恢复。 + permanent 参数对 POP3 无效,仅为保持接口一致性。 + """ + try: + pop = poplib.POP3_SSL(self.pop_server, self.pop_port) + pop.user(self.email_address) + pop.pass_(self.password) + + pop.dele(int(email_id)) + pop.quit() + + return f"✓ 邮件 {email_id} 已永久删除(POP3协议不支持恢复,已从服务器移除)" + + except Exception as e: + raise Exception(f"删除邮件失败: {str(e)}") + + def delete_emails_batch(self, email_ids: List[str], mailbox: str = 'INBOX', permanent: bool = False) -> str: + """批量删除邮件(POP3) + + 注意:POP3 协议的删除始终是永久的,不可恢复。 + permanent 参数对 POP3 无效,仅为保持接口一致性。 + """ + try: + pop = poplib.POP3_SSL(self.pop_server, self.pop_port) + pop.user(self.email_address) + pop.pass_(self.password) + + for eid in email_ids: + pop.dele(int(eid)) + + pop.quit() + return f"✓ 已永久删除 {len(email_ids)} 封邮件(POP3协议不支持恢复,已从服务器移除)" + + except Exception as e: + raise Exception(f"批量删除邮件失败: {str(e)}") + + +def load_config(config_path: str = None) -> Dict: + """加载配置文件""" + if config_path is None: + config_path = os.path.join(os.path.dirname(__file__), 'config.json') + + if not os.path.exists(config_path): + raise FileNotFoundError( + f"配置文件不存在: {config_path}\n" + f"请复制 config.json.example 为 config.json 并填写您的邮箱信息" + ) + + with open(config_path, 'r', encoding='utf-8') as f: + return json.load(f) + + +def save_config(config: Dict, config_path: str = None): + """保存配置文件""" + if config_path is None: + config_path = os.path.join(os.path.dirname(__file__), 'config.json') + + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(config, f, ensure_ascii=False, indent=2) + + +def get_email_manager(email_type: str, config: Dict) -> EmailManager: + """根据邮箱类型获取管理器""" + if email_type == 'qq': + email_config = config['qq_email'] + return QQEmailManager( + email_address=email_config['email'], + auth_code=email_config['auth_code'] + ) + elif email_type == '163': + email_config = config['163_email'] + return Email163Manager( + email_address=email_config['email'], + auth_password=email_config['auth_password'] + ) + else: + raise ValueError(f"不支持的邮箱类型: {email_type},请使用 'qq' 或 '163'") diff --git a/libs/hexagent_demo/backend/skills/email-mail-master/scripts/mail.py b/libs/hexagent_demo/backend/skills/email-mail-master/scripts/mail.py new file mode 100644 index 00000000..dc7a8b53 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/email-mail-master/scripts/mail.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +"""非交互式邮件管理脚本""" +import argparse +import json +import sys +from datetime import datetime, timedelta +from email_manager import load_config, get_email_manager + + +def cmd_send(args): + """发送邮件""" + try: + config = load_config() + manager = get_email_manager(args.mailbox, config) + + attachments = args.attach if args.attach else None + + manager.send_email( + to_addr=args.to, + subject=args.subject, + content=args.content, + attachments=attachments + ) + + print(f"邮件已发送到 {args.to}") + if attachments: + print(f" 附件: {', '.join(attachments)}") + + except Exception as e: + print(f"发送失败: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_receive(args): + """接收邮件""" + try: + config = load_config() + manager = get_email_manager(args.mailbox, config) + + emails = manager.receive_emails(limit=args.limit) + + if not emails: + if args.json: + print(json.dumps({"count": 0, "emails": []}, ensure_ascii=False)) + else: + print("收件箱为空") + return + + if args.json: + print(json.dumps({ + "count": len(emails), + "emails": emails + }, ensure_ascii=False, indent=2)) + return + + print(f"收到 {len(emails)} 封邮件:\n") + for i, e in enumerate(emails, 1): + print(f"[{i}] {e['subject']}") + print(f" 发件人: {e['from']}") + print(f" 日期: {e['date']}") + print(f" 内容: {e['content'][:100]}...") + print() + + except Exception as e: + print(f"接收失败: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_check_new(args): + """检查新邮件""" + try: + config = load_config() + manager = get_email_manager(args.mailbox, config) + + since_date = datetime.now() - timedelta(days=args.since) + new_emails = manager.receive_emails_since(since_date) + + if not new_emails: + if args.json: + print(json.dumps({"count": 0, "since_days": args.since, "emails": []}, ensure_ascii=False)) + else: + print(f"没有新邮件(最近 {args.since} 天)") + return + + if args.json: + print(json.dumps({ + "count": len(new_emails), + "since_days": args.since, + "emails": [ + {"subject": e['subject'], "from": e['from'], "date": e['date']} + for e in new_emails + ] + }, ensure_ascii=False, indent=2)) + return + + print(f"找到 {len(new_emails)} 封新邮件(最近 {args.since} 天):\n") + for i, e in enumerate(new_emails, 1): + print(f"[{i}] {e['subject']}") + print(f" 发件人: {e['from']}") + print(f" 日期: {e['date']}") + print() + + except Exception as e: + print(f"检查失败: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_delete(args): + """删除邮件""" + try: + config = load_config() + manager = get_email_manager(args.mailbox, config) + + email_ids = args.ids + + if len(email_ids) == 1: + result = manager.delete_email(email_ids[0], permanent=args.permanent) + else: + result = manager.delete_emails_batch(email_ids, permanent=args.permanent) + + print(result) + + except Exception as e: + print(f"删除失败: {e}", file=sys.stderr) + sys.exit(1) + + +def main(): + # 加载配置以获取默认邮箱 + try: + config = load_config() + default_mailbox = config.get('default_mailbox', 'qq') + except: + default_mailbox = 'qq' + + parser = argparse.ArgumentParser( + description='邮件管理工具', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + # 发送邮件(使用默认邮箱) + %(prog)s send --to user@example.com --subject "Hello" --content "Test" + + # 发送带附件 + %(prog)s send --to user@example.com --subject "Report" --content "See file" --attach report.pdf + + # 接收最新 5 封邮件 + %(prog)s receive --limit 5 + + # 检查最近 2 天的新邮件 + %(prog)s check-new --since 2 + + # 删除邮件(移到已删除文件夹,可在30天内恢复) + %(prog)s delete --ids 123 + + # 批量删除(移到已删除文件夹) + %(prog)s delete --ids 123 124 125 + + # 彻底删除(不可恢复,立即从服务器移除) + %(prog)s delete --ids 123 --permanent + + # 批量彻底删除 + %(prog)s delete --ids 123 124 125 --permanent + + # 使用 163 邮箱 + %(prog)s send --mailbox 163 --to user@example.com --subject "Test" +""" + ) + + parser.add_argument( + '--mailbox', + choices=['qq', '163'], + default=default_mailbox, + help=f'邮箱类型 (默认: {default_mailbox},可在 config.json 中修改 default_mailbox)' + ) + + subparsers = parser.add_subparsers(dest='command', help='命令') + + # send 命令 + send_parser = subparsers.add_parser('send', help='发送邮件') + send_parser.add_argument('--to', required=True, help='收件人邮箱') + send_parser.add_argument('--subject', required=True, help='邮件主题') + send_parser.add_argument('--content', required=True, help='邮件内容') + send_parser.add_argument('--attach', nargs='+', help='附件文件路径') + + # receive 命令 + receive_parser = subparsers.add_parser('receive', help='接收邮件') + receive_parser.add_argument('--limit', type=int, default=10, help='接收数量 (默认: 10)') + receive_parser.add_argument('--json', action='store_true', help='JSON 格式输出') + + # check-new 命令 + check_parser = subparsers.add_parser('check-new', help='检查新邮件') + check_parser.add_argument('--since', type=int, default=1, help='检查最近 N 天 (默认: 1)') + check_parser.add_argument('--json', action='store_true', help='JSON 格式输出') + + # delete 命令 + delete_parser = subparsers.add_parser('delete', help='删除邮件') + delete_parser.add_argument('--ids', nargs='+', required=True, + help='要删除的邮件 ID(可指定多个,用空格分隔)') + delete_parser.add_argument('--permanent', action='store_true', + help='彻底删除(不可恢复)。不指定此参数时,邮件将移到已删除文件夹,可在30天内恢复。注意:163邮箱使用POP3协议,删除始终是永久的') + + args = parser.parse_args() + + if not args.command: + parser.print_help() + sys.exit(1) + + # 执行命令 + if args.command == 'send': + cmd_send(args) + elif args.command == 'receive': + cmd_receive(args) + elif args.command == 'check-new': + cmd_check_new(args) + elif args.command == 'delete': + cmd_delete(args) + + +if __name__ == '__main__': + main() diff --git a/libs/hexagent_demo/backend/skills/email-mail-master/scripts/requirements.txt b/libs/hexagent_demo/backend/skills/email-mail-master/scripts/requirements.txt new file mode 100644 index 00000000..47365ab0 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/email-mail-master/scripts/requirements.txt @@ -0,0 +1,2 @@ +# 邮箱管理所需的 Python 包 +# 注意: imaplib, smtplib, email 是 Python 标准库,无需安装 diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/LICENSE.txt b/libs/hexagent_demo/backend/skills/pptx-plus-linux/LICENSE.txt new file mode 100644 index 00000000..81c1c957 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/LICENSE.txt @@ -0,0 +1,14 @@ +© 2026 元景, PBC. 保留所有权利。 + +额外限制:尽管协议中有任何相反规定,用户不得: + +- 从服务中提取这些资料或在服务之外保留这些资料的副本 +- 复制或拷贝这些资料,但在授权使用服务期间自动创建的临时副本除外 +- 基于这些资料创作衍生作品 +- 向任何第三方分发、再许可或转让这些资料 +- 制造、许诺销售、销售或进口这些资料中体现的任何发明 +- 对这些资料进行逆向工程、反编译或反汇编 + +接收、查看或持有这些资料并不授予或暗示任何超出上述明确授予的许可或权利。 + +元景保留这些资料的所有权利、所有权和利益,包括所有版权、专利和其他知识产权。 diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/editing.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/editing.md new file mode 100644 index 00000000..66dcf7e2 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/editing.md @@ -0,0 +1,205 @@ +# 编辑演示文稿 + +## 基于模板的工作流 + +使用现有演示文稿作为模板时: + +1. **分析现有幻灯片**: + ```bash + python scripts/thumbnail.py template.pptx + python -m markitdown template.pptx + ``` + 查看 `thumbnails.jpg` 了解布局,查看 markitdown 输出了解占位符文本。 + +2. **规划幻灯片映射**:为每个内容部分选择一个模板幻灯片。 + + ⚠️ **使用多样化的布局** —— 布局单调是常见的失败模式。不要默认使用基本的标题 + 列表幻灯片。主动寻找: + - 多栏布局(双栏、三栏) + - 图片 + 文字组合 + - 全出血图片配文字覆盖 + - 引用或强调幻灯片 + - 章节分隔页 + - 数据/数字突出显示 + - 图标网格或图标 + 文字行 + + **避免:** 每张幻灯片重复使用相同的文字密集型布局。 + + 将内容类型与布局风格匹配(如:要点 → 列表幻灯片,团队信息 → 多栏,证言 → 引用幻灯片)。 + +3. **解包**:`python scripts/office/unpack.py template.pptx unpacked/` + +4. **构建演示文稿**(自己完成,不要使用子代理): + - 删除不需要的幻灯片(从 `` 中移除) + - 复制要重用的幻灯片(`add_slide.py`) + - 在 `` 中重新排序幻灯片 + - **在步骤 5 之前完成所有结构性更改** + +5. **编辑内容**:更新每个 `slide{N}.xml` 中的文本。 + **如果可用,在此处使用子代理** —— 幻灯片是独立的 XML 文件,所以子代理可以并行编辑。 + +6. **清理**:`python scripts/clean.py unpacked/` + +7. **打包**:`python scripts/office/pack.py unpacked/ output.pptx --original template.pptx` + +--- + +## 脚本 + +| 脚本 | 用途 | +|------|------| +| `unpack.py` | 解压并格式化 PPTX | +| `add_slide.py` | 复制幻灯片或从布局创建 | +| `clean.py` | 删除孤立文件 | +| `pack.py` | 验证后重新打包 | +| `thumbnail.py` | 创建幻灯片可视化网格 | + +### unpack.py + +```bash +python scripts/office/unpack.py input.pptx unpacked/ +``` + +解压 PPTX,格式化 XML,转义智能引号。 + +### add_slide.py + +```bash +python scripts/add_slide.py unpacked/ slide2.xml # 复制幻灯片 +python scripts/add_slide.py unpacked/ slideLayout2.xml # 从布局创建 +``` + +打印要添加到 `` 中所需位置的 ``。 + +### clean.py + +```bash +python scripts/clean.py unpacked/ +``` + +删除不在 `` 中的幻灯片、未引用的媒体、孤立的关系文件。 + +### pack.py + +```bash +python scripts/office/pack.py unpacked/ output.pptx --original input.pptx +``` + +验证、修复、压缩 XML、重新编码智能引号。 + +### thumbnail.py + +```bash +python scripts/thumbnail.py input.pptx [output_prefix] [--cols N] +``` + +创建 `thumbnails.jpg`,以幻灯片文件名作为标签。默认 3 列,每网格最多 12 张。 + +**仅用于模板分析**(选择布局)。对于可视化 QA,使用 `soffice` + `pdftoppm` 创建全分辨率单张幻灯片图片 —— 见 SKILL.md。 + +--- + +## 幻灯片操作 + +幻灯片顺序在 `ppt/presentation.xml` → `` 中。 + +**重新排序**:重新排列 `` 元素。 + +**删除**:移除 ``,然后运行 `clean.py`。 + +**添加**:使用 `add_slide.py`。永远不要手动复制幻灯片文件 —— 脚本会处理手动复制会遗漏的备注引用、Content_Types.xml 和关系 ID。 + +--- + +## 编辑内容 + +**子代理:** 如果可用,在此处使用(完成步骤 4 后)。每张幻灯片是独立的 XML 文件,所以子代理可以并行编辑。在给子代理的提示中包含: +- 要编辑的幻灯片文件路径 +- **"所有更改使用 Edit 工具"** +- 下面的格式规则和常见陷阱 + +对于每张幻灯片: +1. 读取幻灯片的 XML +2. 识别所有占位符内容 —— 文本、图片、图表、图标、说明文字 +3. 用最终内容替换每个占位符 + +**使用 Edit 工具,而不是 sed 或 Python 脚本。** Edit 工具强制明确要替换什么和在哪里替换,从而提供更好的可靠性。 + +### 格式规则 + +- **所有标题、副标题和行内标签加粗**:在 `` 上使用 `b="1"`。包括: + - 幻灯片标题 + - 幻灯片内的章节标题 + - 行首的行内标签(如:"状态:"、"描述:") +- **永远不要使用 unicode 项目符号(•)**:使用正确的列表格式 `` 或 `` +- **项目符号一致性**:让项目符号从布局继承。只指定 `` 或 ``。 + +--- + +## 常见陷阱 + +### 模板适配 + +当源内容项目少于模板时: +- **完全删除多余元素**(图片、形状、文本框),不要只清除文本 +- 清除文本内容后检查孤立的视觉元素 +- 进行可视化 QA 以发现数量不匹配 + +用不同长度的内容替换文本时: +- **更短的替换**:通常安全 +- **更长的替换**:可能溢出或意外换行 +- 文本更改后用可视化 QA 测试 +- 考虑截断或拆分内容以适应模板的设计约束 + +**模板槽位 ≠ 源项目**:如果模板有 4 个团队成员但源有 3 个用户,删除第 4 个成员的整个组(图片 + 文本框),而不只是文本。 + +### 多项内容 + +如果源有多项内容(编号列表、多个部分),为每项创建单独的 `` 元素 —— **永远不要连接成一个字符串**。 + +**❌ 错误** —— 所有项目在一个段落中: +```xml + + 步骤 1:做第一件事。步骤 2:做第二件事。 + +``` + +**✅ 正确** —— 分开的段落配粗体标题: +```xml + + + 步骤 1 + + + + 做第一件事。 + + + + 步骤 2 + + +``` + +从原始段落复制 `` 以保留行间距。在标题上使用 `b="1"`。 + +### 智能引号 + +由 unpack/pack 自动处理。但 Edit 工具会将智能引号转换为 ASCII。 + +**添加带引号的新文本时,使用 XML 实体:** + +```xml +the “Agreement” +``` + +| 字符 | 名称 | Unicode | XML 实体 | +|------|------|---------|----------| +| `"` | 左双引号 | U+201C | `“` | +| `"` | 右双引号 | U+201D | `”` | +| `'` | 左单引号 | U+2018 | `‘` | +| `'` | 右单引号 | U+2019 | `’` | + +### 其他 + +- **空白**:在有前导/尾随空格的 `` 上使用 `xml:space="preserve"` +- **XML 解析**:使用 `defusedxml.minidom`,而非 `xml.etree.ElementTree`(会破坏命名空间) diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/examin.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/examin.md new file mode 100644 index 00000000..b3ed0249 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/examin.md @@ -0,0 +1,117 @@ +# PPTX Plus Examination & QA (Linux) + +## 可视化工具(PPT 转图片) + +将 PPTX 幻灯片转换为图片以便进行可视化检查和 AI 审核。 + +```bash +# 将目录中所有 PPTX 文件转换为图片 +python scripts/ppt_to_pic.py --ppt-dir ./ppt --output-dir ./images + +# 将单个 PPTX 文件转换为图片 +python scripts/ppt_to_pic.py --file presentation.pptx --output ./images + +# 输出:为每个 PPTX 的幻灯片创建 JPG 图片 +``` + +### Qwen Vision 图片描述 + +使用 Qwen Vision 分析和描述图片: + +```bash +# 描述单张图片 +python scripts/vision_qwen.py --image path/to/image.png --prompt "描述这张幻灯片" + +# 批量描述多张图片(每批最多 5 张) +python scripts/vision_qwen.py --images img1.png img2.png img3.png +``` + +**使用场景:** +- 分析生成的幻灯片进行可视化 QA +- 描述参考图片获取设计灵感 +- 从截图中提取文本和布局信息 + +--- + +## QA(必需) + +**假设存在问题。你的任务是找出它们。** + +你的第一次渲染几乎从来不是正确的。将 QA 视为 bug 狩猎,而非确认步骤。如果在第一次检查时没有发现问题,说明你检查得不够仔细。 + +### 内容 QA + +```bash +python -m markitdown output.pptx +``` + +检查缺失内容、错别字、错误顺序。 + +**使用模板时,检查残留的占位符文本:** + +```bash +python -m markitdown output.pptx | grep -iE "xxxx|lorem|ipsum|this.*(page|slide).*layout" +``` + +如果 grep 返回结果,在声明成功前修复它们。 + +### 可视化 QA + +**⚠️ 使用子代理** —— 即使只有 2-3 张幻灯片。你一直在盯着代码,会看到你期望看到的内容,而不是实际存在的内容。子代理有全新的视角。 + +将幻灯片转换为图片(见[转换为图片](#转换为图片)),然后使用此提示: + +``` +可视化检查这些幻灯片。假设存在问题 —— 找出它们。 + +查找: +- 重叠元素(文字穿过形状、线条穿过文字、堆叠元素) +- 文字溢出或在边缘/框边界处被截断 +- 为单行文本定位的装饰线,但标题换行成了两行 +- 来源引用或页脚与上方内容冲突 +- 元素过近(< 0.3" 间隙)或卡片/区块几乎接触 +- 间隙不均匀(一处大面积空白,另一处拥挤) +- 距幻灯片边缘边距不足(< 0.5") +- 列或类似元素未一致对齐 +- 低对比度文字(如奶油色背景上的浅灰色文字) +- 低对比度图标(如深色背景上的深色图标,没有对比色圆圈) +- 文本框过窄导致过度换行 +- 残留的占位符内容 + +对于每张幻灯片,列出问题或关注点,即使是次要的。 + +读取并分析这些图片: +1. /path/to/slide-01.jpg(预期:[简要描述]) +2. /path/to/slide-02.jpg(预期:[简要描述]) + +报告发现的所有问题,包括次要问题。 +``` + +### 验证循环 + +1. 生成幻灯片 → 转换为图片 → 检查 +2. **列出发现的问题**(如果没有发现问题,更批判性地再次查看) +3. 修复问题 +4. **重新验证受影响的幻灯片** —— 一个修复经常会引发另一个问题 +5. 重复直到完整检查没有发现新问题 + +**在完成至少一次修复-验证循环之前,不要声明成功。** + +--- + +## 转换为图片 + +将演示文稿转换为单独的幻灯片图片以便进行可视化检查: + +```bash +python scripts/office/soffice.py --headless --convert-to pdf output.pptx +pdftoppm -jpeg -r 150 output.pdf slide +``` + +这将创建 `slide-01.jpg`、`slide-02.jpg` 等。 + +修复后重新渲染特定幻灯片: + +```bash +pdftoppm -jpeg -r 150 -f N -l N output.pdf slide-fixed +``` diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/pptxgenjs.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/pptxgenjs.md new file mode 100644 index 00000000..375607e4 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/pptxgenjs.md @@ -0,0 +1,422 @@ +# PptxGenJS 教程 + +## 设置与基本结构 + +```javascript +const pptxgen = require("pptxgenjs"); + +let pres = new pptxgen(); +pres.layout = 'LAYOUT_16x9'; // 或 'LAYOUT_16x10', 'LAYOUT_4x3', 'LAYOUT_WIDE' +pres.author = 'Your Name'; +pres.title = 'Presentation Title'; + +let slide = pres.addSlide(); +slide.addText("Hello World!", { x: 0.5, y: 0.5, fontSize: 36, color: "363636" }); + +pres.writeFile({ fileName: "Presentation.pptx" }); +``` + +## 布局尺寸 + +幻灯片尺寸(坐标单位为英寸): +- `LAYOUT_16x9`:10" × 5.625"(默认) +- `LAYOUT_16x10`:10" × 6.25" +- `LAYOUT_4x3`:10" × 7.5" +- `LAYOUT_WIDE`:13.3" × 7.5" + +--- + +## 文本与格式 + +```javascript +// 基本文本 +slide.addText("Simple Text", { + x: 1, y: 1, w: 8, h: 2, fontSize: 24, fontFace: "Arial", + color: "363636", bold: true, align: "center", valign: "middle" +}); + +// 字符间距(使用 charSpacing,不是 letterSpacing,后者会被静默忽略) +slide.addText("SPACED TEXT", { x: 1, y: 1, w: 8, h: 1, charSpacing: 6 }); + +// 富文本数组 +slide.addText([ + { text: "Bold ", options: { bold: true } }, + { text: "Italic ", options: { italic: true } } +], { x: 1, y: 3, w: 8, h: 1 }); + +// 多行文本(需要 breakLine: true) +slide.addText([ + { text: "Line 1", options: { breakLine: true } }, + { text: "Line 2", options: { breakLine: true } }, + { text: "Line 3" } // 最后一项不需要 breakLine +], { x: 0.5, y: 0.5, w: 8, h: 2 }); + +// 文本框边距(内边距) +slide.addText("Title", { + x: 0.5, y: 0.3, w: 9, h: 0.6, + margin: 0 // 当需要文本与其他元素(如形状或图标)精确对齐时使用 0 +}); +``` + +**提示:** 文本框默认有内边距。当需要文本与相同 x 位置的形状、线条或图标精确对齐时,设置 `margin: 0`。 + +--- + +## 列表与项目符号 + +```javascript +// ✅ 正确:多个项目符号 +slide.addText([ + { text: "First item", options: { bullet: true, breakLine: true } }, + { text: "Second item", options: { bullet: true, breakLine: true } }, + { text: "Third item", options: { bullet: true } } +], { x: 0.5, y: 0.5, w: 8, h: 3 }); + +// ❌ 错误:永远不要使用 unicode 项目符号 +slide.addText("• First item", { ... }); // 会创建双重项目符号 + +// 子项和编号列表 +{ text: "Sub-item", options: { bullet: true, indentLevel: 1 } } +{ text: "First", options: { bullet: { type: "number" }, breakLine: true } } +``` + +--- + +## 形状 + +```javascript +slide.addShape(pres.shapes.RECTANGLE, { + x: 0.5, y: 0.8, w: 1.5, h: 3.0, + fill: { color: "FF0000" }, line: { color: "000000", width: 2 } +}); + +slide.addShape(pres.shapes.OVAL, { x: 4, y: 1, w: 2, h: 2, fill: { color: "0000FF" } }); + +slide.addShape(pres.shapes.LINE, { + x: 1, y: 3, w: 5, h: 0, line: { color: "FF0000", width: 3, dashType: "dash" } +}); + +// 带透明度 +slide.addShape(pres.shapes.RECTANGLE, { + x: 1, y: 1, w: 3, h: 2, + fill: { color: "0088CC", transparency: 50 } +}); + +// 圆角矩形(rectRadius 仅适用于 ROUNDED_RECTANGLE,不适用于 RECTANGLE) +// ⚠️ 不要与矩形强调叠加层配对 —— 它们无法覆盖圆角。改用 RECTANGLE。 +slide.addShape(pres.shapes.ROUNDED_RECTANGLE, { + x: 1, y: 1, w: 3, h: 2, + fill: { color: "FFFFFF" }, rectRadius: 0.1 +}); + +// 带阴影 +slide.addShape(pres.shapes.RECTANGLE, { + x: 1, y: 1, w: 3, h: 2, + fill: { color: "FFFFFF" }, + shadow: { type: "outer", color: "000000", blur: 6, offset: 2, angle: 135, opacity: 0.15 } +}); +``` + +阴影选项: + +| 属性 | 类型 | 范围 | 说明 | +|------|------|------|------| +| `type` | string | `"outer"`, `"inner"` | | +| `color` | string | 6 字符十六进制(如 `"000000"`) | 无 `#` 前缀,无 8 字符十六进制 —— 见常见陷阱 | +| `blur` | number | 0-100 pt | | +| `offset` | number | 0-200 pt | **必须非负** —— 负值会损坏文件 | +| `angle` | number | 0-359 度 | 阴影投射方向(135 = 右下,270 = 向上) | +| `opacity` | number | 0.0-1.0 | 用此控制透明度,永远不要编码到颜色字符串中 | + +要向上投射阴影(如在页脚栏上),使用 `angle: 270` 配正偏移 —— **不要**使用负偏移。 + +**注意**:渐变填充不原生支持。改用渐变图片作为背景。 + +--- + +## 图片 + +### 图片来源 + +```javascript +// 从文件路径 +slide.addImage({ path: "images/chart.png", x: 1, y: 1, w: 5, h: 3 }); + +// 从 URL +slide.addImage({ path: "https://example.com/image.jpg", x: 1, y: 1, w: 5, h: 3 }); + +// 从 base64(更快,无文件 I/O) +slide.addImage({ data: "image/png;base64,iVBORw0KGgo...", x: 1, y: 1, w: 5, h: 3 }); +``` + +### 图片选项 + +```javascript +slide.addImage({ + path: "image.png", + x: 1, y: 1, w: 5, h: 3, + rotate: 45, // 0-359 度 + rounding: true, // 圆形裁剪 + transparency: 50, // 0-100 + flipH: true, // 水平翻转 + flipV: false, // 垂直翻转 + altText: "Description", // 无障碍访问 + hyperlink: { url: "https://example.com" } +}); +``` + +### 图片尺寸模式 + +```javascript +// Contain - 适应内部,保持比例 +{ sizing: { type: 'contain', w: 4, h: 3 } } + +// Cover - 填充区域,保持比例(可能裁剪) +{ sizing: { type: 'cover', w: 4, h: 3 } } + +// Crop - 裁剪特定部分 +{ sizing: { type: 'crop', x: 0.5, y: 0.5, w: 2, h: 2 } } +``` + +### 计算尺寸(保持宽高比) + +```javascript +const origWidth = 1978, origHeight = 923, maxHeight = 3.0; +const calcWidth = maxHeight * (origWidth / origHeight); +const centerX = (10 - calcWidth) / 2; + +slide.addImage({ path: "image.png", x: centerX, y: 1.2, w: calcWidth, h: maxHeight }); +``` + +### 支持的格式 + +- **标准格式**:PNG、JPG、GIF(动态 GIF 在 Microsoft 365 中可用) +- **SVG**:在现代 PowerPoint/Microsoft 365 中可用 + +--- + +## 图标 + +使用 react-icons 生成 SVG 图标,然后转换为 PNG 以实现通用兼容性。 + +### 设置 + +```javascript +const React = require("react"); +const ReactDOMServer = require("react-dom/server"); +const sharp = require("sharp"); +const { FaCheckCircle, FaChartLine } = require("react-icons/fa"); + +function renderIconSvg(IconComponent, color = "#000000", size = 256) { + return ReactDOMServer.renderToStaticMarkup( + React.createElement(IconComponent, { color, size: String(size) }) + ); +} + +async function iconToBase64Png(IconComponent, color, size = 256) { + const svg = renderIconSvg(IconComponent, color, size); + const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer(); + return "image/png;base64," + pngBuffer.toString("base64"); +} +``` + +### 添加图标到幻灯片 + +```javascript +const iconData = await iconToBase64Png(FaCheckCircle, "#4472C4", 256); + +slide.addImage({ + data: iconData, + x: 1, y: 1, w: 0.5, h: 0.5 // 尺寸单位为英寸 +}); +``` + +**注意**:使用 256 或更高的尺寸以获得清晰的图标。size 参数控制栅格化分辨率,而不是幻灯片上的显示尺寸(由 `w` 和 `h` 以英寸为单位设置)。 + +### 图标库 + +安装:`npm install -g react-icons react react-dom sharp` + +react-icons 中流行的图标集: +- `react-icons/fa` - Font Awesome +- `react-icons/md` - Material Design +- `react-icons/hi` - Heroicons +- `react-icons/bi` - Bootstrap Icons + +--- + +## 幻灯片背景 + +```javascript +// 纯色 +slide.background = { color: "F1F1F1" }; + +// 带透明度的颜色 +slide.background = { color: "FF3399", transparency: 50 }; + +// 从 URL 加载图片 +slide.background = { path: "https://example.com/bg.jpg" }; + +// 从 base64 加载图片 +slide.background = { data: "image/png;base64,iVBORw0KGgo..." }; +``` + +--- + +## 表格 + +```javascript +slide.addTable([ + ["Header 1", "Header 2"], + ["Cell 1", "Cell 2"] +], { + x: 1, y: 1, w: 8, h: 2, + border: { pt: 1, color: "999999" }, fill: { color: "F1F1F1" } +}); + +// 高级用法:合并单元格 +let tableData = [ + [{ text: "Header", options: { fill: { color: "6699CC" }, color: "FFFFFF", bold: true } }, "Cell"], + [{ text: "Merged", options: { colspan: 2 } }] +]; +slide.addTable(tableData, { x: 1, y: 3.5, w: 8, colW: [4, 4] }); +``` + +--- + +## 图表 + +```javascript +// 柱状图 +slide.addChart(pres.charts.BAR, [{ + name: "Sales", labels: ["Q1", "Q2", "Q3", "Q4"], values: [4500, 5500, 6200, 7100] +}], { + x: 0.5, y: 0.6, w: 6, h: 3, barDir: 'col', + showTitle: true, title: 'Quarterly Sales' +}); + +// 折线图 +slide.addChart(pres.charts.LINE, [{ + name: "Temp", labels: ["Jan", "Feb", "Mar"], values: [32, 35, 42] +}], { x: 0.5, y: 4, w: 6, h: 3, lineSize: 3, lineSmooth: true }); + +// 饼图 +slide.addChart(pres.charts.PIE, [{ + name: "Share", labels: ["A", "B", "Other"], values: [35, 45, 20] +}], { x: 7, y: 1, w: 5, h: 4, showPercent: true }); +``` + +### 更美观的图表 + +默认图表看起来过时。应用以下选项以获得现代、简洁的外观: + +```javascript +slide.addChart(pres.charts.BAR, chartData, { + x: 0.5, y: 1, w: 9, h: 4, barDir: "col", + + // 自定义颜色(匹配你的演示配色方案) + chartColors: ["0D9488", "14B8A6", "5EEAD4"], + + // 简洁背景 + chartArea: { fill: { color: "FFFFFF" }, roundedCorners: true }, + + // 柔和的轴标签 + catAxisLabelColor: "64748B", + valAxisLabelColor: "64748B", + + // 细微网格(仅值轴) + valGridLine: { color: "E2E8F0", size: 0.5 }, + catGridLine: { style: "none" }, + + // 柱上的数据标签 + showValue: true, + dataLabelPosition: "outEnd", + dataLabelColor: "1E293B", + + // 单系列时隐藏图例 + showLegend: false, +}); +``` + +**关键样式选项:** +- `chartColors: [...]` - 系列/片段的十六进制颜色 +- `chartArea: { fill, border, roundedCorners }` - 图表背景 +- `catGridLine/valGridLine: { color, style, size }` - 网格线(`style: "none"` 隐藏) +- `lineSmooth: true` - 曲线(折线图) +- `legendPos: "r"` - 图例位置:"b", "t", "l", "r", "tr" + +--- + +## 幻灯片母版 + +```javascript +pres.defineSlideMaster({ + title: 'TITLE_SLIDE', background: { color: '283A5E' }, + objects: [{ + placeholder: { options: { name: 'title', type: 'title', x: 1, y: 2, w: 8, h: 2 } } + }] +}); + +let titleSlide = pres.addSlide({ masterName: "TITLE_SLIDE" }); +titleSlide.addText("My Title", { placeholder: "title" }); +``` + +--- + +## 常见陷阱 + +⚠️ 这些问题会导致文件损坏、视觉错误或输出中断。请避免。 + +1. **永远不要在十六进制颜色中使用 "#"** - 会导致文件损坏 + ```javascript + color: "FF0000" // ✅ 正确 + color: "#FF0000" // ❌ 错误 + ``` + +2. **永远不要在十六进制颜色字符串中编码透明度** - 8 字符颜色(如 `"00000020"`)会损坏文件。改用 `opacity` 属性。 + ```javascript + shadow: { type: "outer", blur: 6, offset: 2, color: "00000020" } // ❌ 损坏文件 + shadow: { type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.12 } // ✅ 正确 + ``` + +3. **使用 `bullet: true`** - 永远不要使用 unicode 符号如 "•"(会创建双重项目符号) + +4. **数组项之间使用 `breakLine: true`** 否则文本会连在一起 + +5. **项目符号避免使用 `lineSpacing`** - 会导致过大间隙;改用 `paraSpaceAfter` + +6. **每个演示文稿需要新实例** - 不要重用 `pptxgen()` 对象 + +7. **永远不要跨调用重用选项对象** - PptxGenJS 会原地修改对象(如将阴影值转换为 EMU)。在多次调用间共享一个对象会损坏第二个形状。 + ```javascript + const shadow = { type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.15 }; + slide.addShape(pres.shapes.RECTANGLE, { shadow, ... }); // ❌ 第二次调用会得到已转换的值 + slide.addShape(pres.shapes.RECTANGLE, { shadow, ... }); + + const makeShadow = () => ({ type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.15 }); + slide.addShape(pres.shapes.RECTANGLE, { shadow: makeShadow(), ... }); // ✅ 每次创建新对象 + slide.addShape(pres.shapes.RECTANGLE, { shadow: makeShadow(), ... }); + ``` + +8. **不要将 `ROUNDED_RECTANGLE` 与强调边框一起使用** - 矩形叠加条无法覆盖圆角。改用 `RECTANGLE`。 + ```javascript + // ❌ 错误:强调条无法覆盖圆角 + slide.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 1, y: 1, w: 3, h: 1.5, fill: { color: "FFFFFF" } }); + slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 0.08, h: 1.5, fill: { color: "0891B2" } }); + + // ✅ 正确:使用 RECTANGLE 实现整洁对齐 + slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 3, h: 1.5, fill: { color: "FFFFFF" } }); + slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 0.08, h: 1.5, fill: { color: "0891B2" } }); + ``` + +9. **永远不要使用中文引号(如:" ")** - 在 JavaScript 字符串中,使用标准 ASCII 引号(`' '` 或 `" "`)。中文引号会导致 PptxGenJS 崩溃或生成损坏文件。 + +--- + +## 快速参考 + +- **形状**:RECTANGLE, OVAL, LINE, ROUNDED_RECTANGLE +- **图表**:BAR, LINE, PIE, DOUGHNUT, SCATTER, BUBBLE, RADAR +- **布局**:LAYOUT_16x9 (10"×5.625"), LAYOUT_16x10, LAYOUT_4x3, LAYOUT_WIDE +- **对齐**:"left", "center", "right" +- **图表数据标签**:"outEnd", "inEnd", "center" diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_area_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_area_chart.md new file mode 100644 index 00000000..0845a814 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_area_chart.md @@ -0,0 +1,27 @@ +# generate_area_chart — 面积图 + +## 功能概述 +展示连续自变量(常为时间)下的数值趋势,可启用堆叠观察不同分组的累计贡献,适合 KPI、能源、产出等时间序列场景。 + +## 输入字段 +### 必填 +- `data`: 数组,元素包含 `time`(string)与 `value`(number),堆叠时需补充 `group`(string),至少 1 条记录。 + +### 可选 +- `stack`: boolean,默认 `false`,开启堆叠需确保每条数据都含 `group` 字段。 +- `style.backgroundColor`: string,设置图表背景色(如 `#fff`)。 +- `style.lineWidth`: number,自定义面积边界的线宽。 +- `style.palette`: string[],传入调色板数组用于系列着色。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough` 以控制手绘质感。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`,控制图表宽度。 +- `height`: number,默认 `400`,控制图表高度。 +- `title`: string,默认空字符串,用于设置图表标题。 +- `axisXTitle`: string,默认空字符串,用于设置 X 轴标题。 +- `axisYTitle`: string,默认空字符串,用于设置 Y 轴标题。 + +## 使用建议 +保证 `time` 字段格式统一(如 `YYYY-MM`);堆叠模式下各组数据需覆盖相同的时间点,可先做缺失补值。 + +## 返回结果 +- 返回图像 URL,并在 `_meta.spec` 中附带完整面积图配置,可供二次渲染或追踪。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_bar_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_bar_chart.md new file mode 100644 index 00000000..5044d535 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_bar_chart.md @@ -0,0 +1,27 @@ +# generate_bar_chart — 条形图 + +## 功能概述 +以横向条形比较不同类别或分组的指标表现,适合 Top-N 排行、不同地区或渠道对比。 + +## 输入字段 +### 必填 +- `data`: array,每条至少含 `category`(string)与 `value`(number),如需分组或堆叠需额外提供 `group`(string)。 + +### 可选 +- `group`: boolean,默认 `false`,启用后以并排形式展示不同 `group`,并要求 `stack=false` 且数据含 `group` 字段。 +- `stack`: boolean,默认 `true`,启用后将不同 `group` 堆叠在同一条形上,并要求 `group=false` 且数据含 `group` 字段。 +- `style.backgroundColor`: string,自定义背景色(如 `#fff`)。 +- `style.palette`: string[],设置系列颜色列表。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`,控制图表宽度。 +- `height`: number,默认 `400`,控制图表高度。 +- `title`: string,默认空字符串,用于设置图表标题。 +- `axisXTitle`: string,默认空字符串,设置 X 轴标题。 +- `axisYTitle`: string,默认空字符串,设置 Y 轴标题。 + +## 使用建议 +类别名称保持简短;若系列数较多可改用堆叠或筛选重点项目,以免图表拥挤。 + +## 返回结果 +- 返回条形图图像 URL,并在 `_meta.spec` 中给出完整配置以便复用。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_boxplot_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_boxplot_chart.md new file mode 100644 index 00000000..d2259a24 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_boxplot_chart.md @@ -0,0 +1,25 @@ +# generate_boxplot_chart — 箱型图 + +## 功能概述 +展示各类别数据的分布范围(最值、四分位、异常值),用于质量监控、实验结果或群体分布比较。 + +## 输入字段 +### 必填 +- `data`: array,每条记录包含 `category`(string)与 `value`(number),可选 `group`(string)用于多组比较。 + +### 可选 +- `style.backgroundColor`: string,设置背景色。 +- `style.palette`: string[],定义配色列表。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 +- `axisXTitle`: string,默认空字符串。 +- `axisYTitle`: string,默认空字符串。 + +## 使用建议 +单个类别至少提供 5 个样本以保证统计意义;如需展示多批次,可通过 `group` 或拆分多次调用。 + +## 返回结果 +- 返回箱型图 URL,并在 `_meta.spec` 中储存输入规格。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_column_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_column_chart.md new file mode 100644 index 00000000..d21ba401 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_column_chart.md @@ -0,0 +1,27 @@ +# generate_column_chart — 柱状图 + +## 功能概述 +纵向柱状对比不同类别或时间段的指标,可分组或堆叠展示,常用于销量、营收、客流对比。 + +## 输入字段 +### 必填 +- `data`: array,每条至少含 `category`(string)与 `value`(number),如需分组或堆叠需补充 `group`(string)。 + +### 可选 +- `group`: boolean,默认 `true`,用于按系列并排展示不同 `group`,开启时需确保 `stack=false` 且数据包含 `group`。 +- `stack`: boolean,默认 `false`,用于将不同 `group` 堆叠到同一柱子,开启时需确保 `group=false` 且数据包含 `group`。 +- `style.backgroundColor`: string,自定义背景色。 +- `style.palette`: string[],定义配色列表。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 +- `axisXTitle`: string,默认空字符串。 +- `axisYTitle`: string,默认空字符串。 + +## 使用建议 +当类别较多(>12)时可按 Top-N 或聚合;堆叠模式要确保各记录都含 `group` 字段以免校验失败。 + +## 返回结果 +- 返回柱状图 URL,并随 `_meta.spec` 提供配置详情。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_district_map.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_district_map.md new file mode 100644 index 00000000..9ce13ccc --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_district_map.md @@ -0,0 +1,28 @@ +# generate_district_map — 行政区地图(中国) + +## 功能概述 +生成中国境内省/市/区/县的覆盖或热力图,可展示指标区间、类别或区域组成,适用于区域销售、政策覆盖等场景。 + +## 输入字段 +### 必填 +- `title`: string,必填且≤16 字,描述地图主题。 +- `data`: object,必填,承载行政区配置及指标信息。 +- `data.name`: string,必填,中国境内的行政区关键词,需明确到省/市/区/县。 + +### 可选 +- `data.style.fillColor`: string,自定义无数据区域的填充色。 +- `data.colors`: string[],枚举或连续色带,默认提供 10 色列表。 +- `data.dataType`: string,枚举 `number`/`enum`,决定颜色映射方式。 +- `data.dataLabel`: string,指标名称(如 `GDP`)。 +- `data.dataValue`: string,指标值或枚举标签。 +- `data.dataValueUnit`: string,指标单位(如 `万亿`)。 +- `data.showAllSubdistricts`: boolean,默认 `false`,是否展示全部下级行政区。 +- `data.subdistricts[]`: array,用于下钻各子区域,元素至少含 `name`,可附 `dataValue` 与 `style.fillColor`。 +- `width`: number,默认 `1600`,设置图宽。 +- `height`: number,默认 `1000`,设置图高。 + +## 使用建议 +名称必须精确到行政层级,避免模糊词;若配置 `subdistricts`,需同时开启 `showAllSubdistricts`;地图只支持中国境内且依赖高德数据。 + +## 返回结果 +- 返回地图图像 URL,并在 `_meta.spec` 中保留完整输入;若配置了 `SERVICE_ID`,生成记录会同步到“我的地图”小程序。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_dual_axes_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_dual_axes_chart.md new file mode 100644 index 00000000..a2b95544 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_dual_axes_chart.md @@ -0,0 +1,25 @@ +# generate_dual_axes_chart — 双轴图 + +## 功能概述 +在同一画布上叠加柱状与折线(或两条不同量纲曲线),用于同时展示趋势与对比,如营收 vs 利润、温度 vs 降雨。 + +## 输入字段 +### 必填 +- `categories`: string[],按顺序提供 X 轴刻度(如年份、月份、品类)。 +- `series`: array,每项至少包含 `type`(`column`/`line`)与 `data`(number[],长度与 `categories` 一致),可选 `axisYTitle`(string)描述该系列 Y 轴含义。 + +### 可选 +- `style.backgroundColor`: string,自定义背景色。 +- `style.palette`: string[],配置多系列配色。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 +- `axisXTitle`: string,默认空字符串。 + +## 使用建议 +仅在确有不同量纲或图例对比需求时使用;保持系列数量 ≤2 以免阅读复杂;若两曲线差值巨大可使用次坐标轴进行缩放。 + +## 返回结果 +- 返回双轴图图像 URL,并随 `_meta.spec` 给出详细参数。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_fishbone_diagram.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_fishbone_diagram.md new file mode 100644 index 00000000..0859852b --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_fishbone_diagram.md @@ -0,0 +1,20 @@ +# generate_fishbone_diagram — 鱼骨图 + +## 功能概述 +用于根因分析,将中心问题放在主干,左右分支展示不同类别的原因及其细化节点,常见于质量管理、流程优化。 + +## 输入字段 +### 必填 +- `data`: object,必填,至少提供根节点 `name`,可通过 `children`(array)递归拓展,最大建议 3 层。 + +### 可选 +- `style.texture`: string,默认 `default`,可选 `default`/`rough` 以切换线条风格。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 + +## 使用建议 +主干节点描述问题陈述;一级分支命名原因类别(人、机、料、法等);叶子节点写具体现象,保持短语式表达。 + +## 返回结果 +- 返回鱼骨图 URL,并在 `_meta.spec` 中保存树形结构,便于后续增删节点。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_flow_diagram.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_flow_diagram.md new file mode 100644 index 00000000..efed160c --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_flow_diagram.md @@ -0,0 +1,22 @@ +# generate_flow_diagram — 流程图 + +## 功能概述 +以节点和连线展示业务流程、审批链或算法步骤,支持开始/判断/操作等多种节点类型。 + +## 输入字段 +### 必填 +- `data`: object,必填,包含节点与连线定义。 +- `data.nodes`: array,至少 1 条,节点需提供唯一 `name`。 +- `data.edges`: array,至少 1 条,包含 `source` 与 `target`(string),可选 `name` 作为连线文本。 + +### 可选 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 + +## 使用建议 +先罗列节点 `name` 并保持唯一,再建立连线;若需要描述条件,可在 `edges.name` 中填写;流程应保持单向或明确分支避免交叉。 + +## 返回结果 +- 返回流程图 URL,并携带 `_meta.spec` 中的节点与边数据,方便下次调整。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_funnel_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_funnel_chart.md new file mode 100644 index 00000000..feb2af87 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_funnel_chart.md @@ -0,0 +1,23 @@ +# generate_funnel_chart — 漏斗图 + +## 功能概述 +展示多阶段转化或流失情况,常用于销售管道、用户旅程等逐步筛选过程。 + +## 输入字段 +### 必填 +- `data`: array,需按流程顺序排列,每条包含 `category`(string)与 `value`(number)。 + +### 可选 +- `style.backgroundColor`: string,设置背景色。 +- `style.palette`: string[],定义各阶段颜色。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 + +## 使用建议 +阶段顺序需按实际流程排列;若数值为百分比应统一基准并在标题或备注中说明口径;避免阶段过多导致阅读困难(建议 ≤6)。 + +## 返回结果 +- 返回漏斗图 URL,并附 `_meta.spec` 方便复用。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_histogram_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_histogram_chart.md new file mode 100644 index 00000000..a081a8d5 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_histogram_chart.md @@ -0,0 +1,26 @@ +# generate_histogram_chart — 直方图 + +## 功能概述 +通过分箱显示连续数值的频数或概率分布,便于识别偏态、离群与集中区间。 + +## 输入字段 +### 必填 +- `data`: number[],至少 1 条,用于构建频数分布。 + +### 可选 +- `binNumber`: number,自定义分箱数量,未设置则自动估算。 +- `style.backgroundColor`: string,设置背景色。 +- `style.palette`: string[],定义柱体颜色。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 +- `axisXTitle`: string,默认空字符串。 +- `axisYTitle`: string,默认空字符串。 + +## 使用建议 +清理空值/异常后再传入;样本量建议 ≥30;根据业务意义调整 `binNumber` 以兼顾细节与整体趋势。 + +## 返回结果 +- 返回直方图 URL,并在 `_meta.spec` 存储参数。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_line_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_line_chart.md new file mode 100644 index 00000000..23b261ff --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_line_chart.md @@ -0,0 +1,26 @@ +# generate_line_chart — 折线图 + +## 功能概述 +展示时间或连续自变量的趋势,可支持多系列对比,适合 KPI 监控、指标预测、走势分析。 + +## 输入字段 +### 必填 +- `data`: array,每条包含 `time`(string)与 `value`(number),多系列时附带 `group`(string)。 + +### 可选 +- `style.lineWidth`: number,自定义折线线宽。 +- `style.backgroundColor`: string,设置背景色。 +- `style.palette`: string[],指定系列颜色。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 +- `axisXTitle`: string,默认空字符串。 +- `axisYTitle`: string,默认空字符串。 + +## 使用建议 +所有系列的时间点应对齐;建议按 ISO 如 `2025-01-01` 或 `2025-W01` 格式化;对于高频数据可先聚合到日/周粒度避免过密。 + +## 返回结果 +- 返回折线图 URL,并附 `_meta.spec` 供后续编辑。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_liquid_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_liquid_chart.md new file mode 100644 index 00000000..5e01d256 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_liquid_chart.md @@ -0,0 +1,24 @@ +# generate_liquid_chart — 水波图 + +## 功能概述 +以液面高度展示单一百分比或进度,视觉动效强,适合达成率、资源占用等指标。 + +## 输入字段 +### 必填 +- `percent`: number,取值范围 [0,1],表示当前百分比或进度。 + +### 可选 +- `shape`: string,默认 `circle`,可选 `circle`/`rect`/`pin`/`triangle`。 +- `style.backgroundColor`: string,自定义背景色。 +- `style.color`: string,自定义水波颜色。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 + +## 使用建议 +确保百分比经过归一化;单图仅支持一个进度,如需多指标请并排生成多个水波图;标题可写“目标完成率 85%”。 + +## 返回结果 +- 返回水波图 URL,并在 `_meta.spec` 中记录参数。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_mind_map.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_mind_map.md new file mode 100644 index 00000000..f4fce637 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_mind_map.md @@ -0,0 +1,20 @@ +# generate_mind_map — 思维导图 + +## 功能概述 +围绕中心主题展开 2~3 级分支,帮助组织想法、计划或知识结构,常用于头脑风暴、方案规划。 + +## 输入字段 +### 必填 +- `data`: object,必填,节点至少含 `name`,可通过 `children`(array)递归扩展,建议深度 ≤3。 + +### 可选 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 + +## 使用建议 +中心节点写主题,一级分支代表主要维度(目标、资源、风险等),叶子节点使用短语;如分支较多,可先分拆多张导图。 + +## 返回结果 +- 返回思维导图 URL,并在 `_meta.spec` 中保留节点树以便后续优化。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_network_graph.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_network_graph.md new file mode 100644 index 00000000..c138070a --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_network_graph.md @@ -0,0 +1,22 @@ +# generate_network_graph — 网络关系图 + +## 功能概述 +以节点与连线呈现实体之间的连接关系,适合社交网络、系统依赖、知识图谱等场景。 + +## 输入字段 +### 必填 +- `data`: object,必填,包含节点与连线。 +- `data.nodes`: array,至少 1 条,需提供唯一 `name`。 +- `data.edges`: array,至少 1 条,包含 `source` 与 `target`(string),可选 `name` 说明关系。 + +### 可选 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 + +## 使用建议 +节点数量保持在 10~50 之间以避免拥挤;确保 `edges` 中的 `source/target` 对应已存在的节点;可在 `label` 中注明关系含义。 + +## 返回结果 +- 返回网络图 URL,并提供 `_meta.spec` 以便后续增删节点。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_organization_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_organization_chart.md new file mode 100644 index 00000000..f6bbc964 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_organization_chart.md @@ -0,0 +1,21 @@ +# generate_organization_chart — 组织架构图 + +## 功能概述 +展示公司、团队或项目的层级关系,并可在节点上描述角色职责。 + +## 输入字段 +### 必填 +- `data`: object,必填,节点至少含 `name`(string),可选 `description`(string),子节点通过 `children`(array)嵌套,最大深度建议为 3。 + +### 可选 +- `orient`: string,默认 `vertical`,可选 `horizontal`/`vertical`。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 + +## 使用建议 +节点名称使用岗位/角色,`description` 简要说明职责或人数;若组织较大可拆分多个子图或按部门分批展示。 + +## 返回结果 +- 返回组织架构图 URL,并在 `_meta.spec` 保存结构便于日后迭代。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_path_map.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_path_map.md new file mode 100644 index 00000000..785c6665 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_path_map.md @@ -0,0 +1,20 @@ +# generate_path_map — 路径地图(中国) + +## 功能概述 +基于高德地图展示中国境内的路线或行程,按顺序连接一系列 POI,适用于物流路线、旅游规划、配送轨迹等。 + +## 输入字段 +### 必填 +- `title`: string,必填且≤16 字,描述路线主题。 +- `data`: array,至少 1 个路线对象。 +- `data[].data`: string[],必填,包含该路线上按顺序排列的中国境内 POI 名称。 + +### 可选 +- `width`: number,默认 `1600`。 +- `height`: number,默认 `1000`。 + +## 使用建议 +POI 名称必须具体且位于中国(如“西安市钟楼”“杭州西湖苏堤春晓”);若需多条线路,可在 `data` 中添加多段对象。 + +## 返回结果 +- 返回路径地图 URL,并在 `_meta.spec` 中保留标题与 POI 列表;若配置 `SERVICE_ID`,还会记录到“我的地图”。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_pie_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_pie_chart.md new file mode 100644 index 00000000..f13e2fb1 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_pie_chart.md @@ -0,0 +1,24 @@ +# generate_pie_chart — 饼/环图 + +## 功能概述 +展示整体与部分的占比,可通过内径形成环图,适用于市场份额、预算构成、用户群划分等。 + +## 输入字段 +### 必填 +- `data`: array,每条记录包含 `category`(string)与 `value`(number)。 + +### 可选 +- `innerRadius`: number,范围 [0, 1],默认 `0`,设为 `0.6` 等值可生成环图。 +- `style.backgroundColor`: string,设置背景色。 +- `style.palette`: string[],定义配色列表。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 + +## 使用建议 +类别数量建议 ≤6,若更多可聚合为“其它”;确保数值单位统一(百分比或绝对值),必要时在标题中说明基数。 + +## 返回结果 +- 返回饼/环图 URL,并附 `_meta.spec`。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_pin_map.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_pin_map.md new file mode 100644 index 00000000..ca48b2a4 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_pin_map.md @@ -0,0 +1,23 @@ +# generate_pin_map — 点标地图(中国) + +## 功能概述 +在中国地图上以标记展示多个 POI 位置,可配合弹窗显示图片或说明,适用于门店分布、资产布点等。 + +## 输入字段 +### 必填 +- `title`: string,必填且≤16 字,概述点位集合。 +- `data`: string[],必填,包含中国境内的 POI 名称列表。 + +### 可选 +- `markerPopup.type`: string,固定为 `image`。 +- `markerPopup.width`: number,默认 `40`,图片宽度。 +- `markerPopup.height`: number,默认 `40`,图片高度。 +- `markerPopup.borderRadius`: number,默认 `8`,图片圆角。 +- `width`: number,默认 `1600`。 +- `height`: number,默认 `1000`。 + +## 使用建议 +POI 名称需包含足够的地理限定(城市+地标);根据业务可在名称中附带属性,如“上海徐汇门店 A”;地图依赖高德数据,仅支持中国。 + +## 返回结果 +- 返回点标地图 URL,并在 `_meta.spec` 中保存点位与弹窗配置。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_radar_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_radar_chart.md new file mode 100644 index 00000000..d41b02cb --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_radar_chart.md @@ -0,0 +1,24 @@ +# generate_radar_chart — 雷达图 + +## 功能概述 +在多维坐标系上比较单个对象或多对象的能力维度,常用于评测、产品对比、绩效画像。 + +## 输入字段 +### 必填 +- `data`: array,每条记录包含 `name`(string)与 `value`(number),可选 `group`(string)。 + +### 可选 +- `style.backgroundColor`: string,设置背景色。 +- `style.lineWidth`: number,设置雷达线宽。 +- `style.palette`: string[],定义系列颜色。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 + +## 使用建议 +维度数量控制在 4~8 之间;不同对象通过 `group` 区分并保证同一维度都给出数值;如量纲不同需先归一化。 + +## 返回结果 +- 返回雷达图 URL,并附 `_meta.spec`。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_sankey_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_sankey_chart.md new file mode 100644 index 00000000..8a5e1ba9 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_sankey_chart.md @@ -0,0 +1,24 @@ +# generate_sankey_chart — 桑基图 + +## 功能概述 +展示资源、能量或用户流在不同节点之间的流向与数量,适合预算分配、流量路径、能耗分布等。 + +## 输入字段 +### 必填 +- `data`: array,每条记录包含 `source`(string)、`target`(string)与 `value`(number)。 + +### 可选 +- `nodeAlign`: string,默认 `center`,可选 `left`/`right`/`justify`/`center`。 +- `style.backgroundColor`: string,设置背景色。 +- `style.palette`: string[],定义节点配色。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 + +## 使用建议 +节点名称保持唯一,避免过多交叉;如存在环路需先打平为阶段流向;可按阈值过滤小流量以聚焦重点。 + +## 返回结果 +- 返回桑基图 URL,并在 `_meta.spec` 存放节点与流量定义。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_scatter_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_scatter_chart.md new file mode 100644 index 00000000..56c010b0 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_scatter_chart.md @@ -0,0 +1,25 @@ +# generate_scatter_chart — 散点图 + +## 功能概述 +展示两个连续变量之间的关系,可通过颜色/形状区分不同分组,适合相关性分析、聚类探索。 + +## 输入字段 +### 必填 +- `data`: array,每条记录包含 `x`(number)与 `y`(number),可选 `group`(string)。 + +### 可选 +- `style.backgroundColor`: string,设置背景色。 +- `style.palette`: string[],指定系列配色。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 +- `axisXTitle`: string,默认空字符串。 +- `axisYTitle`: string,默认空字符串。 + +## 使用建议 +在上传前可对不同量纲进行标准化;若数据量很大可先抽样;使用 `group` 区分不同类别或聚类结果以便阅读。 + +## 返回结果 +- 返回散点图 URL,并附 `_meta.spec`。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_spreadsheet.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_spreadsheet.md new file mode 100644 index 00000000..001ae80c --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_spreadsheet.md @@ -0,0 +1,24 @@ +# generate_spreadsheet — 电子表格/数据透视表 + +## 功能概述 +生成电子表格或数据透视表,用于展示结构化的表格数据。当提供 `rows` 或 `values` 字段时,渲染为数据透视表(交叉表);否则渲染为常规表格。适合展示结构化数据、跨类别比较值以及创建数据汇总。 + +## 输入字段 +### 必填 +- `data`: array,表格数据数组,每个对象代表一行。键是列名,值可以是字符串、数字、null 或 undefined。例如:`[{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }]`。 + +### 可选 +- `rows`: array,数据透视表的行标题字段。当提供 `rows` 或 `values` 时,电子表格将渲染为数据透视表。 +- `columns`: array,列标题字段,用于指定列的顺序。对于常规表格,这决定列的顺序;对于数据透视表,用于列分组。 +- `values`: array,数据透视表的值字段。当提供 `rows` 或 `values` 时,电子表格将渲染为数据透视表。 +- `theme`: string,默认 `default`,可选 `default`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 + +## 使用建议 +- 对于常规表格,只需提供 `data` 和可选的 `columns` 来控制列的顺序。 +- 对于数据透视表(交叉表),提供 `rows` 用于行分组,`columns` 用于列分组,`values` 用于聚合的值字段。 +- 确保数据中的字段名与 `rows`、`columns`、`values` 中指定的字段名一致。 + +## 返回结果 +- 返回电子表格/数据透视表图片 URL,并附 `_meta.spec` 供后续编辑。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_treemap_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_treemap_chart.md new file mode 100644 index 00000000..186af04c --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_treemap_chart.md @@ -0,0 +1,23 @@ +# generate_treemap_chart — 矩形树图 + +## 功能概述 +以嵌套矩形展示层级结构及各节点权重,适合资产占比、市场份额、目录容量等。 + +## 输入字段 +### 必填 +- `data`: array,节点数组,每条含 `name`(string)与 `value`(number),可递归嵌套 `children`。 + +### 可选 +- `style.backgroundColor`: string,设置背景色。 +- `style.palette`: string[],定义配色列表。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 + +## 使用建议 +确保每个节点 `value` ≥0,并与子节点之和一致;树层级不宜过深,可按需要提前聚合;为提升可读性可在节点名中加上数值单位。 + +## 返回结果 +- 返回矩形树图 URL,并同步 `_meta.spec`。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_venn_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_venn_chart.md new file mode 100644 index 00000000..91235c02 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_venn_chart.md @@ -0,0 +1,23 @@ +# generate_venn_chart — 维恩图 + +## 功能概述 +展示多个集合之间的交集、并集与差异,适用于市场细分、特性覆盖、用户重叠分析。 + +## 输入字段 +### 必填 +- `data`: array,每条记录包含 `value`(number)与 `sets`(string[]),可选 `label`(string)。 + +### 可选 +- `style.backgroundColor`: string,设置背景色。 +- `style.palette`: string[],定义配色列表。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 + +## 使用建议 +集合数量建议 ≤4;若缺少精确权重可根据大致占比填写;集合命名保持简洁明确(如“移动端用户”)。 + +## 返回结果 +- 返回维恩图 URL,并保存在 `_meta.spec` 中。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_violin_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_violin_chart.md new file mode 100644 index 00000000..39825d77 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_violin_chart.md @@ -0,0 +1,25 @@ +# generate_violin_chart — 小提琴图 + +## 功能概述 +结合核密度曲线与箱型统计展示不同类别的分布形态,适合对比多批次实验或群体表现。 + +## 输入字段 +### 必填 +- `data`: array,每条记录包含 `category`(string)与 `value`(number),可选 `group`(string)。 + +### 可选 +- `style.backgroundColor`: string,设置背景色。 +- `style.palette`: string[],定义配色列表。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 +- `axisXTitle`: string,默认空字符串。 +- `axisYTitle`: string,默认空字符串。 + +## 使用建议 +各类别样本量建议 ≥30 以确保密度估计稳定;如需要突出四分位信息,可与箱型图结合展示。 + +## 返回结果 +- 返回小提琴图 URL,并在 `_meta.spec` 中保留配置。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_word_cloud_chart.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_word_cloud_chart.md new file mode 100644 index 00000000..047d4938 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/references/generate_word_cloud_chart.md @@ -0,0 +1,23 @@ +# generate_word_cloud_chart — 词云图 + +## 功能概述 +根据词频或权重调节文字大小与位置,用于快速提炼文本主题、情绪或关键词热点。 + +## 输入字段 +### 必填 +- `data`: array,每条记录包含 `text`(string)与 `value`(number)。 + +### 可选 +- `style.backgroundColor`: string,设置背景色。 +- `style.palette`: string[],定义词云配色。 +- `style.texture`: string,默认 `default`,可选 `default`/`rough`。 +- `theme`: string,默认 `default`,可选 `default`/`academy`/`dark`。 +- `width`: number,默认 `600`。 +- `height`: number,默认 `400`。 +- `title`: string,默认空字符串。 + +## 使用建议 +生成前去除停用词并合并同义词;统一大小写避免重复;如需突出情绪可按正负值映射配色。 + +## 返回结果 +- 返回词云图 URL,并附 `_meta.spec`。 \ No newline at end of file diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/__init__.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/add_slide.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/add_slide.py new file mode 100644 index 00000000..13700df0 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/add_slide.py @@ -0,0 +1,195 @@ +"""Add a new slide to an unpacked PPTX directory. + +Usage: python add_slide.py + +The source can be: + - A slide file (e.g., slide2.xml) - duplicates the slide + - A layout file (e.g., slideLayout2.xml) - creates from layout + +Examples: + python add_slide.py unpacked/ slide2.xml + # Duplicates slide2, creates slide5.xml + + python add_slide.py unpacked/ slideLayout2.xml + # Creates slide5.xml from slideLayout2.xml + +To see available layouts: ls unpacked/ppt/slideLayouts/ + +Prints the element to add to presentation.xml. +""" + +import re +import shutil +import sys +from pathlib import Path + + +def get_next_slide_number(slides_dir: Path) -> int: + existing = [int(m.group(1)) for f in slides_dir.glob("slide*.xml") + if (m := re.match(r"slide(\d+)\.xml", f.name))] + return max(existing) + 1 if existing else 1 + + +def create_slide_from_layout(unpacked_dir: Path, layout_file: str) -> None: + slides_dir = unpacked_dir / "ppt" / "slides" + rels_dir = slides_dir / "_rels" + layouts_dir = unpacked_dir / "ppt" / "slideLayouts" + + layout_path = layouts_dir / layout_file + if not layout_path.exists(): + print(f"Error: {layout_path} not found", file=sys.stderr) + sys.exit(1) + + next_num = get_next_slide_number(slides_dir) + dest = f"slide{next_num}.xml" + dest_slide = slides_dir / dest + dest_rels = rels_dir / f"{dest}.rels" + + slide_xml = ''' + + + + + + + + + + + + + + + + + + + + + +''' + dest_slide.write_text(slide_xml, encoding="utf-8") + + rels_dir.mkdir(exist_ok=True) + rels_xml = f''' + + +''' + dest_rels.write_text(rels_xml, encoding="utf-8") + + _add_to_content_types(unpacked_dir, dest) + + rid = _add_to_presentation_rels(unpacked_dir, dest) + + next_slide_id = _get_next_slide_id(unpacked_dir) + + print(f"Created {dest} from {layout_file}") + print(f'Add to presentation.xml : ') + + +def duplicate_slide(unpacked_dir: Path, source: str) -> None: + slides_dir = unpacked_dir / "ppt" / "slides" + rels_dir = slides_dir / "_rels" + + source_slide = slides_dir / source + + if not source_slide.exists(): + print(f"Error: {source_slide} not found", file=sys.stderr) + sys.exit(1) + + next_num = get_next_slide_number(slides_dir) + dest = f"slide{next_num}.xml" + dest_slide = slides_dir / dest + + source_rels = rels_dir / f"{source}.rels" + dest_rels = rels_dir / f"{dest}.rels" + + shutil.copy2(source_slide, dest_slide) + + if source_rels.exists(): + shutil.copy2(source_rels, dest_rels) + + rels_content = dest_rels.read_text(encoding="utf-8") + rels_content = re.sub( + r'\s*]*Type="[^"]*notesSlide"[^>]*/>\s*', + "\n", + rels_content, + ) + dest_rels.write_text(rels_content, encoding="utf-8") + + _add_to_content_types(unpacked_dir, dest) + + rid = _add_to_presentation_rels(unpacked_dir, dest) + + next_slide_id = _get_next_slide_id(unpacked_dir) + + print(f"Created {dest} from {source}") + print(f'Add to presentation.xml : ') + + +def _add_to_content_types(unpacked_dir: Path, dest: str) -> None: + content_types_path = unpacked_dir / "[Content_Types].xml" + content_types = content_types_path.read_text(encoding="utf-8") + + new_override = f'' + + if f"/ppt/slides/{dest}" not in content_types: + content_types = content_types.replace("", f" {new_override}\n") + content_types_path.write_text(content_types, encoding="utf-8") + + +def _add_to_presentation_rels(unpacked_dir: Path, dest: str) -> str: + pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels" + pres_rels = pres_rels_path.read_text(encoding="utf-8") + + rids = [int(m) for m in re.findall(r'Id="rId(\d+)"', pres_rels)] + next_rid = max(rids) + 1 if rids else 1 + rid = f"rId{next_rid}" + + new_rel = f'' + + if f"slides/{dest}" not in pres_rels: + pres_rels = pres_rels.replace("", f" {new_rel}\n") + pres_rels_path.write_text(pres_rels, encoding="utf-8") + + return rid + + +def _get_next_slide_id(unpacked_dir: Path) -> int: + pres_path = unpacked_dir / "ppt" / "presentation.xml" + pres_content = pres_path.read_text(encoding="utf-8") + slide_ids = [int(m) for m in re.findall(r']*id="(\d+)"', pres_content)] + return max(slide_ids) + 1 if slide_ids else 256 + + +def parse_source(source: str) -> tuple[str, str | None]: + if source.startswith("slideLayout") and source.endswith(".xml"): + return ("layout", source) + + return ("slide", None) + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python add_slide.py ", file=sys.stderr) + print("", file=sys.stderr) + print("Source can be:", file=sys.stderr) + print(" slide2.xml - duplicate an existing slide", file=sys.stderr) + print(" slideLayout2.xml - create from a layout template", file=sys.stderr) + print("", file=sys.stderr) + print("To see available layouts: ls /ppt/slideLayouts/", file=sys.stderr) + sys.exit(1) + + unpacked_dir = Path(sys.argv[1]) + source = sys.argv[2] + + if not unpacked_dir.exists(): + print(f"Error: {unpacked_dir} not found", file=sys.stderr) + sys.exit(1) + + source_type, layout_file = parse_source(source) + + if source_type == "layout" and layout_file is not None: + create_slide_from_layout(unpacked_dir, layout_file) + else: + duplicate_slide(unpacked_dir, source) diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/clean.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/clean.py new file mode 100644 index 00000000..3d13994c --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/clean.py @@ -0,0 +1,286 @@ +"""Remove unreferenced files from an unpacked PPTX directory. + +Usage: python clean.py + +Example: + python clean.py unpacked/ + +This script removes: +- Orphaned slides (not in sldIdLst) and their relationships +- [trash] directory (unreferenced files) +- Orphaned .rels files for deleted resources +- Unreferenced media, embeddings, charts, diagrams, drawings, ink files +- Unreferenced theme files +- Unreferenced notes slides +- Content-Type overrides for deleted files +""" + +import sys +from pathlib import Path + +import defusedxml.minidom + + +import re + + +def get_slides_in_sldidlst(unpacked_dir: Path) -> set[str]: + pres_path = unpacked_dir / "ppt" / "presentation.xml" + pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels" + + if not pres_path.exists() or not pres_rels_path.exists(): + return set() + + rels_dom = defusedxml.minidom.parse(str(pres_rels_path)) + rid_to_slide = {} + for rel in rels_dom.getElementsByTagName("Relationship"): + rid = rel.getAttribute("Id") + target = rel.getAttribute("Target") + rel_type = rel.getAttribute("Type") + if "slide" in rel_type and target.startswith("slides/"): + rid_to_slide[rid] = target.replace("slides/", "") + + pres_content = pres_path.read_text(encoding="utf-8") + referenced_rids = set(re.findall(r']*r:id="([^"]+)"', pres_content)) + + return {rid_to_slide[rid] for rid in referenced_rids if rid in rid_to_slide} + + +def remove_orphaned_slides(unpacked_dir: Path) -> list[str]: + slides_dir = unpacked_dir / "ppt" / "slides" + slides_rels_dir = slides_dir / "_rels" + pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels" + + if not slides_dir.exists(): + return [] + + referenced_slides = get_slides_in_sldidlst(unpacked_dir) + removed = [] + + for slide_file in slides_dir.glob("slide*.xml"): + if slide_file.name not in referenced_slides: + rel_path = slide_file.relative_to(unpacked_dir) + slide_file.unlink() + removed.append(str(rel_path)) + + rels_file = slides_rels_dir / f"{slide_file.name}.rels" + if rels_file.exists(): + rels_file.unlink() + removed.append(str(rels_file.relative_to(unpacked_dir))) + + if removed and pres_rels_path.exists(): + rels_dom = defusedxml.minidom.parse(str(pres_rels_path)) + changed = False + + for rel in list(rels_dom.getElementsByTagName("Relationship")): + target = rel.getAttribute("Target") + if target.startswith("slides/"): + slide_name = target.replace("slides/", "") + if slide_name not in referenced_slides: + if rel.parentNode: + rel.parentNode.removeChild(rel) + changed = True + + if changed: + with open(pres_rels_path, "wb") as f: + f.write(rels_dom.toxml(encoding="utf-8")) + + return removed + + +def remove_trash_directory(unpacked_dir: Path) -> list[str]: + trash_dir = unpacked_dir / "[trash]" + removed = [] + + if trash_dir.exists() and trash_dir.is_dir(): + for file_path in trash_dir.iterdir(): + if file_path.is_file(): + rel_path = file_path.relative_to(unpacked_dir) + removed.append(str(rel_path)) + file_path.unlink() + trash_dir.rmdir() + + return removed + + +def get_slide_referenced_files(unpacked_dir: Path) -> set: + referenced = set() + slides_rels_dir = unpacked_dir / "ppt" / "slides" / "_rels" + + if not slides_rels_dir.exists(): + return referenced + + for rels_file in slides_rels_dir.glob("*.rels"): + dom = defusedxml.minidom.parse(str(rels_file)) + for rel in dom.getElementsByTagName("Relationship"): + target = rel.getAttribute("Target") + if not target: + continue + target_path = (rels_file.parent.parent / target).resolve() + try: + referenced.add(target_path.relative_to(unpacked_dir.resolve())) + except ValueError: + pass + + return referenced + + +def remove_orphaned_rels_files(unpacked_dir: Path) -> list[str]: + resource_dirs = ["charts", "diagrams", "drawings"] + removed = [] + slide_referenced = get_slide_referenced_files(unpacked_dir) + + for dir_name in resource_dirs: + rels_dir = unpacked_dir / "ppt" / dir_name / "_rels" + if not rels_dir.exists(): + continue + + for rels_file in rels_dir.glob("*.rels"): + resource_file = rels_dir.parent / rels_file.name.replace(".rels", "") + try: + resource_rel_path = resource_file.resolve().relative_to(unpacked_dir.resolve()) + except ValueError: + continue + + if not resource_file.exists() or resource_rel_path not in slide_referenced: + rels_file.unlink() + rel_path = rels_file.relative_to(unpacked_dir) + removed.append(str(rel_path)) + + return removed + + +def get_referenced_files(unpacked_dir: Path) -> set: + referenced = set() + + for rels_file in unpacked_dir.rglob("*.rels"): + dom = defusedxml.minidom.parse(str(rels_file)) + for rel in dom.getElementsByTagName("Relationship"): + target = rel.getAttribute("Target") + if not target: + continue + target_path = (rels_file.parent.parent / target).resolve() + try: + referenced.add(target_path.relative_to(unpacked_dir.resolve())) + except ValueError: + pass + + return referenced + + +def remove_orphaned_files(unpacked_dir: Path, referenced: set) -> list[str]: + resource_dirs = ["media", "embeddings", "charts", "diagrams", "tags", "drawings", "ink"] + removed = [] + + for dir_name in resource_dirs: + dir_path = unpacked_dir / "ppt" / dir_name + if not dir_path.exists(): + continue + + for file_path in dir_path.glob("*"): + if not file_path.is_file(): + continue + rel_path = file_path.relative_to(unpacked_dir) + if rel_path not in referenced: + file_path.unlink() + removed.append(str(rel_path)) + + theme_dir = unpacked_dir / "ppt" / "theme" + if theme_dir.exists(): + for file_path in theme_dir.glob("theme*.xml"): + rel_path = file_path.relative_to(unpacked_dir) + if rel_path not in referenced: + file_path.unlink() + removed.append(str(rel_path)) + theme_rels = theme_dir / "_rels" / f"{file_path.name}.rels" + if theme_rels.exists(): + theme_rels.unlink() + removed.append(str(theme_rels.relative_to(unpacked_dir))) + + notes_dir = unpacked_dir / "ppt" / "notesSlides" + if notes_dir.exists(): + for file_path in notes_dir.glob("*.xml"): + if not file_path.is_file(): + continue + rel_path = file_path.relative_to(unpacked_dir) + if rel_path not in referenced: + file_path.unlink() + removed.append(str(rel_path)) + + notes_rels_dir = notes_dir / "_rels" + if notes_rels_dir.exists(): + for file_path in notes_rels_dir.glob("*.rels"): + notes_file = notes_dir / file_path.name.replace(".rels", "") + if not notes_file.exists(): + file_path.unlink() + removed.append(str(file_path.relative_to(unpacked_dir))) + + return removed + + +def update_content_types(unpacked_dir: Path, removed_files: list[str]) -> None: + ct_path = unpacked_dir / "[Content_Types].xml" + if not ct_path.exists(): + return + + dom = defusedxml.minidom.parse(str(ct_path)) + changed = False + + for override in list(dom.getElementsByTagName("Override")): + part_name = override.getAttribute("PartName").lstrip("/") + if part_name in removed_files: + if override.parentNode: + override.parentNode.removeChild(override) + changed = True + + if changed: + with open(ct_path, "wb") as f: + f.write(dom.toxml(encoding="utf-8")) + + +def clean_unused_files(unpacked_dir: Path) -> list[str]: + all_removed = [] + + slides_removed = remove_orphaned_slides(unpacked_dir) + all_removed.extend(slides_removed) + + trash_removed = remove_trash_directory(unpacked_dir) + all_removed.extend(trash_removed) + + while True: + removed_rels = remove_orphaned_rels_files(unpacked_dir) + referenced = get_referenced_files(unpacked_dir) + removed_files = remove_orphaned_files(unpacked_dir, referenced) + + total_removed = removed_rels + removed_files + if not total_removed: + break + + all_removed.extend(total_removed) + + if all_removed: + update_content_types(unpacked_dir, all_removed) + + return all_removed + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python clean.py ", file=sys.stderr) + print("Example: python clean.py unpacked/", file=sys.stderr) + sys.exit(1) + + unpacked_dir = Path(sys.argv[1]) + + if not unpacked_dir.exists(): + print(f"Error: {unpacked_dir} not found", file=sys.stderr) + sys.exit(1) + + removed = clean_unused_files(unpacked_dir) + + if removed: + print(f"Removed {len(removed)} unreferenced files:") + for f in removed: + print(f" {f}") + else: + print("No unreferenced files found") diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/generate.js b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/generate.js new file mode 100644 index 00000000..7d5bc965 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/generate.js @@ -0,0 +1,173 @@ +#!/usr/bin/env node + +const fs = require("fs"); + +// Chart type mapping, consistent with src/utils/callTool.ts +const CHART_TYPE_MAP = { + generate_area_chart: "area", + generate_bar_chart: "bar", + generate_boxplot_chart: "boxplot", + generate_column_chart: "column", + generate_district_map: "district-map", + generate_dual_axes_chart: "dual-axes", + generate_fishbone_diagram: "fishbone-diagram", + generate_flow_diagram: "flow-diagram", + generate_funnel_chart: "funnel", + generate_histogram_chart: "histogram", + generate_line_chart: "line", + generate_liquid_chart: "liquid", + generate_mind_map: "mind-map", + generate_network_graph: "network-graph", + generate_organization_chart: "organization-chart", + generate_path_map: "path-map", + generate_pie_chart: "pie", + generate_pin_map: "pin-map", + generate_radar_chart: "radar", + generate_sankey_chart: "sankey", + generate_scatter_chart: "scatter", + generate_treemap_chart: "treemap", + generate_venn_chart: "venn", + generate_violin_chart: "violin", + generate_word_cloud_chart: "word-cloud", +}; + +function getVisRequestServer() { + return ( + process.env.VIS_REQUEST_SERVER || + "https://antv-studio.alipay.com/api/gpt-vis" + ); +} + +function getServiceIdentifier() { + return process.env.SERVICE_ID; +} + +async function httpPost(url, payload) { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`HTTP ${response.status}: ${text}`); + } + + return response.json(); +} + +async function generateChartUrl(chartType, options) { + const url = getVisRequestServer(); + const payload = { + type: chartType, + source: "chart-visualization-creator", + ...options, + }; + + const data = await httpPost(url, payload); + + if (!data.success) { + throw new Error(data.errorMessage || "Unknown error"); + } + + return data.resultObj; +} + +async function generateMap(tool, inputData) { + const url = getVisRequestServer(); + const payload = { + serviceId: getServiceIdentifier(), + tool, + input: inputData, + source: "chart-visualization-creator", + }; + + const data = await httpPost(url, payload); + + if (!data.success) { + throw new Error(data.errorMessage || "Unknown error"); + } + + return data.resultObj; +} + +async function main() { + if (process.argv.length < 3) { + console.error("Usage: node generate.js "); + process.exit(1); + } + + const specArg = process.argv[2]; + let spec; + + try { + if (fs.existsSync(specArg)) { + const fileContent = fs.readFileSync(specArg, "utf-8"); + spec = JSON.parse(fileContent); + } else { + spec = JSON.parse(specArg); + } + } catch (e) { + console.error(`Error parsing spec: ${e.message}`); + process.exit(1); + } + + const specs = Array.isArray(spec) ? spec : [spec]; + + for (const item of specs) { + const tool = item.tool; + const args = item.args || {}; + + if (!tool) { + console.error( + `Error: 'tool' field missing in spec: ${JSON.stringify(item)}`, + ); + continue; + } + + const chartType = CHART_TYPE_MAP[tool]; + if (!chartType) { + console.error(`Error: Unknown tool '${tool}'`); + continue; + } + + const isMapChartTool = [ + "generate_district_map", + "generate_path_map", + "generate_pin_map", + ].includes(tool); + + try { + if (isMapChartTool) { + const result = await generateMap(tool, args); + if (result && result.content) { + for (const contentItem of result.content) { + if (contentItem.type === "text") { + console.log(contentItem.text); + } + } + } else { + console.log(JSON.stringify(result)); + } + } else { + const url = await generateChartUrl(chartType, args); + console.log(url); + } + } catch (e) { + console.error(`Error generating chart for ${tool}: ${e.message}`); + } + } +} + +if (require.main === module) { + main().catch((err) => { + console.error(err.message); + process.exit(1); + }); +} + +// Export functions for testing +module.exports = { generateChartUrl, generateMap, httpPost, CHART_TYPE_MAP }; diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/helpers/__init__.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/helpers/merge_runs.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/helpers/merge_runs.py new file mode 100644 index 00000000..ad7c25ee --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/helpers/merge_runs.py @@ -0,0 +1,199 @@ +"""Merge adjacent runs with identical formatting in DOCX. + +Merges adjacent elements that have identical properties. +Works on runs in paragraphs and inside tracked changes (, ). + +Also: +- Removes rsid attributes from runs (revision metadata that doesn't affect rendering) +- Removes proofErr elements (spell/grammar markers that block merging) +""" + +from pathlib import Path + +import defusedxml.minidom + + +def merge_runs(input_dir: str) -> tuple[int, str]: + doc_xml = Path(input_dir) / "word" / "document.xml" + + if not doc_xml.exists(): + return 0, f"Error: {doc_xml} not found" + + try: + dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8")) + root = dom.documentElement + + _remove_elements(root, "proofErr") + _strip_run_rsid_attrs(root) + + containers = {run.parentNode for run in _find_elements(root, "r")} + + merge_count = 0 + for container in containers: + merge_count += _merge_runs_in(container) + + doc_xml.write_bytes(dom.toxml(encoding="UTF-8")) + return merge_count, f"Merged {merge_count} runs" + + except Exception as e: + return 0, f"Error: {e}" + + + + +def _find_elements(root, tag: str) -> list: + results = [] + + def traverse(node): + if node.nodeType == node.ELEMENT_NODE: + name = node.localName or node.tagName + if name == tag or name.endswith(f":{tag}"): + results.append(node) + for child in node.childNodes: + traverse(child) + + traverse(root) + return results + + +def _get_child(parent, tag: str): + for child in parent.childNodes: + if child.nodeType == child.ELEMENT_NODE: + name = child.localName or child.tagName + if name == tag or name.endswith(f":{tag}"): + return child + return None + + +def _get_children(parent, tag: str) -> list: + results = [] + for child in parent.childNodes: + if child.nodeType == child.ELEMENT_NODE: + name = child.localName or child.tagName + if name == tag or name.endswith(f":{tag}"): + results.append(child) + return results + + +def _is_adjacent(elem1, elem2) -> bool: + node = elem1.nextSibling + while node: + if node == elem2: + return True + if node.nodeType == node.ELEMENT_NODE: + return False + if node.nodeType == node.TEXT_NODE and node.data.strip(): + return False + node = node.nextSibling + return False + + + + +def _remove_elements(root, tag: str): + for elem in _find_elements(root, tag): + if elem.parentNode: + elem.parentNode.removeChild(elem) + + +def _strip_run_rsid_attrs(root): + for run in _find_elements(root, "r"): + for attr in list(run.attributes.values()): + if "rsid" in attr.name.lower(): + run.removeAttribute(attr.name) + + + + +def _merge_runs_in(container) -> int: + merge_count = 0 + run = _first_child_run(container) + + while run: + while True: + next_elem = _next_element_sibling(run) + if next_elem and _is_run(next_elem) and _can_merge(run, next_elem): + _merge_run_content(run, next_elem) + container.removeChild(next_elem) + merge_count += 1 + else: + break + + _consolidate_text(run) + run = _next_sibling_run(run) + + return merge_count + + +def _first_child_run(container): + for child in container.childNodes: + if child.nodeType == child.ELEMENT_NODE and _is_run(child): + return child + return None + + +def _next_element_sibling(node): + sibling = node.nextSibling + while sibling: + if sibling.nodeType == sibling.ELEMENT_NODE: + return sibling + sibling = sibling.nextSibling + return None + + +def _next_sibling_run(node): + sibling = node.nextSibling + while sibling: + if sibling.nodeType == sibling.ELEMENT_NODE: + if _is_run(sibling): + return sibling + sibling = sibling.nextSibling + return None + + +def _is_run(node) -> bool: + name = node.localName or node.tagName + return name == "r" or name.endswith(":r") + + +def _can_merge(run1, run2) -> bool: + rpr1 = _get_child(run1, "rPr") + rpr2 = _get_child(run2, "rPr") + + if (rpr1 is None) != (rpr2 is None): + return False + if rpr1 is None: + return True + return rpr1.toxml() == rpr2.toxml() + + +def _merge_run_content(target, source): + for child in list(source.childNodes): + if child.nodeType == child.ELEMENT_NODE: + name = child.localName or child.tagName + if name != "rPr" and not name.endswith(":rPr"): + target.appendChild(child) + + +def _consolidate_text(run): + t_elements = _get_children(run, "t") + + for i in range(len(t_elements) - 1, 0, -1): + curr, prev = t_elements[i], t_elements[i - 1] + + if _is_adjacent(prev, curr): + prev_text = prev.firstChild.data if prev.firstChild else "" + curr_text = curr.firstChild.data if curr.firstChild else "" + merged = prev_text + curr_text + + if prev.firstChild: + prev.firstChild.data = merged + else: + prev.appendChild(run.ownerDocument.createTextNode(merged)) + + if merged.startswith(" ") or merged.endswith(" "): + prev.setAttribute("xml:space", "preserve") + elif prev.hasAttribute("xml:space"): + prev.removeAttribute("xml:space") + + run.removeChild(curr) diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/helpers/simplify_redlines.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/helpers/simplify_redlines.py new file mode 100644 index 00000000..db963bb9 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/helpers/simplify_redlines.py @@ -0,0 +1,197 @@ +"""Simplify tracked changes by merging adjacent w:ins or w:del elements. + +Merges adjacent elements from the same author into a single element. +Same for elements. This makes heavily-redlined documents easier to +work with by reducing the number of tracked change wrappers. + +Rules: +- Only merges w:ins with w:ins, w:del with w:del (same element type) +- Only merges if same author (ignores timestamp differences) +- Only merges if truly adjacent (only whitespace between them) +""" + +import xml.etree.ElementTree as ET +import zipfile +from pathlib import Path + +import defusedxml.minidom + +WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + + +def simplify_redlines(input_dir: str) -> tuple[int, str]: + doc_xml = Path(input_dir) / "word" / "document.xml" + + if not doc_xml.exists(): + return 0, f"Error: {doc_xml} not found" + + try: + dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8")) + root = dom.documentElement + + merge_count = 0 + + containers = _find_elements(root, "p") + _find_elements(root, "tc") + + for container in containers: + merge_count += _merge_tracked_changes_in(container, "ins") + merge_count += _merge_tracked_changes_in(container, "del") + + doc_xml.write_bytes(dom.toxml(encoding="UTF-8")) + return merge_count, f"Simplified {merge_count} tracked changes" + + except Exception as e: + return 0, f"Error: {e}" + + +def _merge_tracked_changes_in(container, tag: str) -> int: + merge_count = 0 + + tracked = [ + child + for child in container.childNodes + if child.nodeType == child.ELEMENT_NODE and _is_element(child, tag) + ] + + if len(tracked) < 2: + return 0 + + i = 0 + while i < len(tracked) - 1: + curr = tracked[i] + next_elem = tracked[i + 1] + + if _can_merge_tracked(curr, next_elem): + _merge_tracked_content(curr, next_elem) + container.removeChild(next_elem) + tracked.pop(i + 1) + merge_count += 1 + else: + i += 1 + + return merge_count + + +def _is_element(node, tag: str) -> bool: + name = node.localName or node.tagName + return name == tag or name.endswith(f":{tag}") + + +def _get_author(elem) -> str: + author = elem.getAttribute("w:author") + if not author: + for attr in elem.attributes.values(): + if attr.localName == "author" or attr.name.endswith(":author"): + return attr.value + return author + + +def _can_merge_tracked(elem1, elem2) -> bool: + if _get_author(elem1) != _get_author(elem2): + return False + + node = elem1.nextSibling + while node and node != elem2: + if node.nodeType == node.ELEMENT_NODE: + return False + if node.nodeType == node.TEXT_NODE and node.data.strip(): + return False + node = node.nextSibling + + return True + + +def _merge_tracked_content(target, source): + while source.firstChild: + child = source.firstChild + source.removeChild(child) + target.appendChild(child) + + +def _find_elements(root, tag: str) -> list: + results = [] + + def traverse(node): + if node.nodeType == node.ELEMENT_NODE: + name = node.localName or node.tagName + if name == tag or name.endswith(f":{tag}"): + results.append(node) + for child in node.childNodes: + traverse(child) + + traverse(root) + return results + + +def get_tracked_change_authors(doc_xml_path: Path) -> dict[str, int]: + if not doc_xml_path.exists(): + return {} + + try: + tree = ET.parse(doc_xml_path) + root = tree.getroot() + except ET.ParseError: + return {} + + namespaces = {"w": WORD_NS} + author_attr = f"{{{WORD_NS}}}author" + + authors: dict[str, int] = {} + for tag in ["ins", "del"]: + for elem in root.findall(f".//w:{tag}", namespaces): + author = elem.get(author_attr) + if author: + authors[author] = authors.get(author, 0) + 1 + + return authors + + +def _get_authors_from_docx(docx_path: Path) -> dict[str, int]: + try: + with zipfile.ZipFile(docx_path, "r") as zf: + if "word/document.xml" not in zf.namelist(): + return {} + with zf.open("word/document.xml") as f: + tree = ET.parse(f) + root = tree.getroot() + + namespaces = {"w": WORD_NS} + author_attr = f"{{{WORD_NS}}}author" + + authors: dict[str, int] = {} + for tag in ["ins", "del"]: + for elem in root.findall(f".//w:{tag}", namespaces): + author = elem.get(author_attr) + if author: + authors[author] = authors.get(author, 0) + 1 + return authors + except (zipfile.BadZipFile, ET.ParseError): + return {} + + +def infer_author(modified_dir: Path, original_docx: Path, default: str = "Claude") -> str: + modified_xml = modified_dir / "word" / "document.xml" + modified_authors = get_tracked_change_authors(modified_xml) + + if not modified_authors: + return default + + original_authors = _get_authors_from_docx(original_docx) + + new_changes: dict[str, int] = {} + for author, count in modified_authors.items(): + original_count = original_authors.get(author, 0) + diff = count - original_count + if diff > 0: + new_changes[author] = diff + + if not new_changes: + return default + + if len(new_changes) == 1: + return next(iter(new_changes)) + + raise ValueError( + f"Multiple authors added new changes: {new_changes}. " + "Cannot infer which author to validate." + ) diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/pack.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/pack.py new file mode 100644 index 00000000..db29ed8b --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/pack.py @@ -0,0 +1,159 @@ +"""Pack a directory into a DOCX, PPTX, or XLSX file. + +Validates with auto-repair, condenses XML formatting, and creates the Office file. + +Usage: + python pack.py [--original ] [--validate true|false] + +Examples: + python pack.py unpacked/ output.docx --original input.docx + python pack.py unpacked/ output.pptx --validate false +""" + +import argparse +import sys +import shutil +import tempfile +import zipfile +from pathlib import Path + +import defusedxml.minidom + +from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator + +def pack( + input_directory: str, + output_file: str, + original_file: str | None = None, + validate: bool = True, + infer_author_func=None, +) -> tuple[None, str]: + input_dir = Path(input_directory) + output_path = Path(output_file) + suffix = output_path.suffix.lower() + + if not input_dir.is_dir(): + return None, f"Error: {input_dir} is not a directory" + + if suffix not in {".docx", ".pptx", ".xlsx"}: + return None, f"Error: {output_file} must be a .docx, .pptx, or .xlsx file" + + if validate and original_file: + original_path = Path(original_file) + if original_path.exists(): + success, output = _run_validation( + input_dir, original_path, suffix, infer_author_func + ) + if output: + print(output) + if not success: + return None, f"Error: Validation failed for {input_dir}" + + with tempfile.TemporaryDirectory() as temp_dir: + temp_content_dir = Path(temp_dir) / "content" + shutil.copytree(input_dir, temp_content_dir) + + for pattern in ["*.xml", "*.rels"]: + for xml_file in temp_content_dir.rglob(pattern): + _condense_xml(xml_file) + + output_path.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf: + for f in temp_content_dir.rglob("*"): + if f.is_file(): + zf.write(f, f.relative_to(temp_content_dir)) + + return None, f"Successfully packed {input_dir} to {output_file}" + + +def _run_validation( + unpacked_dir: Path, + original_file: Path, + suffix: str, + infer_author_func=None, +) -> tuple[bool, str | None]: + output_lines = [] + validators = [] + + if suffix == ".docx": + author = "Claude" + if infer_author_func: + try: + author = infer_author_func(unpacked_dir, original_file) + except ValueError as e: + print(f"Warning: {e} Using default author 'Claude'.", file=sys.stderr) + + validators = [ + DOCXSchemaValidator(unpacked_dir, original_file), + RedliningValidator(unpacked_dir, original_file, author=author), + ] + elif suffix == ".pptx": + validators = [PPTXSchemaValidator(unpacked_dir, original_file)] + + if not validators: + return True, None + + total_repairs = sum(v.repair() for v in validators) + if total_repairs: + output_lines.append(f"Auto-repaired {total_repairs} issue(s)") + + success = all(v.validate() for v in validators) + + if success: + output_lines.append("All validations PASSED!") + + return success, "\n".join(output_lines) if output_lines else None + + +def _condense_xml(xml_file: Path) -> None: + try: + with open(xml_file, encoding="utf-8") as f: + dom = defusedxml.minidom.parse(f) + + for element in dom.getElementsByTagName("*"): + if element.tagName.endswith(":t"): + continue + + for child in list(element.childNodes): + if ( + child.nodeType == child.TEXT_NODE + and child.nodeValue + and child.nodeValue.strip() == "" + ) or child.nodeType == child.COMMENT_NODE: + element.removeChild(child) + + xml_file.write_bytes(dom.toxml(encoding="UTF-8")) + except Exception as e: + print(f"ERROR: Failed to parse {xml_file.name}: {e}", file=sys.stderr) + raise + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Pack a directory into a DOCX, PPTX, or XLSX file" + ) + parser.add_argument("input_directory", help="Unpacked Office document directory") + parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx)") + parser.add_argument( + "--original", + help="Original file for validation comparison", + ) + parser.add_argument( + "--validate", + type=lambda x: x.lower() == "true", + default=True, + metavar="true|false", + help="Run validation with auto-repair (default: true)", + ) + args = parser.parse_args() + + _, message = pack( + args.input_directory, + args.output_file, + original_file=args.original, + validate=args.validate, + ) + print(message) + + if "Error" in message: + sys.exit(1) diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file mode 100644 index 00000000..6454ef9a --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd @@ -0,0 +1,1499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file mode 100644 index 00000000..afa4f463 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file mode 100644 index 00000000..64e66b8a --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd @@ -0,0 +1,1085 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file mode 100644 index 00000000..687eea82 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd @@ -0,0 +1,11 @@ + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file mode 100644 index 00000000..6ac81b06 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd @@ -0,0 +1,3081 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file mode 100644 index 00000000..1dbf0514 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file mode 100644 index 00000000..f1af17db --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file mode 100644 index 00000000..0a185ab6 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd new file mode 100644 index 00000000..14ef4888 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd @@ -0,0 +1,1676 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file mode 100644 index 00000000..c20f3bf1 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file mode 100644 index 00000000..ac602522 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file mode 100644 index 00000000..424b8ba8 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file mode 100644 index 00000000..2bddce29 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file mode 100644 index 00000000..8a8c18ba --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file mode 100644 index 00000000..5c42706a --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file mode 100644 index 00000000..853c341c --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file mode 100644 index 00000000..da835ee8 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file mode 100644 index 00000000..87ad2658 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file mode 100644 index 00000000..9e86f1b2 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd new file mode 100644 index 00000000..d0be42e7 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd @@ -0,0 +1,4439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file mode 100644 index 00000000..8821dd18 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd @@ -0,0 +1,570 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file mode 100644 index 00000000..ca2575c7 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file mode 100644 index 00000000..dd079e60 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file mode 100644 index 00000000..3dd6cf62 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file mode 100644 index 00000000..f1041e34 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd new file mode 100644 index 00000000..9c5b7a63 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd @@ -0,0 +1,3646 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd new file mode 100644 index 00000000..0f13678d --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd @@ -0,0 +1,116 @@ + + + + + + See http://www.w3.org/XML/1998/namespace.html and + http://www.w3.org/TR/REC-xml for information about this namespace. + + This schema document describes the XML namespace, in a form + suitable for import by other schema documents. + + Note that local names in this namespace are intended to be defined + only by the World Wide Web Consortium or its subgroups. The + following names are currently defined in this namespace and should + not be used with conflicting semantics by any Working Group, + specification, or document instance: + + base (as an attribute name): denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification. + + lang (as an attribute name): denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification. + + space (as an attribute name): denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification. + + Father (in any context at all): denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: + + In appreciation for his vision, leadership and dedication + the W3C XML Plenary on this 10th day of February, 2000 + reserves for Jon Bosak in perpetuity the XML name + xml:Father + + + + + This schema defines attributes and an attribute group + suitable for use by + schemas wishing to allow xml:base, xml:lang or xml:space attributes + on elements they define. + + To enable this, such a schema must import this schema + for the XML namespace, e.g. as follows: + <schema . . .> + . . . + <import namespace="http://www.w3.org/XML/1998/namespace" + schemaLocation="http://www.w3.org/2001/03/xml.xsd"/> + + Subsequently, qualified reference to any of the attributes + or the group defined below will have the desired effect, e.g. + + <type . . .> + . . . + <attributeGroup ref="xml:specialAttrs"/> + + will define a type which will schema-validate an instance + element with any of those attributes + + + + In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + http://www.w3.org/2001/03/xml.xsd. + At the date of issue it can also be found at + http://www.w3.org/2001/xml.xsd. + The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML Schema + itself. In other words, if the XML Schema namespace changes, the version + of this document at + http://www.w3.org/2001/xml.xsd will change + accordingly; the version at + http://www.w3.org/2001/03/xml.xsd will not change. + + + + + + In due course, we should install the relevant ISO 2- and 3-letter + codes as the enumerated possible values . . . + + + + + + + + + + + + + + + See http://www.w3.org/TR/xmlbase/ for + information about this attribute. + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file mode 100644 index 00000000..a6de9d27 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file mode 100644 index 00000000..10e978b6 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd new file mode 100644 index 00000000..4248bf7a --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd new file mode 100644 index 00000000..56497467 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/mce/mc.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/mce/mc.xsd new file mode 100644 index 00000000..ef725457 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/mce/mc.xsd @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-2010.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-2010.xsd new file mode 100644 index 00000000..f65f7777 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-2010.xsd @@ -0,0 +1,560 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-2012.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-2012.xsd new file mode 100644 index 00000000..6b00755a --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-2012.xsd @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-2018.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-2018.xsd new file mode 100644 index 00000000..f321d333 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-2018.xsd @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-cex-2018.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-cex-2018.xsd new file mode 100644 index 00000000..364c6a9b --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-cex-2018.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-cid-2016.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-cid-2016.xsd new file mode 100644 index 00000000..fed9d15b --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-cid-2016.xsd @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd new file mode 100644 index 00000000..680cf154 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd @@ -0,0 +1,4 @@ + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-symex-2015.xsd b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-symex-2015.xsd new file mode 100644 index 00000000..89ada908 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/schemas/microsoft/wml-symex-2015.xsd @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/soffice.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/soffice.py new file mode 100644 index 00000000..ab242954 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/soffice.py @@ -0,0 +1,184 @@ +""" +Helper for running LibreOffice (soffice) in environments where AF_UNIX +sockets may be blocked (e.g., sandboxed VMs). Detects the restriction +at runtime and applies an LD_PRELOAD shim if needed. + +Usage: + from office.soffice import run_soffice, get_soffice_env + + # Option 1 – run soffice directly + result = run_soffice(["--headless", "--convert-to", "pdf", "input.docx"]) + + # Option 2 – get env dict for your own subprocess calls + env = get_soffice_env() + subprocess.run(["soffice", ...], env=env) +""" + +import os +import socket +import subprocess +import tempfile +from pathlib import Path + + +def get_soffice_env() -> dict: + env = os.environ.copy() + env["SAL_USE_VCLPLUGIN"] = "svp" + + if _needs_shim(): + shim = _ensure_shim() + env["LD_PRELOAD"] = str(shim) + + return env + + +def run_soffice(args: list[str], **kwargs) -> subprocess.CompletedProcess: + env = get_soffice_env() + return subprocess.run(["soffice"] + args, env=env, **kwargs) + + + +_SHIM_SO = Path(tempfile.gettempdir()) / "lo_socket_shim.so" + + +def _needs_shim() -> bool: + """Check if AF_UNIX socket shim is needed (Linux/Unix only).""" + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.close() + return False + except (OSError, AttributeError): + return True + + +def _ensure_shim() -> Path: + if _SHIM_SO.exists(): + return _SHIM_SO + + src = Path(tempfile.gettempdir()) / "lo_socket_shim.c" + src.write_text(_SHIM_SOURCE) + subprocess.run( + ["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"], + check=True, + capture_output=True, + ) + src.unlink() + return _SHIM_SO + + + +_SHIM_SOURCE = r""" +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +static int (*real_socket)(int, int, int); +static int (*real_socketpair)(int, int, int, int[2]); +static int (*real_listen)(int, int); +static int (*real_accept)(int, struct sockaddr *, socklen_t *); +static int (*real_close)(int); +static int (*real_read)(int, void *, size_t); + +/* Per-FD bookkeeping (FDs >= 1024 are passed through unshimmed). */ +static int is_shimmed[1024]; +static int peer_of[1024]; +static int wake_r[1024]; /* accept() blocks reading this */ +static int wake_w[1024]; /* close() writes to this */ +static int listener_fd = -1; /* FD that received listen() */ + +__attribute__((constructor)) +static void init(void) { + real_socket = dlsym(RTLD_NEXT, "socket"); + real_socketpair = dlsym(RTLD_NEXT, "socketpair"); + real_listen = dlsym(RTLD_NEXT, "listen"); + real_accept = dlsym(RTLD_NEXT, "accept"); + real_close = dlsym(RTLD_NEXT, "close"); + real_read = dlsym(RTLD_NEXT, "read"); + for (int i = 0; i < 1024; i++) { + peer_of[i] = -1; + wake_r[i] = -1; + wake_w[i] = -1; + } +} + +/* ---- socket ---------------------------------------------------------- */ +int socket(int domain, int type, int protocol) { + if (domain == AF_UNIX) { + int fd = real_socket(domain, type, protocol); + if (fd >= 0) return fd; + /* socket(AF_UNIX) blocked – fall back to socketpair(). */ + int sv[2]; + if (real_socketpair(domain, type, protocol, sv) == 0) { + if (sv[0] >= 0 && sv[0] < 1024) { + is_shimmed[sv[0]] = 1; + peer_of[sv[0]] = sv[1]; + int wp[2]; + if (pipe(wp) == 0) { + wake_r[sv[0]] = wp[0]; + wake_w[sv[0]] = wp[1]; + } + } + return sv[0]; + } + errno = EPERM; + return -1; + } + return real_socket(domain, type, protocol); +} + +/* ---- listen ---------------------------------------------------------- */ +int listen(int sockfd, int backlog) { + if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) { + listener_fd = sockfd; + return 0; + } + return real_listen(sockfd, backlog); +} + +/* ---- accept ---------------------------------------------------------- */ +int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) { + if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) { + /* Block until close() writes to the wake pipe. */ + if (wake_r[sockfd] >= 0) { + char buf; + real_read(wake_r[sockfd], &buf, 1); + } + errno = ECONNABORTED; + return -1; + } + return real_accept(sockfd, addr, addrlen); +} + +/* ---- close ----------------------------------------------------------- */ +int close(int fd) { + if (fd >= 0 && fd < 1024 && is_shimmed[fd]) { + int was_listener = (fd == listener_fd); + is_shimmed[fd] = 0; + + if (wake_w[fd] >= 0) { /* unblock accept() */ + char c = 0; + write(wake_w[fd], &c, 1); + real_close(wake_w[fd]); + wake_w[fd] = -1; + } + if (wake_r[fd] >= 0) { real_close(wake_r[fd]); wake_r[fd] = -1; } + if (peer_of[fd] >= 0) { real_close(peer_of[fd]); peer_of[fd] = -1; } + + if (was_listener) + _exit(0); /* conversion done – exit */ + } + return real_close(fd); +} +""" + + + +if __name__ == "__main__": + import sys + result = run_soffice(sys.argv[1:]) + sys.exit(result.returncode) diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/unpack.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/unpack.py new file mode 100644 index 00000000..00152533 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/unpack.py @@ -0,0 +1,132 @@ +"""Unpack Office files (DOCX, PPTX, XLSX) for editing. + +Extracts the ZIP archive, pretty-prints XML files, and optionally: +- Merges adjacent runs with identical formatting (DOCX only) +- Simplifies adjacent tracked changes from same author (DOCX only) + +Usage: + python unpack.py [options] + +Examples: + python unpack.py document.docx unpacked/ + python unpack.py presentation.pptx unpacked/ + python unpack.py document.docx unpacked/ --merge-runs false +""" + +import argparse +import sys +import zipfile +from pathlib import Path + +import defusedxml.minidom + +from helpers.merge_runs import merge_runs as do_merge_runs +from helpers.simplify_redlines import simplify_redlines as do_simplify_redlines + +SMART_QUOTE_REPLACEMENTS = { + "\u201c": "“", + "\u201d": "”", + "\u2018": "‘", + "\u2019": "’", +} + + +def unpack( + input_file: str, + output_directory: str, + merge_runs: bool = True, + simplify_redlines: bool = True, +) -> tuple[None, str]: + input_path = Path(input_file) + output_path = Path(output_directory) + suffix = input_path.suffix.lower() + + if not input_path.exists(): + return None, f"Error: {input_file} does not exist" + + if suffix not in {".docx", ".pptx", ".xlsx"}: + return None, f"Error: {input_file} must be a .docx, .pptx, or .xlsx file" + + try: + output_path.mkdir(parents=True, exist_ok=True) + + with zipfile.ZipFile(input_path, "r") as zf: + zf.extractall(output_path) + + xml_files = list(output_path.rglob("*.xml")) + list(output_path.rglob("*.rels")) + for xml_file in xml_files: + _pretty_print_xml(xml_file) + + message = f"Unpacked {input_file} ({len(xml_files)} XML files)" + + if suffix == ".docx": + if simplify_redlines: + simplify_count, _ = do_simplify_redlines(str(output_path)) + message += f", simplified {simplify_count} tracked changes" + + if merge_runs: + merge_count, _ = do_merge_runs(str(output_path)) + message += f", merged {merge_count} runs" + + for xml_file in xml_files: + _escape_smart_quotes(xml_file) + + return None, message + + except zipfile.BadZipFile: + return None, f"Error: {input_file} is not a valid Office file" + except Exception as e: + return None, f"Error unpacking: {e}" + + +def _pretty_print_xml(xml_file: Path) -> None: + try: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + xml_file.write_bytes(dom.toprettyxml(indent=" ", encoding="utf-8")) + except Exception: + pass + + +def _escape_smart_quotes(xml_file: Path) -> None: + try: + content = xml_file.read_text(encoding="utf-8") + for char, entity in SMART_QUOTE_REPLACEMENTS.items(): + content = content.replace(char, entity) + xml_file.write_text(content, encoding="utf-8") + except Exception: + pass + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Unpack an Office file (DOCX, PPTX, XLSX) for editing" + ) + parser.add_argument("input_file", help="Office file to unpack") + parser.add_argument("output_directory", help="Output directory") + parser.add_argument( + "--merge-runs", + type=lambda x: x.lower() == "true", + default=True, + metavar="true|false", + help="Merge adjacent runs with identical formatting (DOCX only, default: true)", + ) + parser.add_argument( + "--simplify-redlines", + type=lambda x: x.lower() == "true", + default=True, + metavar="true|false", + help="Merge adjacent tracked changes from same author (DOCX only, default: true)", + ) + args = parser.parse_args() + + _, message = unpack( + args.input_file, + args.output_directory, + merge_runs=args.merge_runs, + simplify_redlines=args.simplify_redlines, + ) + print(message) + + if "Error" in message: + sys.exit(1) diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validate.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validate.py new file mode 100644 index 00000000..03b01f6e --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validate.py @@ -0,0 +1,111 @@ +""" +Command line tool to validate Office document XML files against XSD schemas and tracked changes. + +Usage: + python validate.py [--original ] [--auto-repair] [--author NAME] + +The first argument can be either: +- An unpacked directory containing the Office document XML files +- A packed Office file (.docx/.pptx/.xlsx) which will be unpacked to a temp directory + +Auto-repair fixes: +- paraId/durableId values that exceed OOXML limits +- Missing xml:space="preserve" on w:t elements with whitespace +""" + +import argparse +import sys +import tempfile +import zipfile +from pathlib import Path + +from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator + + +def main(): + parser = argparse.ArgumentParser(description="Validate Office document XML files") + parser.add_argument( + "path", + help="Path to unpacked directory or packed Office file (.docx/.pptx/.xlsx)", + ) + parser.add_argument( + "--original", + required=False, + default=None, + help="Path to original file (.docx/.pptx/.xlsx). If omitted, all XSD errors are reported and redlining validation is skipped.", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable verbose output", + ) + parser.add_argument( + "--auto-repair", + action="store_true", + help="Automatically repair common issues (hex IDs, whitespace preservation)", + ) + parser.add_argument( + "--author", + default="Claude", + help="Author name for redlining validation (default: Claude)", + ) + args = parser.parse_args() + + path = Path(args.path) + assert path.exists(), f"Error: {path} does not exist" + + original_file = None + if args.original: + original_file = Path(args.original) + assert original_file.is_file(), f"Error: {original_file} is not a file" + assert original_file.suffix.lower() in [".docx", ".pptx", ".xlsx"], ( + f"Error: {original_file} must be a .docx, .pptx, or .xlsx file" + ) + + file_extension = (original_file or path).suffix.lower() + assert file_extension in [".docx", ".pptx", ".xlsx"], ( + f"Error: Cannot determine file type from {path}. Use --original or provide a .docx/.pptx/.xlsx file." + ) + + if path.is_file() and path.suffix.lower() in [".docx", ".pptx", ".xlsx"]: + temp_dir = tempfile.mkdtemp() + with zipfile.ZipFile(path, "r") as zf: + zf.extractall(temp_dir) + unpacked_dir = Path(temp_dir) + else: + assert path.is_dir(), f"Error: {path} is not a directory or Office file" + unpacked_dir = path + + match file_extension: + case ".docx": + validators = [ + DOCXSchemaValidator(unpacked_dir, original_file, verbose=args.verbose), + ] + if original_file: + validators.append( + RedliningValidator(unpacked_dir, original_file, verbose=args.verbose, author=args.author) + ) + case ".pptx": + validators = [ + PPTXSchemaValidator(unpacked_dir, original_file, verbose=args.verbose), + ] + case _: + print(f"Error: Validation not supported for file type {file_extension}") + sys.exit(1) + + if args.auto_repair: + total_repairs = sum(v.repair() for v in validators) + if total_repairs: + print(f"Auto-repaired {total_repairs} issue(s)") + + success = all(v.validate() for v in validators) + + if success: + print("All validations PASSED!") + + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/__init__.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/__init__.py new file mode 100644 index 00000000..db092ece --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/__init__.py @@ -0,0 +1,15 @@ +""" +Validation modules for Word document processing. +""" + +from .base import BaseSchemaValidator +from .docx import DOCXSchemaValidator +from .pptx import PPTXSchemaValidator +from .redlining import RedliningValidator + +__all__ = [ + "BaseSchemaValidator", + "DOCXSchemaValidator", + "PPTXSchemaValidator", + "RedliningValidator", +] diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/base.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/base.py new file mode 100644 index 00000000..db4a06a2 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/base.py @@ -0,0 +1,847 @@ +""" +Base validator with common validation logic for document files. +""" + +import re +from pathlib import Path + +import defusedxml.minidom +import lxml.etree + + +class BaseSchemaValidator: + + IGNORED_VALIDATION_ERRORS = [ + "hyphenationZone", + "purl.org/dc/terms", + ] + + UNIQUE_ID_REQUIREMENTS = { + "comment": ("id", "file"), + "commentrangestart": ("id", "file"), + "commentrangeend": ("id", "file"), + "bookmarkstart": ("id", "file"), + "bookmarkend": ("id", "file"), + "sldid": ("id", "file"), + "sldmasterid": ("id", "global"), + "sldlayoutid": ("id", "global"), + "cm": ("authorid", "file"), + "sheet": ("sheetid", "file"), + "definedname": ("id", "file"), + "cxnsp": ("id", "file"), + "sp": ("id", "file"), + "pic": ("id", "file"), + "grpsp": ("id", "file"), + } + + EXCLUDED_ID_CONTAINERS = { + "sectionlst", + } + + ELEMENT_RELATIONSHIP_TYPES = {} + + SCHEMA_MAPPINGS = { + "word": "ISO-IEC29500-4_2016/wml.xsd", + "ppt": "ISO-IEC29500-4_2016/pml.xsd", + "xl": "ISO-IEC29500-4_2016/sml.xsd", + "[Content_Types].xml": "ecma/fouth-edition/opc-contentTypes.xsd", + "app.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd", + "core.xml": "ecma/fouth-edition/opc-coreProperties.xsd", + "custom.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd", + ".rels": "ecma/fouth-edition/opc-relationships.xsd", + "people.xml": "microsoft/wml-2012.xsd", + "commentsIds.xml": "microsoft/wml-cid-2016.xsd", + "commentsExtensible.xml": "microsoft/wml-cex-2018.xsd", + "commentsExtended.xml": "microsoft/wml-2012.xsd", + "chart": "ISO-IEC29500-4_2016/dml-chart.xsd", + "theme": "ISO-IEC29500-4_2016/dml-main.xsd", + "drawing": "ISO-IEC29500-4_2016/dml-main.xsd", + } + + MC_NAMESPACE = "http://schemas.openxmlformats.org/markup-compatibility/2006" + XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace" + + PACKAGE_RELATIONSHIPS_NAMESPACE = ( + "http://schemas.openxmlformats.org/package/2006/relationships" + ) + OFFICE_RELATIONSHIPS_NAMESPACE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + ) + CONTENT_TYPES_NAMESPACE = ( + "http://schemas.openxmlformats.org/package/2006/content-types" + ) + + MAIN_CONTENT_FOLDERS = {"word", "ppt", "xl"} + + OOXML_NAMESPACES = { + "http://schemas.openxmlformats.org/officeDocument/2006/math", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "http://schemas.openxmlformats.org/schemaLibrary/2006/main", + "http://schemas.openxmlformats.org/drawingml/2006/main", + "http://schemas.openxmlformats.org/drawingml/2006/chart", + "http://schemas.openxmlformats.org/drawingml/2006/chartDrawing", + "http://schemas.openxmlformats.org/drawingml/2006/diagram", + "http://schemas.openxmlformats.org/drawingml/2006/picture", + "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing", + "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "http://schemas.openxmlformats.org/presentationml/2006/main", + "http://schemas.openxmlformats.org/spreadsheetml/2006/main", + "http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes", + "http://www.w3.org/XML/1998/namespace", + } + + def __init__(self, unpacked_dir, original_file=None, verbose=False): + self.unpacked_dir = Path(unpacked_dir).resolve() + self.original_file = Path(original_file) if original_file else None + self.verbose = verbose + + self.schemas_dir = Path(__file__).parent.parent / "schemas" + + patterns = ["*.xml", "*.rels"] + self.xml_files = [ + f for pattern in patterns for f in self.unpacked_dir.rglob(pattern) + ] + + if not self.xml_files: + print(f"Warning: No XML files found in {self.unpacked_dir}") + + def validate(self): + raise NotImplementedError("Subclasses must implement the validate method") + + def repair(self) -> int: + return self.repair_whitespace_preservation() + + def repair_whitespace_preservation(self) -> int: + repairs = 0 + + for xml_file in self.xml_files: + try: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + modified = False + + for elem in dom.getElementsByTagName("*"): + if elem.tagName.endswith(":t") and elem.firstChild: + text = elem.firstChild.nodeValue + if text and (text.startswith((' ', '\t')) or text.endswith((' ', '\t'))): + if elem.getAttribute("xml:space") != "preserve": + elem.setAttribute("xml:space", "preserve") + text_preview = repr(text[:30]) + "..." if len(text) > 30 else repr(text) + print(f" Repaired: {xml_file.name}: Added xml:space='preserve' to {elem.tagName}: {text_preview}") + repairs += 1 + modified = True + + if modified: + xml_file.write_bytes(dom.toxml(encoding="UTF-8")) + + except Exception: + pass + + return repairs + + def validate_xml(self): + errors = [] + + for xml_file in self.xml_files: + try: + lxml.etree.parse(str(xml_file)) + except lxml.etree.XMLSyntaxError as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {e.lineno}: {e.msg}" + ) + except Exception as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Unexpected error: {str(e)}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} XML violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All XML files are well-formed") + return True + + def validate_namespaces(self): + errors = [] + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + declared = set(root.nsmap.keys()) - {None} + + for attr_val in [ + v for k, v in root.attrib.items() if k.endswith("Ignorable") + ]: + undeclared = set(attr_val.split()) - declared + errors.extend( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Namespace '{ns}' in Ignorable but not declared" + for ns in undeclared + ) + except lxml.etree.XMLSyntaxError: + continue + + if errors: + print(f"FAILED - {len(errors)} namespace issues:") + for error in errors: + print(error) + return False + if self.verbose: + print("PASSED - All namespace prefixes properly declared") + return True + + def validate_unique_ids(self): + errors = [] + global_ids = {} + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + file_ids = {} + + mc_elements = root.xpath( + ".//mc:AlternateContent", namespaces={"mc": self.MC_NAMESPACE} + ) + for elem in mc_elements: + elem.getparent().remove(elem) + + for elem in root.iter(): + tag = ( + elem.tag.split("}")[-1].lower() + if "}" in elem.tag + else elem.tag.lower() + ) + + if tag in self.UNIQUE_ID_REQUIREMENTS: + in_excluded_container = any( + ancestor.tag.split("}")[-1].lower() in self.EXCLUDED_ID_CONTAINERS + for ancestor in elem.iterancestors() + ) + if in_excluded_container: + continue + + attr_name, scope = self.UNIQUE_ID_REQUIREMENTS[tag] + + id_value = None + for attr, value in elem.attrib.items(): + attr_local = ( + attr.split("}")[-1].lower() + if "}" in attr + else attr.lower() + ) + if attr_local == attr_name: + id_value = value + break + + if id_value is not None: + if scope == "global": + if id_value in global_ids: + prev_file, prev_line, prev_tag = global_ids[ + id_value + ] + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: Global ID '{id_value}' in <{tag}> " + f"already used in {prev_file} at line {prev_line} in <{prev_tag}>" + ) + else: + global_ids[id_value] = ( + xml_file.relative_to(self.unpacked_dir), + elem.sourceline, + tag, + ) + elif scope == "file": + key = (tag, attr_name) + if key not in file_ids: + file_ids[key] = {} + + if id_value in file_ids[key]: + prev_line = file_ids[key][id_value] + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: Duplicate {attr_name}='{id_value}' in <{tag}> " + f"(first occurrence at line {prev_line})" + ) + else: + file_ids[key][id_value] = elem.sourceline + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} ID uniqueness violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All required IDs are unique") + return True + + def validate_file_references(self): + errors = [] + + rels_files = list(self.unpacked_dir.rglob("*.rels")) + + if not rels_files: + if self.verbose: + print("PASSED - No .rels files found") + return True + + all_files = [] + for file_path in self.unpacked_dir.rglob("*"): + if ( + file_path.is_file() + and file_path.name != "[Content_Types].xml" + and not file_path.name.endswith(".rels") + ): + all_files.append(file_path.resolve()) + + all_referenced_files = set() + + if self.verbose: + print( + f"Found {len(rels_files)} .rels files and {len(all_files)} target files" + ) + + for rels_file in rels_files: + try: + rels_root = lxml.etree.parse(str(rels_file)).getroot() + + rels_dir = rels_file.parent + + referenced_files = set() + broken_refs = [] + + for rel in rels_root.findall( + ".//ns:Relationship", + namespaces={"ns": self.PACKAGE_RELATIONSHIPS_NAMESPACE}, + ): + target = rel.get("Target") + if target and not target.startswith( + ("http", "mailto:") + ): + if target.startswith("/"): + target_path = self.unpacked_dir / target.lstrip("/") + elif rels_file.name == ".rels": + target_path = self.unpacked_dir / target + else: + base_dir = rels_dir.parent + target_path = base_dir / target + + try: + target_path = target_path.resolve() + if target_path.exists() and target_path.is_file(): + referenced_files.add(target_path) + all_referenced_files.add(target_path) + else: + broken_refs.append((target, rel.sourceline)) + except (OSError, ValueError): + broken_refs.append((target, rel.sourceline)) + + if broken_refs: + rel_path = rels_file.relative_to(self.unpacked_dir) + for broken_ref, line_num in broken_refs: + errors.append( + f" {rel_path}: Line {line_num}: Broken reference to {broken_ref}" + ) + + except Exception as e: + rel_path = rels_file.relative_to(self.unpacked_dir) + errors.append(f" Error parsing {rel_path}: {e}") + + unreferenced_files = set(all_files) - all_referenced_files + + if unreferenced_files: + for unref_file in sorted(unreferenced_files): + unref_rel_path = unref_file.relative_to(self.unpacked_dir) + errors.append(f" Unreferenced file: {unref_rel_path}") + + if errors: + print(f"FAILED - Found {len(errors)} relationship validation errors:") + for error in errors: + print(error) + print( + "CRITICAL: These errors will cause the document to appear corrupt. " + + "Broken references MUST be fixed, " + + "and unreferenced files MUST be referenced or removed." + ) + return False + else: + if self.verbose: + print( + "PASSED - All references are valid and all files are properly referenced" + ) + return True + + def validate_all_relationship_ids(self): + import lxml.etree + + errors = [] + + for xml_file in self.xml_files: + if xml_file.suffix == ".rels": + continue + + rels_dir = xml_file.parent / "_rels" + rels_file = rels_dir / f"{xml_file.name}.rels" + + if not rels_file.exists(): + continue + + try: + rels_root = lxml.etree.parse(str(rels_file)).getroot() + rid_to_type = {} + + for rel in rels_root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rid = rel.get("Id") + rel_type = rel.get("Type", "") + if rid: + if rid in rid_to_type: + rels_rel_path = rels_file.relative_to(self.unpacked_dir) + errors.append( + f" {rels_rel_path}: Line {rel.sourceline}: " + f"Duplicate relationship ID '{rid}' (IDs must be unique)" + ) + type_name = ( + rel_type.split("/")[-1] if "/" in rel_type else rel_type + ) + rid_to_type[rid] = type_name + + xml_root = lxml.etree.parse(str(xml_file)).getroot() + + r_ns = self.OFFICE_RELATIONSHIPS_NAMESPACE + rid_attrs_to_check = ["id", "embed", "link"] + for elem in xml_root.iter(): + for attr_name in rid_attrs_to_check: + rid_attr = elem.get(f"{{{r_ns}}}{attr_name}") + if not rid_attr: + continue + xml_rel_path = xml_file.relative_to(self.unpacked_dir) + elem_name = ( + elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag + ) + + if rid_attr not in rid_to_type: + errors.append( + f" {xml_rel_path}: Line {elem.sourceline}: " + f"<{elem_name}> r:{attr_name} references non-existent relationship '{rid_attr}' " + f"(valid IDs: {', '.join(sorted(rid_to_type.keys())[:5])}{'...' if len(rid_to_type) > 5 else ''})" + ) + elif attr_name == "id" and self.ELEMENT_RELATIONSHIP_TYPES: + expected_type = self._get_expected_relationship_type( + elem_name + ) + if expected_type: + actual_type = rid_to_type[rid_attr] + if expected_type not in actual_type.lower(): + errors.append( + f" {xml_rel_path}: Line {elem.sourceline}: " + f"<{elem_name}> references '{rid_attr}' which points to '{actual_type}' " + f"but should point to a '{expected_type}' relationship" + ) + + except Exception as e: + xml_rel_path = xml_file.relative_to(self.unpacked_dir) + errors.append(f" Error processing {xml_rel_path}: {e}") + + if errors: + print(f"FAILED - Found {len(errors)} relationship ID reference errors:") + for error in errors: + print(error) + print("\nThese ID mismatches will cause the document to appear corrupt!") + return False + else: + if self.verbose: + print("PASSED - All relationship ID references are valid") + return True + + def _get_expected_relationship_type(self, element_name): + elem_lower = element_name.lower() + + if elem_lower in self.ELEMENT_RELATIONSHIP_TYPES: + return self.ELEMENT_RELATIONSHIP_TYPES[elem_lower] + + if elem_lower.endswith("id") and len(elem_lower) > 2: + prefix = elem_lower[:-2] + if prefix.endswith("master"): + return prefix.lower() + elif prefix.endswith("layout"): + return prefix.lower() + else: + if prefix == "sld": + return "slide" + return prefix.lower() + + if elem_lower.endswith("reference") and len(elem_lower) > 9: + prefix = elem_lower[:-9] + return prefix.lower() + + return None + + def validate_content_types(self): + errors = [] + + content_types_file = self.unpacked_dir / "[Content_Types].xml" + if not content_types_file.exists(): + print("FAILED - [Content_Types].xml file not found") + return False + + try: + root = lxml.etree.parse(str(content_types_file)).getroot() + declared_parts = set() + declared_extensions = set() + + for override in root.findall( + f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Override" + ): + part_name = override.get("PartName") + if part_name is not None: + declared_parts.add(part_name.lstrip("/")) + + for default in root.findall( + f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Default" + ): + extension = default.get("Extension") + if extension is not None: + declared_extensions.add(extension.lower()) + + declarable_roots = { + "sld", + "sldLayout", + "sldMaster", + "presentation", + "document", + "workbook", + "worksheet", + "theme", + } + + media_extensions = { + "png": "image/png", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "gif": "image/gif", + "bmp": "image/bmp", + "tiff": "image/tiff", + "wmf": "image/x-wmf", + "emf": "image/x-emf", + } + + all_files = list(self.unpacked_dir.rglob("*")) + all_files = [f for f in all_files if f.is_file()] + + for xml_file in self.xml_files: + path_str = str(xml_file.relative_to(self.unpacked_dir)).replace( + "\\", "/" + ) + + if any( + skip in path_str + for skip in [".rels", "[Content_Types]", "docProps/", "_rels/"] + ): + continue + + try: + root_tag = lxml.etree.parse(str(xml_file)).getroot().tag + root_name = root_tag.split("}")[-1] if "}" in root_tag else root_tag + + if root_name in declarable_roots and path_str not in declared_parts: + errors.append( + f" {path_str}: File with <{root_name}> root not declared in [Content_Types].xml" + ) + + except Exception: + continue + + for file_path in all_files: + if file_path.suffix.lower() in {".xml", ".rels"}: + continue + if file_path.name == "[Content_Types].xml": + continue + if "_rels" in file_path.parts or "docProps" in file_path.parts: + continue + + extension = file_path.suffix.lstrip(".").lower() + if extension and extension not in declared_extensions: + if extension in media_extensions: + relative_path = file_path.relative_to(self.unpacked_dir) + errors.append( + f' {relative_path}: File with extension \'{extension}\' not declared in [Content_Types].xml - should add: ' + ) + + except Exception as e: + errors.append(f" Error parsing [Content_Types].xml: {e}") + + if errors: + print(f"FAILED - Found {len(errors)} content type declaration errors:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print( + "PASSED - All content files are properly declared in [Content_Types].xml" + ) + return True + + def validate_file_against_xsd(self, xml_file, verbose=False): + xml_file = Path(xml_file).resolve() + unpacked_dir = self.unpacked_dir.resolve() + + is_valid, current_errors = self._validate_single_file_xsd( + xml_file, unpacked_dir + ) + + if is_valid is None: + return None, set() + elif is_valid: + return True, set() + + original_errors = self._get_original_file_errors(xml_file) + + assert current_errors is not None + new_errors = current_errors - original_errors + + new_errors = { + e for e in new_errors + if not any(pattern in e for pattern in self.IGNORED_VALIDATION_ERRORS) + } + + if new_errors: + if verbose: + relative_path = xml_file.relative_to(unpacked_dir) + print(f"FAILED - {relative_path}: {len(new_errors)} new error(s)") + for error in list(new_errors)[:3]: + truncated = error[:250] + "..." if len(error) > 250 else error + print(f" - {truncated}") + return False, new_errors + else: + if verbose: + print( + f"PASSED - No new errors (original had {len(current_errors)} errors)" + ) + return True, set() + + def validate_against_xsd(self): + new_errors = [] + original_error_count = 0 + valid_count = 0 + skipped_count = 0 + + for xml_file in self.xml_files: + relative_path = str(xml_file.relative_to(self.unpacked_dir)) + is_valid, new_file_errors = self.validate_file_against_xsd( + xml_file, verbose=False + ) + + if is_valid is None: + skipped_count += 1 + continue + elif is_valid and not new_file_errors: + valid_count += 1 + continue + elif is_valid: + original_error_count += 1 + valid_count += 1 + continue + + new_errors.append(f" {relative_path}: {len(new_file_errors)} new error(s)") + for error in list(new_file_errors)[:3]: + new_errors.append( + f" - {error[:250]}..." if len(error) > 250 else f" - {error}" + ) + + if self.verbose: + print(f"Validated {len(self.xml_files)} files:") + print(f" - Valid: {valid_count}") + print(f" - Skipped (no schema): {skipped_count}") + if original_error_count: + print(f" - With original errors (ignored): {original_error_count}") + print( + f" - With NEW errors: {len(new_errors) > 0 and len([e for e in new_errors if not e.startswith(' ')]) or 0}" + ) + + if new_errors: + print("\nFAILED - Found NEW validation errors:") + for error in new_errors: + print(error) + return False + else: + if self.verbose: + print("\nPASSED - No new XSD validation errors introduced") + return True + + def _get_schema_path(self, xml_file): + if xml_file.name in self.SCHEMA_MAPPINGS: + return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.name] + + if xml_file.suffix == ".rels": + return self.schemas_dir / self.SCHEMA_MAPPINGS[".rels"] + + if "charts/" in str(xml_file) and xml_file.name.startswith("chart"): + return self.schemas_dir / self.SCHEMA_MAPPINGS["chart"] + + if "theme/" in str(xml_file) and xml_file.name.startswith("theme"): + return self.schemas_dir / self.SCHEMA_MAPPINGS["theme"] + + if xml_file.parent.name in self.MAIN_CONTENT_FOLDERS: + return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.parent.name] + + return None + + def _clean_ignorable_namespaces(self, xml_doc): + xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") + xml_copy = lxml.etree.fromstring(xml_string) + + for elem in xml_copy.iter(): + attrs_to_remove = [] + + for attr in elem.attrib: + if "{" in attr: + ns = attr.split("}")[0][1:] + if ns not in self.OOXML_NAMESPACES: + attrs_to_remove.append(attr) + + for attr in attrs_to_remove: + del elem.attrib[attr] + + self._remove_ignorable_elements(xml_copy) + + return lxml.etree.ElementTree(xml_copy) + + def _remove_ignorable_elements(self, root): + elements_to_remove = [] + + for elem in list(root): + if not hasattr(elem, "tag") or callable(elem.tag): + continue + + tag_str = str(elem.tag) + if tag_str.startswith("{"): + ns = tag_str.split("}")[0][1:] + if ns not in self.OOXML_NAMESPACES: + elements_to_remove.append(elem) + continue + + self._remove_ignorable_elements(elem) + + for elem in elements_to_remove: + root.remove(elem) + + def _preprocess_for_mc_ignorable(self, xml_doc): + root = xml_doc.getroot() + + if f"{{{self.MC_NAMESPACE}}}Ignorable" in root.attrib: + del root.attrib[f"{{{self.MC_NAMESPACE}}}Ignorable"] + + return xml_doc + + def _validate_single_file_xsd(self, xml_file, base_path): + schema_path = self._get_schema_path(xml_file) + if not schema_path: + return None, None + + try: + with open(schema_path, "rb") as xsd_file: + parser = lxml.etree.XMLParser() + xsd_doc = lxml.etree.parse( + xsd_file, parser=parser, base_url=str(schema_path) + ) + schema = lxml.etree.XMLSchema(xsd_doc) + + with open(xml_file, "r") as f: + xml_doc = lxml.etree.parse(f) + + xml_doc, _ = self._remove_template_tags_from_text_nodes(xml_doc) + xml_doc = self._preprocess_for_mc_ignorable(xml_doc) + + relative_path = xml_file.relative_to(base_path) + if ( + relative_path.parts + and relative_path.parts[0] in self.MAIN_CONTENT_FOLDERS + ): + xml_doc = self._clean_ignorable_namespaces(xml_doc) + + if schema.validate(xml_doc): + return True, set() + else: + errors = set() + for error in schema.error_log: + errors.add(error.message) + return False, errors + + except Exception as e: + return False, {str(e)} + + def _get_original_file_errors(self, xml_file): + if self.original_file is None: + return set() + + import tempfile + import zipfile + + xml_file = Path(xml_file).resolve() + unpacked_dir = self.unpacked_dir.resolve() + relative_path = xml_file.relative_to(unpacked_dir) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + with zipfile.ZipFile(self.original_file, "r") as zip_ref: + zip_ref.extractall(temp_path) + + original_xml_file = temp_path / relative_path + + if not original_xml_file.exists(): + return set() + + is_valid, errors = self._validate_single_file_xsd( + original_xml_file, temp_path + ) + return errors if errors else set() + + def _remove_template_tags_from_text_nodes(self, xml_doc): + warnings = [] + template_pattern = re.compile(r"\{\{[^}]*\}\}") + + xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") + xml_copy = lxml.etree.fromstring(xml_string) + + def process_text_content(text, content_type): + if not text: + return text + matches = list(template_pattern.finditer(text)) + if matches: + for match in matches: + warnings.append( + f"Found template tag in {content_type}: {match.group()}" + ) + return template_pattern.sub("", text) + return text + + for elem in xml_copy.iter(): + if not hasattr(elem, "tag") or callable(elem.tag): + continue + tag_str = str(elem.tag) + if tag_str.endswith("}t") or tag_str == "t": + continue + + elem.text = process_text_content(elem.text, "text content") + elem.tail = process_text_content(elem.tail, "tail content") + + return lxml.etree.ElementTree(xml_copy), warnings + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/docx.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/docx.py new file mode 100644 index 00000000..fec405e6 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/docx.py @@ -0,0 +1,446 @@ +""" +Validator for Word document XML files against XSD schemas. +""" + +import random +import re +import tempfile +import zipfile + +import defusedxml.minidom +import lxml.etree + +from .base import BaseSchemaValidator + + +class DOCXSchemaValidator(BaseSchemaValidator): + + WORD_2006_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + W14_NAMESPACE = "http://schemas.microsoft.com/office/word/2010/wordml" + W16CID_NAMESPACE = "http://schemas.microsoft.com/office/word/2016/wordml/cid" + + ELEMENT_RELATIONSHIP_TYPES = {} + + def validate(self): + if not self.validate_xml(): + return False + + all_valid = True + if not self.validate_namespaces(): + all_valid = False + + if not self.validate_unique_ids(): + all_valid = False + + if not self.validate_file_references(): + all_valid = False + + if not self.validate_content_types(): + all_valid = False + + if not self.validate_against_xsd(): + all_valid = False + + if not self.validate_whitespace_preservation(): + all_valid = False + + if not self.validate_deletions(): + all_valid = False + + if not self.validate_insertions(): + all_valid = False + + if not self.validate_all_relationship_ids(): + all_valid = False + + if not self.validate_id_constraints(): + all_valid = False + + if not self.validate_comment_markers(): + all_valid = False + + self.compare_paragraph_counts() + + return all_valid + + def validate_whitespace_preservation(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"): + if elem.text: + text = elem.text + if re.search(r"^[ \t\n\r]", text) or re.search( + r"[ \t\n\r]$", text + ): + xml_space_attr = f"{{{self.XML_NAMESPACE}}}space" + if ( + xml_space_attr not in elem.attrib + or elem.attrib[xml_space_attr] != "preserve" + ): + text_preview = ( + repr(text)[:50] + "..." + if len(repr(text)) > 50 + else repr(text) + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: w:t element with whitespace missing xml:space='preserve': {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} whitespace preservation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All whitespace is properly preserved") + return True + + def validate_deletions(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + for t_elem in root.xpath(".//w:del//w:t", namespaces=namespaces): + if t_elem.text: + text_preview = ( + repr(t_elem.text)[:50] + "..." + if len(repr(t_elem.text)) > 50 + else repr(t_elem.text) + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {t_elem.sourceline}: found within : {text_preview}" + ) + + for instr_elem in root.xpath( + ".//w:del//w:instrText", namespaces=namespaces + ): + text_preview = ( + repr(instr_elem.text or "")[:50] + "..." + if len(repr(instr_elem.text or "")) > 50 + else repr(instr_elem.text or "") + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {instr_elem.sourceline}: found within (use ): {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} deletion validation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - No w:t elements found within w:del elements") + return True + + def count_paragraphs_in_unpacked(self): + count = 0 + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") + count = len(paragraphs) + except Exception as e: + print(f"Error counting paragraphs in unpacked document: {e}") + + return count + + def count_paragraphs_in_original(self): + original = self.original_file + if original is None: + return 0 + + count = 0 + + try: + with tempfile.TemporaryDirectory() as temp_dir: + with zipfile.ZipFile(original, "r") as zip_ref: + zip_ref.extractall(temp_dir) + + doc_xml_path = temp_dir + "/word/document.xml" + root = lxml.etree.parse(doc_xml_path).getroot() + + paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") + count = len(paragraphs) + + except Exception as e: + print(f"Error counting paragraphs in original document: {e}") + + return count + + def validate_insertions(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + invalid_elements = root.xpath( + ".//w:ins//w:delText[not(ancestor::w:del)]", namespaces=namespaces + ) + + for elem in invalid_elements: + text_preview = ( + repr(elem.text or "")[:50] + "..." + if len(repr(elem.text or "")) > 50 + else repr(elem.text or "") + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: within : {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} insertion validation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - No w:delText elements within w:ins elements") + return True + + def compare_paragraph_counts(self): + original_count = self.count_paragraphs_in_original() + new_count = self.count_paragraphs_in_unpacked() + + diff = new_count - original_count + diff_str = f"+{diff}" if diff > 0 else str(diff) + print(f"\nParagraphs: {original_count} → {new_count} ({diff_str})") + + def _parse_id_value(self, val: str, base: int = 16) -> int: + return int(val, base) + + def validate_id_constraints(self): + errors = [] + para_id_attr = f"{{{self.W14_NAMESPACE}}}paraId" + durable_id_attr = f"{{{self.W16CID_NAMESPACE}}}durableId" + + for xml_file in self.xml_files: + try: + for elem in lxml.etree.parse(str(xml_file)).iter(): + if val := elem.get(para_id_attr): + if self._parse_id_value(val, base=16) >= 0x80000000: + errors.append( + f" {xml_file.name}:{elem.sourceline}: paraId={val} >= 0x80000000" + ) + + if val := elem.get(durable_id_attr): + if xml_file.name == "numbering.xml": + try: + if self._parse_id_value(val, base=10) >= 0x7FFFFFFF: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} >= 0x7FFFFFFF" + ) + except ValueError: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} must be decimal in numbering.xml" + ) + else: + if self._parse_id_value(val, base=16) >= 0x7FFFFFFF: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} >= 0x7FFFFFFF" + ) + except Exception: + pass + + if errors: + print(f"FAILED - {len(errors)} ID constraint violations:") + for e in errors: + print(e) + elif self.verbose: + print("PASSED - All paraId/durableId values within constraints") + return not errors + + def validate_comment_markers(self): + errors = [] + + document_xml = None + comments_xml = None + for xml_file in self.xml_files: + if xml_file.name == "document.xml" and "word" in str(xml_file): + document_xml = xml_file + elif xml_file.name == "comments.xml": + comments_xml = xml_file + + if not document_xml: + if self.verbose: + print("PASSED - No document.xml found (skipping comment validation)") + return True + + try: + doc_root = lxml.etree.parse(str(document_xml)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + range_starts = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentRangeStart", namespaces=namespaces + ) + } + range_ends = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentRangeEnd", namespaces=namespaces + ) + } + references = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentReference", namespaces=namespaces + ) + } + + orphaned_ends = range_ends - range_starts + for comment_id in sorted( + orphaned_ends, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + errors.append( + f' document.xml: commentRangeEnd id="{comment_id}" has no matching commentRangeStart' + ) + + orphaned_starts = range_starts - range_ends + for comment_id in sorted( + orphaned_starts, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + errors.append( + f' document.xml: commentRangeStart id="{comment_id}" has no matching commentRangeEnd' + ) + + comment_ids = set() + if comments_xml and comments_xml.exists(): + comments_root = lxml.etree.parse(str(comments_xml)).getroot() + comment_ids = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in comments_root.xpath( + ".//w:comment", namespaces=namespaces + ) + } + + marker_ids = range_starts | range_ends | references + invalid_refs = marker_ids - comment_ids + for comment_id in sorted( + invalid_refs, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + if comment_id: + errors.append( + f' document.xml: marker id="{comment_id}" references non-existent comment' + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append(f" Error parsing XML: {e}") + + if errors: + print(f"FAILED - {len(errors)} comment marker violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All comment markers properly paired") + return True + + def repair(self) -> int: + repairs = super().repair() + repairs += self.repair_durableId() + return repairs + + def repair_durableId(self) -> int: + repairs = 0 + + for xml_file in self.xml_files: + try: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + modified = False + + for elem in dom.getElementsByTagName("*"): + if not elem.hasAttribute("w16cid:durableId"): + continue + + durable_id = elem.getAttribute("w16cid:durableId") + needs_repair = False + + if xml_file.name == "numbering.xml": + try: + needs_repair = ( + self._parse_id_value(durable_id, base=10) >= 0x7FFFFFFF + ) + except ValueError: + needs_repair = True + else: + try: + needs_repair = ( + self._parse_id_value(durable_id, base=16) >= 0x7FFFFFFF + ) + except ValueError: + needs_repair = True + + if needs_repair: + value = random.randint(1, 0x7FFFFFFE) + if xml_file.name == "numbering.xml": + new_id = str(value) + else: + new_id = f"{value:08X}" + + elem.setAttribute("w16cid:durableId", new_id) + print( + f" Repaired: {xml_file.name}: durableId {durable_id} → {new_id}" + ) + repairs += 1 + modified = True + + if modified: + xml_file.write_bytes(dom.toxml(encoding="UTF-8")) + + except Exception: + pass + + return repairs + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/pptx.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/pptx.py new file mode 100644 index 00000000..09842aa9 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/pptx.py @@ -0,0 +1,275 @@ +""" +Validator for PowerPoint presentation XML files against XSD schemas. +""" + +import re + +from .base import BaseSchemaValidator + + +class PPTXSchemaValidator(BaseSchemaValidator): + + PRESENTATIONML_NAMESPACE = ( + "http://schemas.openxmlformats.org/presentationml/2006/main" + ) + + ELEMENT_RELATIONSHIP_TYPES = { + "sldid": "slide", + "sldmasterid": "slidemaster", + "notesmasterid": "notesmaster", + "sldlayoutid": "slidelayout", + "themeid": "theme", + "tablestyleid": "tablestyles", + } + + def validate(self): + if not self.validate_xml(): + return False + + all_valid = True + if not self.validate_namespaces(): + all_valid = False + + if not self.validate_unique_ids(): + all_valid = False + + if not self.validate_uuid_ids(): + all_valid = False + + if not self.validate_file_references(): + all_valid = False + + if not self.validate_slide_layout_ids(): + all_valid = False + + if not self.validate_content_types(): + all_valid = False + + if not self.validate_against_xsd(): + all_valid = False + + if not self.validate_notes_slide_references(): + all_valid = False + + if not self.validate_all_relationship_ids(): + all_valid = False + + if not self.validate_no_duplicate_slide_layouts(): + all_valid = False + + return all_valid + + def validate_uuid_ids(self): + import lxml.etree + + errors = [] + uuid_pattern = re.compile( + r"^[\{\(]?[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}[\}\)]?$" + ) + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + for elem in root.iter(): + for attr, value in elem.attrib.items(): + attr_name = attr.split("}")[-1].lower() + if attr_name == "id" or attr_name.endswith("id"): + if self._looks_like_uuid(value): + if not uuid_pattern.match(value): + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: ID '{value}' appears to be a UUID but contains invalid hex characters" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} UUID ID validation errors:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All UUID-like IDs contain valid hex values") + return True + + def _looks_like_uuid(self, value): + clean_value = value.strip("{}()").replace("-", "") + return len(clean_value) == 32 and all(c.isalnum() for c in clean_value) + + def validate_slide_layout_ids(self): + import lxml.etree + + errors = [] + + slide_masters = list(self.unpacked_dir.glob("ppt/slideMasters/*.xml")) + + if not slide_masters: + if self.verbose: + print("PASSED - No slide masters found") + return True + + for slide_master in slide_masters: + try: + root = lxml.etree.parse(str(slide_master)).getroot() + + rels_file = slide_master.parent / "_rels" / f"{slide_master.name}.rels" + + if not rels_file.exists(): + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: " + f"Missing relationships file: {rels_file.relative_to(self.unpacked_dir)}" + ) + continue + + rels_root = lxml.etree.parse(str(rels_file)).getroot() + + valid_layout_rids = set() + for rel in rels_root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rel_type = rel.get("Type", "") + if "slideLayout" in rel_type: + valid_layout_rids.add(rel.get("Id")) + + for sld_layout_id in root.findall( + f".//{{{self.PRESENTATIONML_NAMESPACE}}}sldLayoutId" + ): + r_id = sld_layout_id.get( + f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id" + ) + layout_id = sld_layout_id.get("id") + + if r_id and r_id not in valid_layout_rids: + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: " + f"Line {sld_layout_id.sourceline}: sldLayoutId with id='{layout_id}' " + f"references r:id='{r_id}' which is not found in slide layout relationships" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} slide layout ID validation errors:") + for error in errors: + print(error) + print( + "Remove invalid references or add missing slide layouts to the relationships file." + ) + return False + else: + if self.verbose: + print("PASSED - All slide layout IDs reference valid slide layouts") + return True + + def validate_no_duplicate_slide_layouts(self): + import lxml.etree + + errors = [] + slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) + + for rels_file in slide_rels_files: + try: + root = lxml.etree.parse(str(rels_file)).getroot() + + layout_rels = [ + rel + for rel in root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ) + if "slideLayout" in rel.get("Type", "") + ] + + if len(layout_rels) > 1: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: has {len(layout_rels)} slideLayout references" + ) + + except Exception as e: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print("FAILED - Found slides with duplicate slideLayout references:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All slides have exactly one slideLayout reference") + return True + + def validate_notes_slide_references(self): + import lxml.etree + + errors = [] + notes_slide_references = {} + + slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) + + if not slide_rels_files: + if self.verbose: + print("PASSED - No slide relationship files found") + return True + + for rels_file in slide_rels_files: + try: + root = lxml.etree.parse(str(rels_file)).getroot() + + for rel in root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rel_type = rel.get("Type", "") + if "notesSlide" in rel_type: + target = rel.get("Target", "") + if target: + normalized_target = target.replace("../", "") + + slide_name = rels_file.stem.replace( + ".xml", "" + ) + + if normalized_target not in notes_slide_references: + notes_slide_references[normalized_target] = [] + notes_slide_references[normalized_target].append( + (slide_name, rels_file) + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + for target, references in notes_slide_references.items(): + if len(references) > 1: + slide_names = [ref[0] for ref in references] + errors.append( + f" Notes slide '{target}' is referenced by multiple slides: {', '.join(slide_names)}" + ) + for slide_name, rels_file in references: + errors.append(f" - {rels_file.relative_to(self.unpacked_dir)}") + + if errors: + print( + f"FAILED - Found {len([e for e in errors if not e.startswith(' ')])} notes slide reference validation errors:" + ) + for error in errors: + print(error) + print("Each slide may optionally have its own slide file.") + return False + else: + if self.verbose: + print("PASSED - All notes slide references are unique") + return True + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/redlining.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/redlining.py new file mode 100644 index 00000000..71c81b6b --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/office/validators/redlining.py @@ -0,0 +1,247 @@ +""" +Validator for tracked changes in Word documents. +""" + +import subprocess +import tempfile +import zipfile +from pathlib import Path + + +class RedliningValidator: + + def __init__(self, unpacked_dir, original_docx, verbose=False, author="Claude"): + self.unpacked_dir = Path(unpacked_dir) + self.original_docx = Path(original_docx) + self.verbose = verbose + self.author = author + self.namespaces = { + "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + } + + def repair(self) -> int: + return 0 + + def validate(self): + modified_file = self.unpacked_dir / "word" / "document.xml" + if not modified_file.exists(): + print(f"FAILED - Modified document.xml not found at {modified_file}") + return False + + try: + import xml.etree.ElementTree as ET + + tree = ET.parse(modified_file) + root = tree.getroot() + + del_elements = root.findall(".//w:del", self.namespaces) + ins_elements = root.findall(".//w:ins", self.namespaces) + + author_del_elements = [ + elem + for elem in del_elements + if elem.get(f"{{{self.namespaces['w']}}}author") == self.author + ] + author_ins_elements = [ + elem + for elem in ins_elements + if elem.get(f"{{{self.namespaces['w']}}}author") == self.author + ] + + if not author_del_elements and not author_ins_elements: + if self.verbose: + print(f"PASSED - No tracked changes by {self.author} found.") + return True + + except Exception: + pass + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + try: + with zipfile.ZipFile(self.original_docx, "r") as zip_ref: + zip_ref.extractall(temp_path) + except Exception as e: + print(f"FAILED - Error unpacking original docx: {e}") + return False + + original_file = temp_path / "word" / "document.xml" + if not original_file.exists(): + print( + f"FAILED - Original document.xml not found in {self.original_docx}" + ) + return False + + try: + import xml.etree.ElementTree as ET + + modified_tree = ET.parse(modified_file) + modified_root = modified_tree.getroot() + original_tree = ET.parse(original_file) + original_root = original_tree.getroot() + except ET.ParseError as e: + print(f"FAILED - Error parsing XML files: {e}") + return False + + self._remove_author_tracked_changes(original_root) + self._remove_author_tracked_changes(modified_root) + + modified_text = self._extract_text_content(modified_root) + original_text = self._extract_text_content(original_root) + + if modified_text != original_text: + error_message = self._generate_detailed_diff( + original_text, modified_text + ) + print(error_message) + return False + + if self.verbose: + print(f"PASSED - All changes by {self.author} are properly tracked") + return True + + def _generate_detailed_diff(self, original_text, modified_text): + error_parts = [ + f"FAILED - Document text doesn't match after removing {self.author}'s tracked changes", + "", + "Likely causes:", + " 1. Modified text inside another author's or tags", + " 2. Made edits without proper tracked changes", + " 3. Didn't nest inside when deleting another's insertion", + "", + "For pre-redlined documents, use correct patterns:", + " - To reject another's INSERTION: Nest inside their ", + " - To restore another's DELETION: Add new AFTER their ", + "", + ] + + git_diff = self._get_git_word_diff(original_text, modified_text) + if git_diff: + error_parts.extend(["Differences:", "============", git_diff]) + else: + error_parts.append("Unable to generate word diff (git not available)") + + return "\n".join(error_parts) + + def _get_git_word_diff(self, original_text, modified_text): + try: + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + original_file = temp_path / "original.txt" + modified_file = temp_path / "modified.txt" + + original_file.write_text(original_text, encoding="utf-8") + modified_file.write_text(modified_text, encoding="utf-8") + + result = subprocess.run( + [ + "git", + "diff", + "--word-diff=plain", + "--word-diff-regex=.", + "-U0", + "--no-index", + str(original_file), + str(modified_file), + ], + capture_output=True, + text=True, + ) + + if result.stdout.strip(): + lines = result.stdout.split("\n") + content_lines = [] + in_content = False + for line in lines: + if line.startswith("@@"): + in_content = True + continue + if in_content and line.strip(): + content_lines.append(line) + + if content_lines: + return "\n".join(content_lines) + + result = subprocess.run( + [ + "git", + "diff", + "--word-diff=plain", + "-U0", + "--no-index", + str(original_file), + str(modified_file), + ], + capture_output=True, + text=True, + ) + + if result.stdout.strip(): + lines = result.stdout.split("\n") + content_lines = [] + in_content = False + for line in lines: + if line.startswith("@@"): + in_content = True + continue + if in_content and line.strip(): + content_lines.append(line) + return "\n".join(content_lines) + + except (subprocess.CalledProcessError, FileNotFoundError, Exception): + pass + + return None + + def _remove_author_tracked_changes(self, root): + ins_tag = f"{{{self.namespaces['w']}}}ins" + del_tag = f"{{{self.namespaces['w']}}}del" + author_attr = f"{{{self.namespaces['w']}}}author" + + for parent in root.iter(): + to_remove = [] + for child in parent: + if child.tag == ins_tag and child.get(author_attr) == self.author: + to_remove.append(child) + for elem in to_remove: + parent.remove(elem) + + deltext_tag = f"{{{self.namespaces['w']}}}delText" + t_tag = f"{{{self.namespaces['w']}}}t" + + for parent in root.iter(): + to_process = [] + for child in parent: + if child.tag == del_tag and child.get(author_attr) == self.author: + to_process.append((child, list(parent).index(child))) + + for del_elem, del_index in reversed(to_process): + for elem in del_elem.iter(): + if elem.tag == deltext_tag: + elem.tag = t_tag + + for child in reversed(list(del_elem)): + parent.insert(del_index, child) + parent.remove(del_elem) + + def _extract_text_content(self, root): + p_tag = f"{{{self.namespaces['w']}}}p" + t_tag = f"{{{self.namespaces['w']}}}t" + + paragraphs = [] + for p_elem in root.findall(f".//{p_tag}"): + text_parts = [] + for t_elem in p_elem.findall(f".//{t_tag}"): + if t_elem.text: + text_parts.append(t_elem.text) + paragraph_text = "".join(text_parts) + if paragraph_text: + paragraphs.append(paragraph_text) + + return "\n".join(paragraphs) + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/ppt_to_pic.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/ppt_to_pic.py new file mode 100644 index 00000000..2afefb4d --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/ppt_to_pic.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +""" +PPT to Image Converter for PPTX Plus Skill (Linux) +Converts PPTX slides to images for visual inspection. + +Usage: + python ppt_to_pic.py --ppt-dir ./ppt --output-dir ./images + python ppt_to_pic.py --file presentation.pptx --output slide.png + +Requirements: + - LibreOffice with soffice available + - Poppler (pdftoppm) for PDF to image conversion +""" + +import argparse +import os +import sys +import time +from pathlib import Path + + +def ppt_to_pic_libreoffice(ppt_path: str, output_dir: str) -> bool: + """Convert PPTX to images using LibreOffice (cross-platform)""" + try: + import subprocess + + abs_ppt_path = os.path.abspath(ppt_path) + abs_output_dir = os.path.abspath(output_dir) + os.makedirs(abs_output_dir, exist_ok=True) + + # First convert to PDF + result = subprocess.run([ + "soffice", "--headless", "--convert-to", "pdf", + "--outdir", abs_output_dir, abs_ppt_path + ], capture_output=True, text=True, timeout=60) + + if result.returncode != 0: + print(f"LibreOffice PDF conversion failed: {result.stderr}") + return False + + # Get PDF path + pdf_name = Path(ppt_path).stem + ".pdf" + pdf_path = os.path.join(abs_output_dir, pdf_name) + + if not os.path.exists(pdf_path): + print(f"PDF not found: {pdf_path}") + return False + + # Convert PDF to images using pdftoppm + result = subprocess.run([ + "pdftoppm", "-jpeg", "-r", "150", pdf_path, + os.path.join(abs_output_dir, "slide") + ], capture_output=True, text=True, timeout=60) + + if result.returncode != 0: + print(f"pdftoppm failed: {result.stderr}") + return False + + # Clean up PDF + try: + os.remove(pdf_path) + except: + pass + + return True + + except FileNotFoundError as e: + print(f"Required tool not found: {e}") + print("Please install LibreOffice and Poppler (pdftoppm)") + return False + except Exception as e: + print(f"LibreOffice conversion failed: {e}") + return False + + +def convert_pptx_to_images(ppt_path: str, output_dir: str) -> str: + """Convert a single PPTX file to images""" + abs_ppt_path = os.path.abspath(ppt_path) + abs_output_dir = os.path.abspath(output_dir) + + if not os.path.exists(abs_ppt_path): + return f"Error: PPTX file not found: {abs_ppt_path}" + + os.makedirs(abs_output_dir, exist_ok=True) + + base_name = Path(ppt_path).stem + + # Use LibreOffice for conversion (Linux) + if ppt_to_pic_libreoffice(abs_ppt_path, abs_output_dir): + return f"Successfully converted {ppt_path} to images in {abs_output_dir}" + + return f"Failed to convert {ppt_path}. Please ensure LibreOffice and Poppler (pdftoppm) are installed." + + +def convert_directory(ppt_dir: str, output_dir: str) -> str: + """Convert all PPTX files in a directory to images""" + abs_ppt_dir = os.path.abspath(ppt_dir.lstrip("/").lstrip("\\")) + abs_output_dir = os.path.abspath(output_dir.lstrip("/").lstrip("\\")) + + if not os.path.exists(abs_ppt_dir): + return f"Error: Directory not found: {abs_ppt_dir}" + + os.makedirs(abs_output_dir, exist_ok=True) + + ppt_files = [f for f in os.listdir(abs_ppt_dir) + if f.endswith('.pptx') and not f.startswith('~$')] + + if not ppt_files: + return f"No PPTX files found in {abs_ppt_dir}" + + print(f"Converting {len(ppt_files)} PPTX files...") + + success_count = 0 + for filename in ppt_files: + ppt_path = os.path.join(abs_ppt_dir, filename) + print(f" Processing: {filename}...", end="", flush=True) + + base_name = Path(filename).stem + file_output_dir = os.path.join(abs_output_dir, base_name) + + if ppt_to_pic_libreoffice(ppt_path, file_output_dir): + print(" DONE") + success_count += 1 + else: + print(" FAILED") + + return f"Successfully converted {success_count}/{len(ppt_files)} files to {abs_output_dir}" + + +def main(): + parser = argparse.ArgumentParser(description="Convert PPTX slides to images") + parser.add_argument("--ppt-dir", "-d", help="Directory containing PPTX files") + parser.add_argument("--file", "-f", help="Single PPTX file to convert") + parser.add_argument("--output", "-o", help="Output directory or file path") + + args = parser.parse_args() + + if args.file: + output = args.output or os.path.splitext(args.file)[0] + result = convert_pptx_to_images(args.file, output) + print(result) + + elif args.ppt_dir: + output = args.output or os.path.join(args.ppt_dir, "images") + result = convert_directory(args.ppt_dir, output) + print(result) + + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/thumbnail.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/thumbnail.py new file mode 100644 index 00000000..edcbdc0f --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/thumbnail.py @@ -0,0 +1,289 @@ +"""Create thumbnail grids from PowerPoint presentation slides. + +Creates a grid layout of slide thumbnails for quick visual analysis. +Labels each thumbnail with its XML filename (e.g., slide1.xml). +Hidden slides are shown with a placeholder pattern. + +Usage: + python thumbnail.py input.pptx [output_prefix] [--cols N] + +Examples: + python thumbnail.py presentation.pptx + # Creates: thumbnails.jpg + + python thumbnail.py template.pptx grid --cols 4 + # Creates: grid.jpg (or grid-1.jpg, grid-2.jpg for large decks) +""" + +import argparse +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path + +import defusedxml.minidom +from office.soffice import get_soffice_env +from PIL import Image, ImageDraw, ImageFont + +THUMBNAIL_WIDTH = 300 +CONVERSION_DPI = 100 +MAX_COLS = 6 +DEFAULT_COLS = 3 +JPEG_QUALITY = 95 +GRID_PADDING = 20 +BORDER_WIDTH = 2 +FONT_SIZE_RATIO = 0.10 +LABEL_PADDING_RATIO = 0.4 + + +def main(): + parser = argparse.ArgumentParser( + description="Create thumbnail grids from PowerPoint slides." + ) + parser.add_argument("input", help="Input PowerPoint file (.pptx)") + parser.add_argument( + "output_prefix", + nargs="?", + default="thumbnails", + help="Output prefix for image files (default: thumbnails)", + ) + parser.add_argument( + "--cols", + type=int, + default=DEFAULT_COLS, + help=f"Number of columns (default: {DEFAULT_COLS}, max: {MAX_COLS})", + ) + + args = parser.parse_args() + + cols = min(args.cols, MAX_COLS) + if args.cols > MAX_COLS: + print(f"Warning: Columns limited to {MAX_COLS}") + + input_path = Path(args.input) + if not input_path.exists() or input_path.suffix.lower() != ".pptx": + print(f"Error: Invalid PowerPoint file: {args.input}", file=sys.stderr) + sys.exit(1) + + output_path = Path(f"{args.output_prefix}.jpg") + + try: + slide_info = get_slide_info(input_path) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + visible_images = convert_to_images(input_path, temp_path) + + if not visible_images and not any(s["hidden"] for s in slide_info): + print("Error: No slides found", file=sys.stderr) + sys.exit(1) + + slides = build_slide_list(slide_info, visible_images, temp_path) + + grid_files = create_grids(slides, cols, THUMBNAIL_WIDTH, output_path) + + print(f"Created {len(grid_files)} grid(s):") + for grid_file in grid_files: + print(f" {grid_file}") + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def get_slide_info(pptx_path: Path) -> list[dict]: + with zipfile.ZipFile(pptx_path, "r") as zf: + rels_content = zf.read("ppt/_rels/presentation.xml.rels").decode("utf-8") + rels_dom = defusedxml.minidom.parseString(rels_content) + + rid_to_slide = {} + for rel in rels_dom.getElementsByTagName("Relationship"): + rid = rel.getAttribute("Id") + target = rel.getAttribute("Target") + rel_type = rel.getAttribute("Type") + if "slide" in rel_type and target.startswith("slides/"): + rid_to_slide[rid] = target.replace("slides/", "") + + pres_content = zf.read("ppt/presentation.xml").decode("utf-8") + pres_dom = defusedxml.minidom.parseString(pres_content) + + slides = [] + for sld_id in pres_dom.getElementsByTagName("p:sldId"): + rid = sld_id.getAttribute("r:id") + if rid in rid_to_slide: + hidden = sld_id.getAttribute("show") == "0" + slides.append({"name": rid_to_slide[rid], "hidden": hidden}) + + return slides + + +def build_slide_list( + slide_info: list[dict], + visible_images: list[Path], + temp_dir: Path, +) -> list[tuple[Path, str]]: + if visible_images: + with Image.open(visible_images[0]) as img: + placeholder_size = img.size + else: + placeholder_size = (1920, 1080) + + slides = [] + visible_idx = 0 + + for info in slide_info: + if info["hidden"]: + placeholder_path = temp_dir / f"hidden-{info['name']}.jpg" + placeholder_img = create_hidden_placeholder(placeholder_size) + placeholder_img.save(placeholder_path, "JPEG") + slides.append((placeholder_path, f"{info['name']} (hidden)")) + else: + if visible_idx < len(visible_images): + slides.append((visible_images[visible_idx], info["name"])) + visible_idx += 1 + + return slides + + +def create_hidden_placeholder(size: tuple[int, int]) -> Image.Image: + img = Image.new("RGB", size, color="#F0F0F0") + draw = ImageDraw.Draw(img) + line_width = max(5, min(size) // 100) + draw.line([(0, 0), size], fill="#CCCCCC", width=line_width) + draw.line([(size[0], 0), (0, size[1])], fill="#CCCCCC", width=line_width) + return img + + +def convert_to_images(pptx_path: Path, temp_dir: Path) -> list[Path]: + pdf_path = temp_dir / f"{pptx_path.stem}.pdf" + + result = subprocess.run( + [ + "soffice", + "--headless", + "--convert-to", + "pdf", + "--outdir", + str(temp_dir), + str(pptx_path), + ], + capture_output=True, + text=True, + env=get_soffice_env(), + ) + if result.returncode != 0 or not pdf_path.exists(): + raise RuntimeError("PDF conversion failed") + + result = subprocess.run( + [ + "pdftoppm", + "-jpeg", + "-r", + str(CONVERSION_DPI), + str(pdf_path), + str(temp_dir / "slide"), + ], + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise RuntimeError("Image conversion failed") + + return sorted(temp_dir.glob("slide-*.jpg")) + + +def create_grids( + slides: list[tuple[Path, str]], + cols: int, + width: int, + output_path: Path, +) -> list[str]: + max_per_grid = cols * (cols + 1) + grid_files = [] + + for chunk_idx, start_idx in enumerate(range(0, len(slides), max_per_grid)): + end_idx = min(start_idx + max_per_grid, len(slides)) + chunk_slides = slides[start_idx:end_idx] + + grid = create_grid(chunk_slides, cols, width) + + if len(slides) <= max_per_grid: + grid_filename = output_path + else: + stem = output_path.stem + suffix = output_path.suffix + grid_filename = output_path.parent / f"{stem}-{chunk_idx + 1}{suffix}" + + grid_filename.parent.mkdir(parents=True, exist_ok=True) + grid.save(str(grid_filename), quality=JPEG_QUALITY) + grid_files.append(str(grid_filename)) + + return grid_files + + +def create_grid( + slides: list[tuple[Path, str]], + cols: int, + width: int, +) -> Image.Image: + font_size = int(width * FONT_SIZE_RATIO) + label_padding = int(font_size * LABEL_PADDING_RATIO) + + with Image.open(slides[0][0]) as img: + aspect = img.height / img.width + height = int(width * aspect) + + rows = (len(slides) + cols - 1) // cols + grid_w = cols * width + (cols + 1) * GRID_PADDING + grid_h = rows * (height + font_size + label_padding * 2) + (rows + 1) * GRID_PADDING + + grid = Image.new("RGB", (grid_w, grid_h), "white") + draw = ImageDraw.Draw(grid) + + try: + font = ImageFont.load_default(size=font_size) + except Exception: + font = ImageFont.load_default() + + for i, (img_path, slide_name) in enumerate(slides): + row, col = i // cols, i % cols + x = col * width + (col + 1) * GRID_PADDING + y_base = ( + row * (height + font_size + label_padding * 2) + (row + 1) * GRID_PADDING + ) + + label = slide_name + bbox = draw.textbbox((0, 0), label, font=font) + text_w = bbox[2] - bbox[0] + draw.text( + (x + (width - text_w) // 2, y_base + label_padding), + label, + fill="black", + font=font, + ) + + y_thumbnail = y_base + label_padding + font_size + label_padding + + with Image.open(img_path) as img: + img.thumbnail((width, height), Image.Resampling.LANCZOS) + w, h = img.size + tx = x + (width - w) // 2 + ty = y_thumbnail + (height - h) // 2 + grid.paste(img, (tx, ty)) + + if BORDER_WIDTH > 0: + draw.rectangle( + [ + (tx - BORDER_WIDTH, ty - BORDER_WIDTH), + (tx + w + BORDER_WIDTH - 1, ty + h + BORDER_WIDTH - 1), + ], + outline="gray", + width=BORDER_WIDTH, + ) + + return grid + + +if __name__ == "__main__": + main() diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/vision_qwen.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/vision_qwen.py new file mode 100644 index 00000000..72eef411 --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/vision_qwen.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +""" +Qwen Vision Tool for PPTX Pro Skill +Provides image description capabilities using Qwen Vision model. + +Usage: + python vision_qwen.py --image path/to/image.png --prompt "Describe this slide" + python vision_qwen.py --images img1.png img2.png img3.png +""" + +import argparse +import requests +import base64 +import os +import sys +import json +import re +import time +from typing import Optional, List + +# Try to import from utils/config.py in parent project, fallback to defaults +try: + # Add parent paths to find config + config_path = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'metierial', 'utils', 'config.py') + if os.path.exists(config_path): + sys.path.insert(0, os.path.dirname(config_path)) + from config import QWEN_KEY, LLM_API_URL + else: + raise ImportError("Config not found") +except ImportError: + # Default API configuration + QWEN_KEY = "sk-1b897ac944044d6aae35ca862a23fbdb" + LLM_API_URL = "https://maas-api.ai-yuanjing.com/openapi/compatible-mode-nosensitive/v1/chat/completions" + + +class QwenVisionTool: + """Qwen Vision API wrapper for image description""" + + def __init__(self): + self.api_key = QWEN_KEY + self.url = LLM_API_URL + + def _prepare_image_data(self, image_path: str) -> Optional[str]: + """Convert image to base64, SVG is converted to PNG first""" + target_path = image_path + temp_png = None + try: + if image_path.lower().endswith(".svg"): + try: + from svglib.svglib import svg2rlg + from reportlab.graphics import renderPM + import tempfile + drawing = svg2rlg(image_path) + fd, temp_png = tempfile.mkstemp(suffix=".png") + os.close(fd) + renderPM.drawToFile(drawing, temp_png, fmt="PNG") + target_path = temp_png + except ImportError: + print("Warning: svglib not installed, cannot convert SVG") + return None + + with open(target_path, "rb") as f: + return base64.b64encode(f.read()).decode() + except Exception as e: + print(f"Error processing image {image_path}: {e}") + return None + finally: + if temp_png and os.path.exists(temp_png): + try: + os.remove(temp_png) + except: + pass + + def describe_images_batch(self, image_paths: List[str], prompt: str = "请详细描述这些图片的内容。") -> List[str]: + """ + Batch describe images, up to 5 images per request. + """ + results = [] + # Group by max 5 images + for i in range(0, len(image_paths), 5): + batch = image_paths[i:i+5] + batch_results = self._call_qwen_vision_batch(batch, prompt) + results.extend(batch_results) + # 1 second interval between batches + if i + 5 < len(image_paths): + time.sleep(1) + return results + + def _call_qwen_vision_batch(self, image_paths: List[str], prompt: str) -> List[str]: + """Send a single batch (max 5 images) to Qwen Vision API""" + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" + } + + json_format_instruction = ( + "\n请严格按照以下 JSON 数组格式返回每张图片的描述内容,严禁包含任何额外的 Markdown 标记、解释性文字或示例内容:\n" + "[\"描述1\", \"描述2\", ...]\n" + f"注意:你必须提供正好 {len(image_paths)} 个字符串,每个字符串对应一张图片的详细描述。" + ) + + content = [{"type": "text", "text": f"{prompt} {json_format_instruction}"}] + + for path in image_paths: + img_b64 = self._prepare_image_data(path) + if img_b64: + content.append({ + "type": "image_url", + "image_url": {"url": f"data:image/png;base64,{img_b64}"}, + }) + else: + content.append({"type": "text", "text": f"[无法读取图片: {os.path.basename(path)}]"}) + + payload = { + "model": "qwen3.5-397b-a17b", + "messages": [{"role": "user", "content": content}], + "stream": False, + "response_format": {"type": "json_object"} + } + + try: + resp = requests.post(self.url, headers=headers, json=payload, timeout=120) + if resp.status_code == 200: + full_text = resp.json()["choices"][0]["message"]["content"] + + # Try to parse JSON + descriptions = [] + try: + start_idx = full_text.find("[") + end_idx = full_text.rfind("]") + if start_idx != -1 and end_idx != -1: + json_str = full_text[start_idx:end_idx+1] + data = json.loads(json_str) + if isinstance(data, list): + descriptions = [str(d) for d in data] + except: + pass + + # Fallback: regex matching + if len(descriptions) < len(image_paths): + matches = re.findall(r'"([^"]+)"', full_text) + if len(matches) >= len(image_paths): + descriptions = matches + + # Filter invalid descriptions + invalid_keywords = ["描述", "示例", "Image [", "图片"] + final_descriptions = [] + for d in descriptions: + if len(d) < 20 and any(k in d for k in invalid_keywords): + continue + final_descriptions.append(d) + + if len(final_descriptions) >= len(image_paths): + return final_descriptions[:len(image_paths)] + + # Last resort: split by markers + fallback_descs = [] + parts = re.split(r'Image \[\d+\]:|第\d+张图片:|^\d+\.', full_text) + for p in parts: + clean_p = p.strip() + if len(clean_p) > 10: + fallback_descs.append(clean_p) + + if len(fallback_descs) >= len(image_paths): + return fallback_descs[:len(image_paths)] + + print(f"Warning: Could not extract valid descriptions. Output length: {len(full_text)}") + return [full_text] * len(image_paths) + else: + return [f"API Error: {resp.status_code}"] * len(image_paths) + except Exception as e: + return [f"Request Exception: {e}"] * len(image_paths) + + def describe_image(self, image_path: str, prompt: str = "请详细描述这张图片的内容。") -> str: + """Single image description (backward compatible)""" + return self.describe_images_batch([image_path], prompt)[0] + + +def main(): + parser = argparse.ArgumentParser(description="Qwen Vision Tool for image description") + parser.add_argument("--image", "-i", help="Single image path to describe") + parser.add_argument("--images", "-I", nargs="+", help="Multiple image paths for batch description") + parser.add_argument("--prompt", "-p", default="请详细描述这张/这些图片的内容。", help="Prompt for description") + parser.add_argument("--output", "-o", help="Output file for results (JSON format for batch)") + + args = parser.parse_args() + + tool = QwenVisionTool() + + if args.image: + result = tool.describe_image(args.image, args.prompt) + print(f"\n描述结果:\n{result}") + + elif args.images: + results = tool.describe_images_batch(args.images, args.prompt) + print(f"\n批量描述结果:") + for i, (path, desc) in enumerate(zip(args.images, results)): + print(f"\n[{i+1}] {os.path.basename(path)}:") + print(f" {desc[:200]}..." if len(desc) > 200 else f" {desc}") + + if args.output: + with open(args.output, "w", encoding="utf-8") as f: + json.dump(dict(zip(args.images, results)), f, ensure_ascii=False, indent=2) + print(f"\n结果已保存到: {args.output}") + + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/web_search.py b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/web_search.py new file mode 100644 index 00000000..0e3bf06a --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/scripts/web_search.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +""" +Web Search Tool for PPTX Pro Skill +Provides web search and image search capabilities using Tencent Search API. + +Usage: + python web_search.py --query "AI trends 2024" --type text --count 10 + python web_search.py --query "technology background" --type image --count 10 + +Limits: + - Text search: max 3 queries per session + - Image search: max 3 queries per session +""" + +import argparse +import json +import sys +import os +from typing import Dict, Any, Optional + +# Try to import from utils/config.py, fallback to defaults +try: + config_path = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'metierial', 'utils', 'config.py') + if os.path.exists(config_path): + sys.path.insert(0, os.path.dirname(config_path)) + from config import TENCENT_ID, TENCENT_KEY, TENCENT_ENDPOINT, TENCENT_IMG_ENDPOINT + else: + raise ImportError("Config not found") +except ImportError: + # Default API configuration + TENCENT_ID = "TENCENT_SECRET_ID_PLACEHOLDER" + TENCENT_KEY = "TENCENT_SECRET_KEY_PLACEHOLDER" + TENCENT_ENDPOINT = "wsa.tencentcloudapi.com" + TENCENT_IMG_ENDPOINT = "wimgs.tencentcloudapi.com" + + +class TencentSearchTool: + """Tencent Search API wrapper""" + + def __init__(self, endpoint: str = TENCENT_ENDPOINT, service: str = "wsa", version: str = "2025-05-08"): + try: + from tencentcloud.common import credential + from tencentcloud.common.profile.client_profile import ClientProfile + from tencentcloud.common.profile.http_profile import HttpProfile + from tencentcloud.common.common_client import CommonClient + + self.cred = credential.Credential(TENCENT_ID, TENCENT_KEY) + httpProfile = HttpProfile() + httpProfile.endpoint = endpoint + clientProfile = ClientProfile() + clientProfile.httpProfile = httpProfile + self.client = CommonClient(service, version, self.cred, "", profile=clientProfile) + self.available = True + except ImportError: + print("Warning: tencentcloud-sdk-python not installed. Search functionality disabled.") + print("Install with: pip install tencentcloud-sdk-python") + self.available = False + + def search(self, query: str, action: str = "SearchPro", params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + if not self.available: + return {"error": "Tencent SDK not available"} + + if params is None: + params = {"Query": query} + + try: + resp = self.client.call_json(action, params) + return resp + except Exception as e: + return {"error": str(e)} + + +# Session limits and cache +_search_cache = set() +_search_count = 0 +_image_search_count = 0 + + +def web_search(query: str, count: int = 10) -> str: + """ + Search the web using Tencent Search API. + Returns passage results. + + Limits: 3 searches per session. + """ + global _search_cache, _search_count + + if _search_count >= 3: + return "Warning: Reached search limit (3). Please use existing information." + + if query in _search_cache: + return "Warning: This query was already executed. Avoid duplicate searches." + + _search_cache.add(query) + _search_count += 1 + + try: + print(f" Searching for: '{query}'...", end="", flush=True) + searcher = TencentSearchTool(endpoint=TENCENT_ENDPOINT, service="wsa", version="2025-05-08") + + params = {"Query": query, "Mode": 0} + if count in [10, 20, 30, 40, 50]: + params["Cnt"] = count + + result = searcher.search(query, action="SearchPro", params=params) + + if "error" in result: + print(f" FAILED: {result['error']}") + return f"Search failed: {result['error']}" + + pages = result.get("Response", {}).get("Pages", []) + formatted_results = [] + for page_str in pages: + try: + page = json.loads(page_str) + title = page.get("title", "No title") + url = page.get("url", "No URL") + passage = page.get("passage", "") + formatted_results.append(f"Title: {title}\nURL: {url}\nSummary: {passage}\n---") + except: + continue + + res_str = "\n".join(formatted_results) if formatted_results else "No results found." + print(f" Found {len(formatted_results)} results.") + return res_str + except Exception as e: + print(f" Exception: {e}") + return f"Search exception: {str(e)}" + + +def image_search(query: str, count: int = 10) -> str: + """ + Search for images using Tencent Image Search API. + Returns image URLs and descriptions. + + Limits: 3 image searches per session. + """ + global _image_search_count + + if _image_search_count >= 3: + return "Warning: Reached image search limit (3). Use found images." + + _image_search_count += 1 + + try: + print(f" Searching images for: '{query}'...", end="", flush=True) + searcher = TencentSearchTool(endpoint=TENCENT_ENDPOINT, service="wsa", version="2025-05-08") + + # Mode=2 includes image results + params = {"Query": query, "Mode": 2, "Cnt": 10} + result = searcher.search(query, action="SearchPro", params=params) + + # Fallback to wimgs endpoint if needed + if "error" in result: + searcher_fallback = TencentSearchTool(endpoint=TENCENT_IMG_ENDPOINT, service="wimgs", version="2022-08-18") + result = searcher_fallback.search(query, action="SearchImage", params={"Query": query}) + + if "error" in result and "InvalidAction" in result["error"]: + result = searcher_fallback.search(query, action="SearchPro", params={"Query": query}) + + if "error" in result: + print(f" FAILED: {result['error']}") + return f"Image search failed: {result['error']}" + + pages = result.get("Response", {}).get("Pages", []) + image_results = [] + for page_str in pages: + try: + page = json.loads(page_str) + imgs = page.get("images", []) + title = page.get("title", "Related image") + for img_url in imgs: + image_results.append(f"Description: {title}\nURL: {img_url}") + except: + continue + + res_str = "\n".join(image_results[:count]) if image_results else "No images found." + print(f" Found {len(image_results)} images.") + return res_str + except Exception as e: + print(f" Exception: {e}") + return f"Image search exception: {str(e)}" + + +def main(): + parser = argparse.ArgumentParser(description="Web Search Tool for PPTX Pro") + parser.add_argument("--query", "-q", required=True, help="Search query") + parser.add_argument("--type", "-t", choices=["text", "image"], default="text", help="Search type") + parser.add_argument("--count", "-c", type=int, default=10, help="Number of results") + parser.add_argument("--output", "-o", help="Output file for results") + + args = parser.parse_args() + + if args.type == "text": + result = web_search(args.query, args.count) + else: + result = image_search(args.query, args.count) + + print(f"\n{result}") + + if args.output: + with open(args.output, "w", encoding="utf-8") as f: + f.write(result) + print(f"\nResults saved to: {args.output}") + + +if __name__ == "__main__": + main() diff --git a/libs/hexagent_demo/backend/skills/pptx-plus-linux/skill.md b/libs/hexagent_demo/backend/skills/pptx-plus-linux/skill.md new file mode 100644 index 00000000..718d6eed --- /dev/null +++ b/libs/hexagent_demo/backend/skills/pptx-plus-linux/skill.md @@ -0,0 +1,427 @@ +--- +name: pptx-plus-linux +description: "处理 .pptx 文件(创建、读取、编辑、合并、拆分)。支持幻灯片生成、图表添加和模板管理。在处理演示文稿、deck 或 slides 时触发。" +license: 专有软件。完整条款请参阅 LICENSE.txt +--- + +# PPTX Plus Skill (Linux) + +## 快速参考 + +| 任务 | 指南 | +| ------------------ | ---------------------------------------- | +| 读取/分析内容 | `python -m markitdown presentation.pptx` | +| 编辑或基于模板创建 | 阅读 [editing.md](editing.md) | +| 从零创建 | 阅读 [pptxgenjs.md](pptxgenjs.md) | +| 可视化检查与 QA | 阅读 [examin.md](examin.md) | +| **添加图表** | 见下方「图表生成」章节 | +| 搜索网络素材 | `python scripts/web_search.py` | + +--- + +## ⚠️ 重要:分批写入策略 + +**使用 PptxGenJS 创建 PPTX 时,务必采用分批写入以避免 token 溢出错误。** + +### 为什么要分批写入? + +生成复杂、视觉效果丰富的演示文稿时,代码可能变得非常长。在单次响应中写入所有幻灯片可能导致 token 溢出错误。**分批写入同一个文件可解决此问题。** + +### 如何分批写入 + +1. **严格限制:每批最多 5 张幻灯片** — 5 张是最佳选择。 +2. **一个文件,多次编辑**:所有代码必须写入**同一个 JavaScript 文件**。不要为不同批次创建多个文件。 +3. **增量追加策略**:使用 Edit 工具将新幻灯片代码**追加**到现有文件中。每批应继续使用第一批中定义的同一个 `pres` 对象。 +4. **构建支持增量添加的代码结构:** + +```javascript +// 批次 1:初始化设置 + 幻灯片 1-5 +const pptxgen = require('pptxgenjs') + +let pres = new pptxgen() +pres.layout = 'LAYOUT_16x9' + +// 幻灯片 1-5 代码在此... +// 可选的增量保存检查点 + +// --- 批次 1 结束 --- + +// 批次 2:幻灯片 6-10(继续向同一个 pres 对象添加) +// 更多幻灯片代码... + +// 批次 N:最终保存 +pres.writeFile({ fileName: 'output.pptx' }) +``` + +### 分批写入工作流 + +``` +步骤 1:写入初始设置 + 幻灯片 1-5 → 保存/继续 +步骤 2:写入幻灯片 6-10 → 继续使用同一个 pres 对象 +步骤 3:写入幻灯片 11-15 → 继续 +... +最终:写入幻灯片 N-M + pres.writeFile() +``` + +**请记住:** 你是增量地向同一个 JavaScript 文件添加内容。每批向文件添加更多代码,而不是创建单独的文件。 + +--- + +## 读取内容 + +```bash +# 文本提取 +python -m markitdown presentation.pptx + +# 可视化概览 +python scripts/thumbnail.py presentation.pptx + +# 原始 XML +python scripts/office/unpack.py presentation.pptx unpacked/ +``` + +--- + +## 编辑工作流 + +**完整详情请阅读** **[editing.md](editing.md)。** + +1. 使用 `thumbnail.py` 分析模板 +2. 解包 → 操作幻灯片 → 编辑内容 → 清理 → 打包 + +--- + +## 图表生成 + +**为演示文稿添加精美图表,让数据可视化更具冲击力。** + +### 图表类型选择指南 + +根据数据特征选择最合适的图表类型: + +| 数据类型 | 推荐图表 | 用途 | +| ------------ | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | +| **时间序列** | `line_chart`, `area_chart` | 趋势、累积变化 | +| **对比** | `bar_chart`, `column_chart` | 类别对比、Top-N 排行 | +| **占比** | `pie_chart`, `treemap_chart` | 整体与部分、层级占比 | +| **相关性** | `scatter_chart`, `dual_axes_chart` | 变量关系、双轴对比 | +| **流程** | `funnel_chart`, `flow_diagram` | 转化漏斗、流程步骤 | +| **分布** | `histogram_chart`, `boxplot_chart`, `violin_chart` | 频率分布、统计分布 | +| **层级** | `organization_chart`, `mind_map` | 组织结构、思维导图 | +| **地理** | `district_map`, `pin_map`, `path_map` | 区域数据、点位、路线 | +| **专项** | `radar_chart`, `liquid_chart`, `word_cloud_chart`, `network_graph`, `sankey_chart`, `venn_chart`, `fishbone_diagram` | 多维对比、进度、词频、网络、流向、交集、因果 | + +### 图表生成方法 + +#### 方法一:图片图表(推荐用于复杂图表) + +生成高质量图表图片,然后插入幻灯片。适合需要精美视觉效果或复杂图表类型。 + +```bash +# 生成图表图片 +node scripts/generate.js '{"tool":"generate_pie_chart","args":{"data":[{"category":"A","value":35},{"category":"B","value":45},{"category":"C","value":20}],"title":"市场份额","theme":"dark"}}' +``` + +返回图表图片 URL,然后在 JavaScript 中使用: + +```javascript +// 在 PptxGenJS 中插入图表图片 +slide.addImage({ + path: '返回的图表URL', + x: 0.5, + y: 1.5, + w: 4.5, + h: 3.5 +}) +``` + +**图表参数规格详见** **[references/](references/)** **目录下的各图表文档。** + +#### 方法二:原生图表(适合简单图表) + +使用 PptxGenJS 内置图表功能,适合快速创建简单柱状图、折线图、饼图。 + +```javascript +// 柱状图 +slide.addChart(pres.charts.BAR, [{ + name: '销售额', + labels: ['Q1', 'Q2', 'Q3', 'Q4'], + values: [4500, 5500, 6200, 7100] +}], { + x: 0.5, + y: 0.6, + w: 6, + h: 3, + barDir: 'col', + showTitle: true, + title: '季度销售', + chartColors: ['0D9488', '14B8A6', '5EEAD4'], + showValue: true, + dataLabelPosition: 'outEnd' +}) + +// 饼图 +slide.addChart(pres.charts.PIE, [{ + name: '份额', + labels: ['A', 'B', '其他'], + values: [35, 45, 20] +}], { x: 7, y: 1, w: 5, h: 4, showPercent: true }) +``` + +### 方法选择建议 + +| 场景 | 推荐方法 | 原因 | +| -------------------------------- | -------- | ---------------- | +| 简单柱状/折线/饼图 | 原生图表 | 快速、代码简洁 | +| 需要与PPT主题配色统一 | 原生图表 | 可自定义颜色 | +| 复杂图表类型(雷达图、桑基图等) | 图片图表 | 原生不支持 | +| 需要精美视觉效果 | 图片图表 | 更丰富的视觉样式 | +| 需要动态交互 | 原生图表 | 可在PPT中编辑 | +| 暗色主题/特殊样式 | 图片图表 | 支持多种主题 | + +### 图表主题与样式 + +图片图表支持三种主题: + +- `default` - 标准白色背景 +- `dark` - 深色背景,适合深色PPT +- `academy` - 学术风格 + +自定义配色: + +```json +{ + "tool": "generate_column_chart", + "args": { + "data": [...], + "title": "销售数据", + "theme": "dark", + "style": { + "palette": ["#1E2761", "#CADCFC", "#FFFFFF"], + "backgroundColor": "#1a1a2e" + } + } +} +``` + +### 详细图表规格 + +每种图表的完整参数说明,请参阅对应的参考文档: + +- `references/generate_line_chart.md` - 折线图 +- `references/generate_bar_chart.md` - 条形图 +- `references/generate_column_chart.md` - 柱状图 +- `references/generate_pie_chart.md` - 饼图/环图 +- `references/generate_area_chart.md` - 面积图 +- `references/generate_scatter_chart.md` - 散点图 +- `references/generate_radar_chart.md` - 雷达图 +- `references/generate_funnel_chart.md` - 漏斗图 +- `references/generate_treemap_chart.md` - 树图 +- `references/generate_sankey_chart.md` - 桑基图 +- `references/generate_dual_axes_chart.md` - 双轴图 +- 以及其他 15+ 种图表类型 + +--- + +## 网络搜索(腾讯搜索) + +搜索网络内容和图片以丰富你的演示文稿。 + +```bash +# 文本搜索(返回文本段落) +python scripts/web_search.py --query "AI 趋势 2026" --count 10 + +# 图片搜索(返回图片 URL) +python scripts/web_search.py --query "科技背景" --type image --count 10 +``` + +**限制:** + +- 文本搜索:每次会话最多 10 次查询 +- 图片搜索:每次会话最多 10 次查询 + +**使用场景:** + +- 收集事实和数据 +- 寻找设计参考图片 +- 研究主题背景 + +## 从零创建 + +**完整详情请阅读** **[pptxgenjs.md](pptxgenjs.md)。** + +当没有模板或参考演示文稿可用时使用。 + +--- + +## 设计思路 + +**不要创建无聊的幻灯片。** 白底黑字的简单列表无法打动任何人。为每张幻灯片考虑以下设计思路。 + +### 开始之前 + +- **选择大胆、契合内容的配色方案**:配色应为此主题量身设计。如果将你的配色方案换到一个完全不同的演示文稿中仍然"适用",说明你的选择还不够具体。 +- **主次分明而非均等分配**:一种颜色应占主导地位(60-70% 视觉权重),配以 1-2 种辅助色调和一种锐利的强调色。永远不要给所有颜色相等的权重。 +- **深浅对比**:标题 + 结尾幻灯片使用深色背景,内容幻灯片使用浅色背景("三明治"结构)。或者全程使用深色背景以营造高端感。 +- **坚持一个视觉母题**:选择一个独特的元素并重复使用 —— 圆角图片框、彩色圆形图标、单侧粗边框。在每张幻灯片中贯彻使用。 + +### 配色方案 + +选择与主题匹配的颜色 —— 不要默认使用通用蓝色。以下配色方案供参考: + +| 主题 | 主色 | 辅色 | 强调色 | +| -------------- | ------------------ | ------------------ | ------------------ | +| **午夜高管** | `1E2761`(藏蓝) | `CADCFC`(冰蓝) | `FFFFFF`(白色) | +| **森林苔藓** | `2C5F2D`(森林绿) | `97BC62`(苔藓绿) | `F5F5F5`(奶油色) | +| **珊瑚活力** | `F96167`(珊瑚红) | `F9E795`(金色) | `2F3C7E`(藏蓝) | +| **暖赤陶** | `B85042`(赤陶色) | `E7E8D1`(沙色) | `A7BEAE`(鼠尾草) | +| **海洋渐变** | `065A82`(深海蓝) | `1C7293`(青色) | `21295C`(午夜蓝) | +| **炭灰极简** | `36454F`(炭灰) | `F2F2F2`(灰白) | `212121`(黑色) | +| **青绿信赖** | `028090`(青色) | `00A896`(海泡色) | `02C39A`(薄荷绿) | +| **浆果奶油** | `6D2E46`(浆果色) | `A26769`(玫瑰灰) | `ECE2D0`(奶油色) | +| **鼠尾草宁静** | `84B59F`(鼠尾草) | `69A297`(桉树绿) | `50808E`(板岩灰) | +| **樱桃大胆** | `990011`(樱桃红) | `FCF6F5`(灰白) | `2F3C7E`(藏蓝) | + +### 每张幻灯片 + +**每张幻灯片都需要一个视觉元素** —— 图片、图表、图标或形状。纯文字的幻灯片容易被遗忘。 + +**布局选项:** + +- 双栏(左侧文字,右侧插图) +- 图标 + 文字行(彩色圆圈中的图标,粗体标题,下方描述) +- 2x2 或 2x3 网格(一侧放图片,另一侧放内容块网格) +- 半出血图片(完整的左侧或右侧)配内容覆盖 + +**数据展示:** + +- 大号数据突出(60-72pt 大数字,下方小标签) +- 对比栏(前后对比、优缺点、并排选项) +- 时间线或流程图(编号步骤,箭头) +- **精美图表**(使用图表生成功能,数据可视化更具冲击力) + +**视觉打磨:** + +- 章节标题旁的小彩色圆圈图标 +- 关键数据或标语使用斜体强调文字 + +### 排版 + +**选择有趣的字体搭配** —— 不要默认使用 Arial。选择有个性的标题字体,搭配清晰的正文字体。 + +| 标题字体 | 正文字体 | +| ------------ | ------------- | +| Georgia | Calibri | +| Arial Black | Arial | +| Calibri | Calibri Light | +| Cambria | Calibri | +| Trebuchet MS | Calibri | +| Impact | Arial | +| Palatino | Garamond | +| Consolas | Calibri | + +| 元素 | 字号 | +| ---------- | ------------ | +| 幻灯片标题 | 36-44pt 粗体 | +| 章节标题 | 20-24pt 粗体 | +| 正文文本 | 14-16pt | +| 说明文字 | 10-12pt 弱化 | + +### 间距 + +- 最小边距 0.5 英寸 +- 内容块之间 0.3-0.5 英寸 +- 留出呼吸空间 —— 不要填满每一寸 + +### 避免事项(常见错误) + +- **不要重复使用相同布局** —— 在幻灯片间变化使用栏、卡片和突出显示 +- **正文不要居中** —— 段落和列表左对齐;只有标题居中 +- **不要吝啬字号对比** —— 标题需要 36pt+ 才能与 14-16pt 正文区分 +- **不要默认使用蓝色** —— 选择反映特定主题的颜色 +- **不要随意混合间距** —— 选择 0.3" 或 0.5" 间隙并保持一致 +- **不要只设计一张幻灯片而让其他保持朴素** —— 要么完全投入,要么全程保持简洁 +- **不要创建纯文字幻灯片** —— 添加图片、图标、图表或视觉元素;避免纯标题 + 列表 +- **不要忘记文本框内边距** —— 当将线条或形状与文本边缘对齐时,在文本框上设置 `margin: 0` 或偏移形状以考虑内边距 +- **不要使用低对比度元素** —— 图标和文字都需要与背景形成强对比;避免浅色背景上的浅色文字或深色背景上的深色文字 +- **绝对不要在标题下使用装饰线** —— 这是 AI 生成幻灯片的标志;改用留白或背景色 +- **绝对不要在 JavaScript 中使用中文引号(如:" ")** —— 会导致 PptxGenJS 崩溃或生成损坏文件;始终使用标准 ASCII 引号(`' '` 或 `" "`) + +### 设计质量检查清单 + +创建完幻灯片后,对照以下清单进行自我检查: + +**布局与对齐** + +- [ ] 图片、表格、图表是否对齐(底部或顶部对齐)? +- [ ] 文字块之间间距是否一致(统一使用 0.3" 或 0.5")? +- [ ] 是否避免了"后加"的感觉——底部内容是否与整体融为一体? + +**视觉层次** + +- [ ] 标题字号是否足够大(36pt+)与正文区分? +- [ ] 是否有清晰的视觉焦点(主图、核心数据、关键结论)? +- [ ] 信息密度是否适中——既不拥挤也不空洞? + +**图表与图片** + +- [ ] 图表颜色是否与整体配色方案协调? +- [ ] 图表是否与文物/照片风格统一? +- [ ] 图表是否放置在合适的位置——不是孤立在角落? + +**内容完整性** + +- [ ] 每张幻灯片是否有明确的单一主题? +- [ ] 数据是否有来源标注? +- [ ] 结论是否清晰可见? + +**可视化检查** + +```bash +# 生成缩略图检查整体效果 +python scripts/ppt_to_pic.py --file presentation.pptx --output thumbnails + +# 使用 Qwen 视觉分析 +python scripts/vision_qwen.py --image thumbnails/slide1.PNG --prompt "分析这张幻灯片的设计质量和改进建议" +``` + +--- + +## 依赖项 + +**核心依赖:** + +- `pip install "markitdown[pptx]"` - 文本提取 +- `pip install Pillow` - 缩略图网格 +- `npm install -g pptxgenjs` - 从零创建 +- LibreOffice (`soffice`) - PDF 转换(Linux) +- Poppler (`pdftoppm`) - PDF 转图片 + +**可视化工具:** + +- `pip install tencentcloud-sdk-python` - 腾讯搜索 API +- `pip install svglib reportlab` - SVG 转 PNG,用于视觉工具 + +**图表生成:** + +- Node.js >= 18.0.0 - 运行图表生成脚本 + +--- + +## 环境设置 + +为获得可视化工具和网络搜索的最佳效果: + +```bash +# 验证工具工作正常 +python scripts/web_search.py --query "test" --count 1 + +# 验证图表生成 +node scripts/generate.js '{"tool":"generate_column_chart","args":{"data":[{"category":"测试","value":100}],"title":"测试图表"}}' + +# 验证 LibreOffice 安装 +soffice --version + +# 验证 pdftoppm 安装 +pdftoppm -v +``` From 0f809ff2d6c6dc2d676623724f203646dacec675 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Sat, 28 Mar 2026 17:07:57 +0800 Subject: [PATCH 33/34] fix: resolve lint and CodeQL issues in PR checks --- libs/hexagent/tests/unit_tests/computer/test_wsl.py | 1 - .../backend/skills/email-mail-master/scripts/email_manager.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/hexagent/tests/unit_tests/computer/test_wsl.py b/libs/hexagent/tests/unit_tests/computer/test_wsl.py index c13bcd79..164e1ae1 100644 --- a/libs/hexagent/tests/unit_tests/computer/test_wsl.py +++ b/libs/hexagent/tests/unit_tests/computer/test_wsl.py @@ -860,4 +860,3 @@ async def test_empty_mounts_is_noop(self) -> None: ): await vm._apply_bind_mounts() mock_shell.assert_not_awaited() - diff --git a/libs/hexagent_demo/backend/skills/email-mail-master/scripts/email_manager.py b/libs/hexagent_demo/backend/skills/email-mail-master/scripts/email_manager.py index 1b6bcd95..ebf6faaf 100644 --- a/libs/hexagent_demo/backend/skills/email-mail-master/scripts/email_manager.py +++ b/libs/hexagent_demo/backend/skills/email-mail-master/scripts/email_manager.py @@ -102,8 +102,8 @@ def _html_to_text(self, html_content: str) -> str: from html import unescape # 移除 script 和 style 标签及其内容 - text = re.sub(r']*>.*?', '', html_content, flags=re.DOTALL | re.IGNORECASE) - text = re.sub(r']*>.*?', '', text, flags=re.DOTALL | re.IGNORECASE) + text = re.sub(r']*>.*?', '', html_content, flags=re.DOTALL | re.IGNORECASE) + text = re.sub(r']*>.*?', '', text, flags=re.DOTALL | re.IGNORECASE) # 移除所有 HTML 标签 text = re.sub(r'<[^>]+>', ' ', text) From 6d2765ff7729bc48b3d839c291008ad292f131f1 Mon Sep 17 00:00:00 2001 From: xuelin-cell Date: Sat, 28 Mar 2026 17:17:00 +0800 Subject: [PATCH 34/34] fix: harden html script/style tag filtering regex --- .../backend/skills/email-mail-master/scripts/email_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/hexagent_demo/backend/skills/email-mail-master/scripts/email_manager.py b/libs/hexagent_demo/backend/skills/email-mail-master/scripts/email_manager.py index ebf6faaf..8bee782a 100644 --- a/libs/hexagent_demo/backend/skills/email-mail-master/scripts/email_manager.py +++ b/libs/hexagent_demo/backend/skills/email-mail-master/scripts/email_manager.py @@ -102,8 +102,8 @@ def _html_to_text(self, html_content: str) -> str: from html import unescape # 移除 script 和 style 标签及其内容 - text = re.sub(r']*>.*?', '', html_content, flags=re.DOTALL | re.IGNORECASE) - text = re.sub(r']*>.*?', '', text, flags=re.DOTALL | re.IGNORECASE) + text = re.sub(r']*>.*?]*>', '', html_content, flags=re.DOTALL | re.IGNORECASE) + text = re.sub(r']*>.*?]*>', '', text, flags=re.DOTALL | re.IGNORECASE) # 移除所有 HTML 标签 text = re.sub(r'<[^>]+>', ' ', text)