From 0a4710871475bd09ae255a2822ff198e556f1d4e Mon Sep 17 00:00:00 2001 From: theater <1347507191@qq.com> Date: Wed, 23 Sep 2026 15:40:44 +0800 Subject: [PATCH 01/14] fix(update): checkpoint SQLite WAL before desktop process restart (#918) * fix(update): checkpoint SQLite WAL before desktop process restart In desktop/portable mode, POST /api/update/restart replaced the process image with os.execv without flushing the SQLite WAL. Because SqlitePool uses WAL mode with a large autocheckpoint window, all writes since the last checkpoint were lost on restart (#800). Change _restart_desktop_process to accept the OctopServer, and before execv run PRAGMA wal_checkpoint(TRUNCATE) and close the pool when the database is SQLite. The restart endpoint now injects the server via Depends(get_server). Fixes #800. * style(tests): format desktop restart regression cases --- src/octop/api/routers/update.py | 17 +++- tests/unit/api/test_update_router.py | 116 +++++++++++++++++++++++++-- 2 files changed, 126 insertions(+), 7 deletions(-) diff --git a/src/octop/api/routers/update.py b/src/octop/api/routers/update.py index 20761ab7..c1899d8f 100644 --- a/src/octop/api/routers/update.py +++ b/src/octop/api/routers/update.py @@ -22,6 +22,7 @@ update_task, ) from octop.infra.errors import ErrorCode, OctopError +from octop.infra.server import OctopServer from octop.infra.setup.github_releases import SOURCE_LABEL from octop.infra.setup.self_update import ( UpgradeResult, @@ -368,8 +369,19 @@ def _is_desktop_process() -> bool: return raw in {"1", "true", "yes", "on"} -def _restart_desktop_process() -> None: +def _restart_desktop_process(server: OctopServer) -> None: time.sleep(0.4) + + # Flush SQLite WAL before replacing the process image, otherwise uncheckpointed + # writes (common in WAL mode) are lost when the connection FD is closed by execv. + if server.services is not None and getattr(server.services.db, "dialect", None) == "sqlite": + try: + with server.services.db.connect() as conn: + conn.execute("PRAGMA wal_checkpoint(TRUNCATE)") + server.services.db.close() + except Exception: + logger.exception("failed to checkpoint/close database before desktop restart") + argv = list(getattr(sys, "orig_argv", None) or [sys.executable, *sys.argv]) os.execv(argv[0], argv) @@ -384,10 +396,11 @@ def _restart_service_task(runtime: ServiceRuntime) -> None: @router.post("/restart") async def restart_service_endpoint( background_tasks: BackgroundTasks, + server: OctopServer = Depends(get_server), _: Any = Depends(require_permission("update")), ) -> dict[str, Any]: if _is_desktop_process(): - background_tasks.add_task(_restart_desktop_process) + background_tasks.add_task(_restart_desktop_process, server) return {"status": "restarting", "service_mode": "desktop"} mode = detect_service_mode() if mode is None: diff --git a/tests/unit/api/test_update_router.py b/tests/unit/api/test_update_router.py index 08cbbbc9..ba18bac5 100644 --- a/tests/unit/api/test_update_router.py +++ b/tests/unit/api/test_update_router.py @@ -134,24 +134,130 @@ async def test_restart_endpoint_rejects_when_service_not_installed( async def test_restart_endpoint_desktop_schedules_process_exec( monkeypatch: pytest.MonkeyPatch, ) -> None: - called: list[bool] = [] + received: list[object] = [] monkeypatch.setattr(update_router, "_is_desktop_process", lambda: True) monkeypatch.setattr( update_router, "_restart_desktop_process", - lambda: called.append(True), + lambda server: received.append(server), ) from fastapi import BackgroundTasks + fake_server = object() bg = BackgroundTasks() - result = await update_router.restart_service_endpoint(bg, _=None) + result = await update_router.restart_service_endpoint(bg, server=fake_server, _=None) assert result == {"status": "restarting", "service_mode": "desktop"} - assert called == [] + assert received == [] for task in bg.tasks: await task() - assert called == [True] + assert received == [fake_server] + + +@pytest.mark.asyncio +async def test_restart_desktop_process_checkpoints_sqlite_wal( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Regression for #800: desktop restart must flush WAL before execv.""" + checkpoints: list[str] = [] + closes: list[bool] = [] + exec_args: list[tuple[str, list[str]]] = [] + + class FakeConn: + def execute(self, sql: str) -> None: + checkpoints.append(sql) + + class FakePool: + dialect = "sqlite" + + def connect(self): + class Ctx: + def __enter__(_self): + return FakeConn() + + def __exit__(_self, *args): + return None + + return Ctx() + + def close(self) -> None: + closes.append(True) + + class FakeServices: + db = FakePool() + + class FakeServer: + services = FakeServices() + + monkeypatch.setattr( + update_router, + "os", + type("os", (), {"execv": lambda _self, p, a: exec_args.append((p, a))})(), + ) + monkeypatch.setattr( + update_router, + "sys", + type( + "sys", (), {"executable": "/bin/python", "argv": ["octop", "run"], "orig_argv": None} + )(), + ) + monkeypatch.setattr(update_router.time, "sleep", lambda _s: None) + + update_router._restart_desktop_process(FakeServer()) + + assert len(checkpoints) == 1 + assert "PRAGMA wal_checkpoint(TRUNCATE)" in checkpoints[0] + assert closes == [True] + assert len(exec_args) == 1 + + +@pytest.mark.asyncio +async def test_restart_desktop_process_swallows_checkpoint_errors( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A failing checkpoint should not prevent restart; data loss is logged.""" + exec_args: list[tuple[str, list[str]]] = [] + + class BadPool: + dialect = "sqlite" + + def connect(self): + class Ctx: + def __enter__(_self): + raise RuntimeError("db busy") + + def __exit__(_self, *args): + return None + + return Ctx() + + def close(self) -> None: + pass + + class FakeServices: + db = BadPool() + + class FakeServer: + services = FakeServices() + + monkeypatch.setattr( + update_router, + "os", + type("os", (), {"execv": lambda _self, p, a: exec_args.append((p, a))})(), + ) + monkeypatch.setattr( + update_router, + "sys", + type( + "sys", (), {"executable": "/bin/python", "argv": ["octop", "run"], "orig_argv": None} + )(), + ) + monkeypatch.setattr(update_router.time, "sleep", lambda _s: None) + + update_router._restart_desktop_process(FakeServer()) + + assert len(exec_args) == 1 @pytest.mark.asyncio From 0b2ac8b66e9e43343df94993c14a7d485d5ccec2 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 3 Oct 2026 23:42:50 +0800 Subject: [PATCH 02/14] fix(ollama): discover user-started local daemon --- CHANGELOG.md | 4 ++ src/octop/api/routers/ollama_models.py | 12 ++++-- src/octop/infra/utils/ollama_manager.py | 6 +-- tests/unit/api/test_ollama_service_default.py | 41 +++++++++++++++++++ 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f87fb586..1a7ed040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ ## [Unreleased] +### Fixed + +- Detect models from an already-running Ollama daemon even when FreeOS-managed Ollama startup is disabled; model discovery never starts Ollama unless the user enabled it. + ## [0.0.10] - 2026-10-03 ### Fixed diff --git a/src/octop/api/routers/ollama_models.py b/src/octop/api/routers/ollama_models.py index 4d42e2c8..334f517e 100644 --- a/src/octop/api/routers/ollama_models.py +++ b/src/octop/api/routers/ollama_models.py @@ -81,15 +81,19 @@ async def list_ollama_models( _: Any = Depends(require_permission("ollama_models")), ) -> list[OllamaModelResponse]: """Return the current Ollama model list via the SDK.""" - if not _ollama_service_enabled(server): + from octop.infra.utils.ollama_manager import ( + OllamaModelManager, + is_ollama_reachable, + ) + + service_enabled = _ollama_service_enabled(server) + if not service_enabled and not is_ollama_reachable(): raise HTTPException( status_code=503, detail="Ollama service is disabled. Enable it from the provider card.", ) try: - from octop.infra.utils.ollama_manager import OllamaModelManager - - models = OllamaModelManager.list_models() + models = OllamaModelManager.list_models(start_if_needed=service_enabled) except (OSError, ImportError) as exc: logger.warning("Ollama bootstrap failed: %s", exc) raise HTTPException( diff --git a/src/octop/infra/utils/ollama_manager.py b/src/octop/infra/utils/ollama_manager.py index e29d0c3a..1558922c 100644 --- a/src/octop/infra/utils/ollama_manager.py +++ b/src/octop/infra/utils/ollama_manager.py @@ -387,9 +387,9 @@ class OllamaModelManager: """High-level wrapper around the Ollama SDK for model lifecycle.""" @staticmethod - def list_models() -> list[OllamaModelInfo]: - """Return the current model list from ``ollama.list()``.""" - ollama = _ensure_ollama() + def list_models(*, start_if_needed: bool = True) -> list[OllamaModelInfo]: + """Return models without starting Ollama unless explicitly allowed.""" + ollama = _ensure_ollama() if start_if_needed else _ensure_ollama_sdk() models: list[OllamaModelInfo] = [] for m in _iter_ollama_list_models(ollama.list()): name = str(m.get("model") or m.get("name") or "") diff --git a/tests/unit/api/test_ollama_service_default.py b/tests/unit/api/test_ollama_service_default.py index de913972..53370dda 100644 --- a/tests/unit/api/test_ollama_service_default.py +++ b/tests/unit/api/test_ollama_service_default.py @@ -5,6 +5,9 @@ from types import SimpleNamespace from unittest.mock import MagicMock +import pytest +from fastapi import HTTPException + from octop.api.routers import ollama_models @@ -18,3 +21,41 @@ def test_ollama_service_respects_stored_true() -> None: server = SimpleNamespace(services=SimpleNamespace(settings_repo=MagicMock())) server.services.settings_repo.get.return_value = "true" assert ollama_models._ollama_service_enabled(server) is True + + +@pytest.mark.asyncio +async def test_list_uses_running_daemon_when_service_disabled( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from octop.infra.utils import ollama_manager as om + from octop.infra.utils.ollama_manager import OllamaModelInfo + + monkeypatch.setattr(om, "is_ollama_reachable", lambda: True) + listed = MagicMock(return_value=[OllamaModelInfo(name="llama3.2:latest")]) + monkeypatch.setattr(om.OllamaModelManager, "list_models", listed) + server = SimpleNamespace(services=SimpleNamespace(settings_repo=MagicMock())) + server.services.settings_repo.get.return_value = "false" + + result = await ollama_models.list_ollama_models(server=server, _=None) + + assert [model.name for model in result] == ["llama3.2:latest"] + listed.assert_called_once_with(start_if_needed=False) + + +@pytest.mark.asyncio +async def test_list_does_not_start_unreachable_daemon_when_service_disabled( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from octop.infra.utils import ollama_manager as om + + monkeypatch.setattr(om, "is_ollama_reachable", lambda: False) + listed = MagicMock() + monkeypatch.setattr(om.OllamaModelManager, "list_models", listed) + server = SimpleNamespace(services=SimpleNamespace(settings_repo=MagicMock())) + server.services.settings_repo.get.return_value = "false" + + with pytest.raises(HTTPException) as exc_info: + await ollama_models.list_ollama_models(server=server, _=None) + + assert exc_info.value.status_code == 503 + listed.assert_not_called() From fff84117a1cf940ff22620ee2eedf9e756f268f9 Mon Sep 17 00:00:00 2001 From: HUANG Cheng Date: Thu, 17 Sep 2026 00:36:40 -0700 Subject: [PATCH 03/14] fix(config): never rewrite an unparseable config.json as empty (#730) (#738) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(config): never rewrite an unparseable config.json as empty (#730) Four call sites read config.json, treated a parse failure as an empty dict and wrote that back, destroying every other key. One trailing comma plus one `octop run --port 8088` wiped 17 of 18 keys including the whole `database` section; the next boot silently came up as a greenfield SQLite instance (fresh setup-wizard password, zero warnings) while the PostgreSQL data sat unreachable. Losing `bind_host` the same way is what makes #730's reporter unable to reach the server from the LAN. A torn write creates the same precondition with no user error: three of the four sites wrote config.json with a bare write_text, so a crash / OOM / full disk mid-write left invalid JSON that the next plugin toggle then wiped. One shared policy in a new leaf helper, infra/utils/json_file.py: absent file -> {} and write (first run, unchanged) valid JSON object -> merge and write (unchanged) unparseable/non-object -> raise CONFIG_FILE_CORRUPT, write nothing The error names the file and the parser line/column but never the contents, which hold database credentials. Writes now go through write_json_atomic (temp + os.replace, fd closed on every path, temp removed on failure) so no reader can observe a torn file. Sites: cli/commands/run.py (_save_configfile_overrides, plus _load_configfile_overrides which used to fall back to launch defaults), infra/agents/plugins/manager.py (_write_global_plugin_enabled = the dashboard toggle), infra/setup/service.py (persist_bind_options), infra/agents/plugins/seed.py. `octop run` / `octop service start` report the error and exit 1 via fail_octop (now NoReturn) instead of a traceback. Supersedes the pinned "treat a malformed file as empty so the next run rewrites it cleanly" contract: its fixture (`{not valid json`) had nothing to lose, so the test could not observe the destruction. Rewritten as test_run_reports_corrupt_config_cleanly and test_run_does_not_overwrite_corrupt_config. Verified end-to-end against a real OctopServer + real PostgreSQL. Before: one `octop run --port` flipped driver postgresql -> sqlite, the instance reverted to the first-run wizard, and users=1/agents=1 were orphaned in PG with no warning. After: exit 1, config.json byte-identical, boot fails loudly instead of switching backends. Mutation check: restoring the lenient policy in the helper reddens 9 tests spanning all four sites. make all green (3299 passed, 16 skipped); mypy --strict clean; dashboard `npx tsc --noEmit` clean. * fix(config): name the file when config.json is corrupt (#730) `load_config` let a bare `json.JSONDecodeError` escape: it reports a line and column but never says *which* file failed, and this entry point is reached from boot, backup and several CLI commands — not only `octop run`. Raise a ValueError naming the path and the parser position, refuse a non-object document, and never echo file contents (config.json holds database credentials). Same policy as infra/utils/json_file.py, duplicated here because octop.config must stay free of infra imports (AGENTS.md §5). `octop run` already exits cleanly on a corrupt config through resolve_bind; this covers the remaining entry points. make all green (3301 passed, 16 skipped); mypy --strict clean. * fix(config): preserve config.json permissions in atomic writes Self-review against CONTRIBUTING.md / AGENTS.md §7 caught a regression in the atomic writer added earlier in this branch: tempfile.mkstemp creates the temp file 0600, so replacing config.json silently tightened an existing 0644 file to 0600. Under `octop service start --scope system` that runs as root and writes into the service user's home, a root-owned 0600 config.json is unreadable by the service user and the unit cannot start; the bare write_text it replaced produced the umask default. write_json_atomic now chmods the temp file to the mode of the file being replaced, or to `0o666 & ~umask` for a new file — matching the two pre-existing config.json writers (infra/db/rebind.py, infra/backup/auto.py; both write_text + replace) rather than mkstemp's 0600. POSIX-guarded, so Windows behaviour is unchanged and no test asserts mode bits off POSIX (AGENTS.md §7). Also adds the missing CHANGELOG line for the load_config error-message change. Verified: new file -> 0644, existing 0644 -> 0644, existing 0600 -> 0600. make all green (3303 passed, 16 skipped); make typecheck-frontend green; eslint 0 errors (67 pre-existing react-hooks warnings). Note: `make check-all` is red on develop itself — prettier --check fails on dashboard/src/pages/Agent/Skills/components/SkillsTabs.tsx exactly as committed by 83378876. Left untouched here; confirmed pre-existing by checking develop's version of that file in isolation. --- CHANGELOG.md | 1 + dashboard/src/locales/en.json | 3 +- dashboard/src/locales/zh.json | 3 +- src/octop/cli/commands/run.py | 99 +++++++++---------- src/octop/cli/commands/service.py | 13 ++- src/octop/cli/support/errors.py | 4 +- src/octop/config.py | 22 ++++- src/octop/i18n/en.json | 3 +- src/octop/i18n/zh.json | 3 +- src/octop/infra/agents/plugins/manager.py | 31 +++--- src/octop/infra/agents/plugins/seed.py | 27 +++-- src/octop/infra/errors.py | 11 +++ src/octop/infra/setup/service.py | 30 +++--- src/octop/infra/utils/json_file.py | 109 +++++++++++++++++++++ tests/unit/cli/test_run_bind_resolution.py | 78 ++++++++++++++- tests/unit/cli/test_run_cmd.py | 49 ++++++--- tests/unit/i18n/test_errors.py | 9 ++ tests/unit/infra/setup/test_service.py | 13 +++ tests/unit/infra/utils/test_json_file.py | 107 ++++++++++++++++++++ tests/unit/test_config.py | 22 +++++ tests/unit/test_plugin_manager.py | 35 +++++++ tests/unit/test_plugin_seed.py | 18 ++++ 22 files changed, 577 insertions(+), 113 deletions(-) create mode 100644 src/octop/infra/utils/json_file.py create mode 100644 tests/unit/infra/utils/test_json_file.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a7ed040..95e96071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed - Detect models from an already-running Ollama daemon even when FreeOS-managed Ollama startup is disabled; model discovery never starts Ollama unless the user enabled it. +- Preserve `config.json` when it is malformed: startup and all configuration writers now report the file and parser position, avoid leaking stored credentials, and use atomic replacement instead of silently rebuilding an empty configuration. ## [0.0.10] - 2026-10-03 diff --git a/dashboard/src/locales/en.json b/dashboard/src/locales/en.json index d361d312..a80b207a 100644 --- a/dashboard/src/locales/en.json +++ b/dashboard/src/locales/en.json @@ -440,7 +440,8 @@ "INVITE_RATE_LIMITED": "Too many invite attempts. Try again later.", "WORKSPACE_ROOT_RESTRICTED": "The selected storage root is outside the allowed directory for this account.", "TOKEN_QUOTA_EXCEEDED": "This account has reached its token quota. Please contact an administrator to raise the limit ({{used}}/{{quota}}).", - "LOCAL_MODEL_REGISTER_FAILED": "Could not register the local model: {{reason}}" + "LOCAL_MODEL_REGISTER_FAILED": "Could not register the local model: {{reason}}", + "CONFIG_FILE_CORRUPT": "Cannot update {{path}}: the file is not valid JSON ({{detail}}). Fix the syntax and retry — no settings were changed." }, "nav": { "chat": "Conversation", diff --git a/dashboard/src/locales/zh.json b/dashboard/src/locales/zh.json index 3cc0ad3e..edb2a764 100644 --- a/dashboard/src/locales/zh.json +++ b/dashboard/src/locales/zh.json @@ -440,7 +440,8 @@ "INVITE_RATE_LIMITED": "邀请尝试次数过多,请稍后再试。", "WORKSPACE_ROOT_RESTRICTED": "所选存储根目录超出了该账号允许的范围。", "TOKEN_QUOTA_EXCEEDED": "该账号已达到 token 配额上限,请联系管理员提升额度({{used}}/{{quota}})。", - "LOCAL_MODEL_REGISTER_FAILED": "无法注册本地模型:{{reason}}" + "LOCAL_MODEL_REGISTER_FAILED": "无法注册本地模型:{{reason}}", + "CONFIG_FILE_CORRUPT": "无法更新 {{path}}:该文件不是合法 JSON({{detail}})。请修正语法后重试;现有配置未被改动。" }, "nav": { "chat": "对话", diff --git a/src/octop/cli/commands/run.py b/src/octop/cli/commands/run.py index 643cf90e..5ce91c7c 100644 --- a/src/octop/cli/commands/run.py +++ b/src/octop/cli/commands/run.py @@ -2,15 +2,16 @@ from __future__ import annotations -import contextlib -import json -import os -import tempfile -from pathlib import Path - import click +from octop.cli.support.errors import fail_octop from octop.config import env_bind_overrides +from octop.infra.errors import OctopError, corrupt_config_error +from octop.infra.utils.json_file import ( + JsonFileCorruptError, + read_json_object, + write_json_atomic, +) from octop.infra.utils.paths import PathLayout @@ -69,20 +70,29 @@ def _maybe_generate_self_signed( def _load_configfile_overrides() -> tuple[str | None, int | None]: + """Read ``bind_host`` / ``port`` from ``config.json``. + + An absent or unreadable file falls back to the launch defaults, but a + *corrupt* one raises: silently ignoring it would bind defaults while + discarding every other setting, and boot would then die with a bare + ``JSONDecodeError`` traceback instead of an actionable message. + """ cfg_path = PathLayout.from_env().config - if not cfg_path.exists(): + try: + data = read_json_object(cfg_path) + except JsonFileCorruptError as exc: + raise corrupt_config_error(exc.path, exc.detail) from exc + except OSError: return None, None - with contextlib.suppress(OSError, json.JSONDecodeError): - data = json.loads(cfg_path.read_text(encoding="utf-8")) - if isinstance(data, dict): - # Prefer new key "bind_host"; fall back to legacy "host" for old configs. - host = data.get("bind_host") or data.get("host") - port = data.get("port") - return ( - host if isinstance(host, str) else None, - port if isinstance(port, int) and not isinstance(port, bool) else None, - ) - return None, None + if data is None: + return None, None + # Prefer new key "bind_host"; fall back to legacy "host" for old configs. + host = data.get("bind_host") or data.get("host") + port = data.get("port") + return ( + host if isinstance(host, str) else None, + port if isinstance(port, int) and not isinstance(port, bool) else None, + ) def resolve_bind(host: str | None, port: int | None) -> tuple[str | None, int | None]: @@ -96,48 +106,30 @@ def resolve_bind(host: str | None, port: int | None) -> tuple[str | None, int | return resolved_host, resolved_port -def _atomic_write_json(path: Path, data: dict[str, object]) -> None: - """Write JSON atomically: temp file + ``os.replace`` to avoid torn writes. +def _save_configfile_overrides(host: str | None, port: int | None) -> None: + """Persist resolved host/port to config.json (read-merge-write, atomic). - Uses ``os.write`` with pre-encoded bytes (instead of ``os.fdopen``) so the - file descriptor is owned and closed deterministically, even if encoding - is unavailable or the process is interrupted mid-write. + A corrupt config.json is a hard error, never an empty base: merging into + ``{}`` and writing back destroys every other setting (issue #730 — one + trailing comma plus ``octop run --port`` wiped the ``database`` section and + silently flipped a PostgreSQL instance back to greenfield SQLite). """ - payload = (json.dumps(data, indent=2) + "\n").encode("utf-8") - fd, tmp_name = tempfile.mkstemp(dir=path.parent, prefix=".config.", suffix=".tmp") - try: - try: - os.write(fd, payload) - finally: - # Always close — covers the case where ``os.write`` itself raises - # (disk full, EIO, EPIPE) so the fd does not leak. - os.close(fd) - os.replace(tmp_name, path) - except BaseException: # also covers KeyboardInterrupt / SystemExit - # Best-effort cleanup of the temp file on any failure path. - with contextlib.suppress(OSError): - os.unlink(tmp_name) - raise - - -def _save_configfile_overrides(host: str | None, port: int | None) -> None: - """Persist resolved host/port to config.json (read-merge-write, atomic).""" paths = PathLayout.from_env() paths.ensure_root() cfg_path = paths.config - data: dict[str, object] = {} - if cfg_path.exists(): - with contextlib.suppress(OSError, json.JSONDecodeError): - loaded = json.loads(cfg_path.read_text(encoding="utf-8")) - if isinstance(loaded, dict): - data = loaded + try: + data = read_json_object(cfg_path) + except JsonFileCorruptError as exc: + raise corrupt_config_error(exc.path, exc.detail) from exc + if data is None: + data = {} if host is not None: data["bind_host"] = host if port is not None: data["port"] = port click.echo(f"Saved config to {cfg_path}") try: - _atomic_write_json(cfg_path, data) + write_json_atomic(cfg_path, data) except OSError as exc: click.echo(f"Warning: could not save config.json: {exc}", err=True) @@ -181,9 +173,12 @@ def run( starts. """ cli_host, cli_port = host, port - host, port = resolve_bind(host, port) - if cli_host is not None or cli_port is not None: - _save_configfile_overrides(cli_host, cli_port) + try: + host, port = resolve_bind(host, port) + if cli_host is not None or cli_port is not None: + _save_configfile_overrides(cli_host, cli_port) + except OctopError as exc: + fail_octop(exc) certfile, keyfile = _maybe_generate_self_signed(ssl, ssl_certfile, ssl_keyfile) _run_uvicorn( host=host, diff --git a/src/octop/cli/commands/service.py b/src/octop/cli/commands/service.py index b23f9af5..168a9cbd 100644 --- a/src/octop/cli/commands/service.py +++ b/src/octop/cli/commands/service.py @@ -2,8 +2,11 @@ from __future__ import annotations +from typing import NoReturn + import click +from octop.infra.errors import OctopError from octop.infra.setup.service import ( DEFAULT_HEALTH_ATTEMPTS, DEFAULT_HEALTH_DELAY_SECONDS, @@ -35,7 +38,7 @@ def _runtime(host: str | None, port: int | None, scope: ServiceScope | None) -> raise SystemExit(1) from exc -def _fail(exc: Exception) -> None: +def _fail(exc: Exception) -> NoReturn: click.echo(f"error: {exc}", err=True) raise SystemExit(1) from exc @@ -125,13 +128,13 @@ def _echo_summary_for(runtime: ServiceRuntime, *, warn_health: bool) -> None: def start(host: str | None, port: int | None, force_install: bool, scope: str) -> None: """Install if needed, ensure the NOFILE drop-in, then start the service.""" runtime = _runtime(host, port, _resolve_scope(scope)) - if host is not None or port is not None: - persist_bind_options(runtime.home, host=host, port=port) - runtime = _runtime(host, port, _resolve_scope(scope)) try: + if host is not None or port is not None: + persist_bind_options(runtime.home, host=host, port=port) + runtime = _runtime(host, port, _resolve_scope(scope)) wrote = install_service(runtime, force=force_install) start_service(runtime, apply_unit=wrote) - except RuntimeError as exc: + except (RuntimeError, OctopError) as exc: _fail(exc) _echo_summary_for(runtime, warn_health=True) diff --git a/src/octop/cli/support/errors.py b/src/octop/cli/support/errors.py index efa4f2b3..d445a176 100644 --- a/src/octop/cli/support/errors.py +++ b/src/octop/cli/support/errors.py @@ -2,11 +2,13 @@ from __future__ import annotations +from typing import NoReturn + import click from octop.infra.errors import OctopError -def fail_octop(exc: OctopError) -> None: +def fail_octop(exc: OctopError) -> NoReturn: click.echo(f"error: {exc.message}", err=True) raise SystemExit(1) from exc diff --git a/src/octop/config.py b/src/octop/config.py index 8a396882..4d61334a 100644 --- a/src/octop/config.py +++ b/src/octop/config.py @@ -410,10 +410,28 @@ def env_bind_overrides() -> tuple[str | None, int | None]: def load_config(path: Path) -> OctopConfig: - """Load ``config.json``; write defaults if absent. Apply env overrides.""" + """Load ``config.json``; write defaults if absent. Apply env overrides. + + A corrupt file raises with the path and parser position named, and is never + treated as empty: this is the same policy as ``infra/utils/json_file.py``, + duplicated here because ``octop.config`` must stay free of ``infra`` imports + (AGENTS.md §5). The message never echoes file contents — they hold database + credentials. + """ file_defaults = _defaults_for_file() if path.exists(): - raw = json.loads(path.read_text(encoding="utf-8")) + try: + raw = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError( + f"{path} is not valid JSON (line {exc.lineno}, column {exc.colno});" + " fix it and retry — no settings were changed" + ) from exc + if not isinstance(raw, dict): + raise ValueError( + f"{path} must contain a JSON object, got {type(raw).__name__};" + " fix it and retry — no settings were changed" + ) database_in_file = "database" in raw else: path.parent.mkdir(parents=True, exist_ok=True) diff --git a/src/octop/i18n/en.json b/src/octop/i18n/en.json index ab42ce25..a51f35e9 100644 --- a/src/octop/i18n/en.json +++ b/src/octop/i18n/en.json @@ -405,7 +405,8 @@ "INVITE_RATE_LIMITED": "Too many invite attempts. Try again later.", "WORKSPACE_ROOT_RESTRICTED": "The selected storage root is outside the allowed directory for this account.", "TOKEN_QUOTA_EXCEEDED": "This account has reached its token quota. Please contact an administrator to raise the limit ({used}/{quota}).", - "LOCAL_MODEL_REGISTER_FAILED": "Could not register the local model: {reason}" + "LOCAL_MODEL_REGISTER_FAILED": "Could not register the local model: {reason}", + "CONFIG_FILE_CORRUPT": "Cannot update {path}: the file is not valid JSON ({detail}). Fix the syntax and retry — no settings were changed." }, "desktop": { "install_log_deps": "Installing Python dependencies…", diff --git a/src/octop/i18n/zh.json b/src/octop/i18n/zh.json index 9ed4b607..0f9f7c5d 100644 --- a/src/octop/i18n/zh.json +++ b/src/octop/i18n/zh.json @@ -405,7 +405,8 @@ "INVITE_RATE_LIMITED": "邀请尝试次数过多,请稍后再试。", "WORKSPACE_ROOT_RESTRICTED": "所选存储根目录超出了该账号允许的范围。", "TOKEN_QUOTA_EXCEEDED": "该账号已达到 token 配额上限,请联系管理员提升额度({used}/{quota})。", - "LOCAL_MODEL_REGISTER_FAILED": "无法注册本地模型:{reason}" + "LOCAL_MODEL_REGISTER_FAILED": "无法注册本地模型:{reason}", + "CONFIG_FILE_CORRUPT": "无法更新 {path}:该文件不是合法 JSON({detail})。请修正语法后重试;现有配置未被改动。" }, "desktop": { "install_log_deps": "正在安装 Python 依赖…", diff --git a/src/octop/infra/agents/plugins/manager.py b/src/octop/infra/agents/plugins/manager.py index 56596498..1ad51d27 100644 --- a/src/octop/infra/agents/plugins/manager.py +++ b/src/octop/infra/agents/plugins/manager.py @@ -24,7 +24,12 @@ unload_plugin, ) -from octop.infra.errors import ErrorCode, OctopError +from octop.infra.errors import ErrorCode, OctopError, corrupt_config_error +from octop.infra.utils.json_file import ( + JsonFileCorruptError, + read_json_object, + write_json_atomic, +) logger = logging.getLogger(__name__) @@ -73,15 +78,18 @@ def _read_global_plugins(config_path: Path) -> dict[str, bool]: def _write_global_plugin_enabled(config_path: Path, plugin_id: str, enabled: bool) -> None: - """Merge ``plugins..enabled`` into ``config.json`` without dropping other keys.""" - data: dict[str, Any] = {} - if config_path.is_file(): - try: - raw = json.loads(config_path.read_text(encoding="utf-8")) - if isinstance(raw, dict): - data = raw - except Exception: - data = {} + """Merge ``plugins..enabled`` into ``config.json`` without dropping other keys. + + A corrupt config.json raises instead of being treated as empty: merging into + ``{}`` and writing back destroys every other setting, including the + ``database`` section (issue #730). + """ + try: + data = read_json_object(config_path) + except JsonFileCorruptError as exc: + raise corrupt_config_error(exc.path, exc.detail) from exc + if data is None: + data = {} plugins = data.get("plugins") if not isinstance(plugins, dict): plugins = {} @@ -91,8 +99,7 @@ def _write_global_plugin_enabled(config_path: Path, plugin_id: str, enabled: boo entry = {} entry["enabled"] = bool(enabled) plugins[plugin_id] = entry - config_path.parent.mkdir(parents=True, exist_ok=True) - config_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + write_json_atomic(config_path, data) def _assert_http_url(url: str) -> None: diff --git a/src/octop/infra/agents/plugins/seed.py b/src/octop/infra/agents/plugins/seed.py index ba2dcbac..9fa79a61 100644 --- a/src/octop/infra/agents/plugins/seed.py +++ b/src/octop/infra/agents/plugins/seed.py @@ -2,27 +2,36 @@ from __future__ import annotations -import json import shutil from pathlib import Path from typing import Any import yaml +from octop.infra.errors import corrupt_config_error +from octop.infra.utils.json_file import ( + JsonFileCorruptError, + read_json_object, + write_json_atomic, +) + def _read_config(config_path: Path) -> dict[str, Any]: - if not config_path.is_file(): - return {} + """Load ``config.json`` for merging; ``{}`` when absent. + + A corrupt file raises instead of reading as empty: ``seed_bundled_plugins`` + always writes back, so merging into ``{}`` would destroy every other setting + (issue #730). + """ try: - raw = json.loads(config_path.read_text(encoding="utf-8")) - except Exception: - return {} - return raw if isinstance(raw, dict) else {} + data = read_json_object(config_path) + except JsonFileCorruptError as exc: + raise corrupt_config_error(exc.path, exc.detail) from exc + return data if data is not None else {} def _write_config(config_path: Path, data: dict[str, Any]) -> None: - config_path.parent.mkdir(parents=True, exist_ok=True) - config_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + write_json_atomic(config_path, data) def _plugin_version(plugin_dir: Path) -> tuple[int, ...]: diff --git a/src/octop/infra/errors.py b/src/octop/infra/errors.py index 23344cc8..5033812c 100644 --- a/src/octop/infra/errors.py +++ b/src/octop/infra/errors.py @@ -110,6 +110,7 @@ class ErrorCode(StrEnum): WORKSPACE_ROOT_RESTRICTED = "WORKSPACE_ROOT_RESTRICTED" TOKEN_QUOTA_EXCEEDED = "TOKEN_QUOTA_EXCEEDED" LOCAL_MODEL_REGISTER_FAILED = "LOCAL_MODEL_REGISTER_FAILED" + CONFIG_FILE_CORRUPT = "CONFIG_FILE_CORRUPT" _DEFAULT_STATUS: dict[ErrorCode, int] = { @@ -211,6 +212,7 @@ class ErrorCode(StrEnum): ErrorCode.WORKSPACE_ROOT_RESTRICTED: 400, ErrorCode.TOKEN_QUOTA_EXCEEDED: 403, ErrorCode.LOCAL_MODEL_REGISTER_FAILED: 400, + ErrorCode.CONFIG_FILE_CORRUPT: 400, } @@ -260,3 +262,12 @@ def to_envelope(self, *, locale: str | Locale | None = None) -> dict[str, Any]: "details": self.details, } } + + +def corrupt_config_error(path: object, detail: str) -> OctopError: + """Report invalid JSON without exposing configuration contents.""" + return OctopError( + ErrorCode.CONFIG_FILE_CORRUPT, + f"{path} is not valid JSON ({detail}); fix it and retry — no settings were changed", + details={"path": str(path), "detail": detail}, + ) diff --git a/src/octop/infra/setup/service.py b/src/octop/infra/setup/service.py index a81abfee..f84fc77a 100644 --- a/src/octop/infra/setup/service.py +++ b/src/octop/infra/setup/service.py @@ -2,7 +2,6 @@ from __future__ import annotations -import json import logging import os import platform @@ -11,13 +10,18 @@ import sys import tempfile import time -from contextlib import suppress from dataclasses import dataclass from pathlib import Path from typing import Literal from octop.config import load_config +from octop.infra.errors import corrupt_config_error from octop.infra.utils import posix_compat as pwd +from octop.infra.utils.json_file import ( + JsonFileCorruptError, + read_json_object, + write_json_atomic, +) from octop.infra.utils.paths import PathLayout from octop.infra.utils.posix_compat import chown, geteuid, getuid, is_root @@ -232,22 +236,26 @@ def resolve_service_home(*, run_as_user: str | None = None) -> Path: def persist_bind_options(home: Path, *, host: str | None = None, port: int | None = None) -> None: - """Write bind host/port into ``config.json`` before (re)installing the unit.""" + """Write bind host/port into ``config.json`` before (re)installing the unit. + + Raises on a corrupt config.json rather than treating it as empty — writing a + merged-into-``{}`` dict would destroy every other setting, including the + ``database`` section (issue #730). + """ if host is None and port is None: return config_path = home / "config.json" - data: dict[str, object] = {} - if config_path.is_file(): - with suppress(OSError, json.JSONDecodeError): - loaded = json.loads(config_path.read_text(encoding="utf-8")) - if isinstance(loaded, dict): - data = loaded + try: + data = read_json_object(config_path) + except JsonFileCorruptError as exc: + raise corrupt_config_error(exc.path, exc.detail) from exc + if data is None: + data = {} if host is not None: data["bind_host"] = host if port is not None: data["port"] = port - home.mkdir(parents=True, exist_ok=True) - config_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + write_json_atomic(config_path, data) def resolve_bind_options(home: Path | None = None) -> tuple[str, int]: diff --git a/src/octop/infra/utils/json_file.py b/src/octop/infra/utils/json_file.py new file mode 100644 index 00000000..184904d4 --- /dev/null +++ b/src/octop/infra/utils/json_file.py @@ -0,0 +1,109 @@ +"""Safe read/write helpers for the small JSON config files under ``OCTOP_HOME``. + +Leaf module (AGENTS.md §5): stdlib only, no ``infra`` imports. Callers translate +:class:`JsonFileCorruptError` into ``OctopError`` via +``octop.infra.errors.corrupt_config_error``. + +Both helpers exist because of issue #730: four call sites used to treat an +unparseable ``config.json`` as an empty dict and then wrote that back, silently +destroying every other key. One trailing comma plus one ``octop run --port`` +wiped the ``database`` section and flipped a running PostgreSQL instance back to +a greenfield SQLite one, with no warning in the log. A torn write from a crash or +a full disk creates the same precondition without any user error, so writes here +are atomic too. +""" + +from __future__ import annotations + +import contextlib +import json +import os +import stat +import tempfile +from pathlib import Path +from typing import Any + +__all__ = ["JsonFileCorruptError", "read_json_object", "write_json_atomic"] + + +def _default_file_mode() -> int: + """The mode a plain ``write_text`` would produce (``0o666 & ~umask``). + + POSIX has no get-only umask call, so read it by set-and-restore. Matches the + two pre-existing config.json writers (``db/rebind.py``, ``backup/auto.py``) + instead of leaving ``mkstemp``'s 0600, which would make a root-created + config.json unreadable to the service user after + ``octop service start --scope system``. + """ + mask = os.umask(0o022) + os.umask(mask) + return 0o666 & ~mask + + +class JsonFileCorruptError(ValueError): + """A JSON file exists but cannot be parsed into an object. + + Carries the path and the parser position only — never the file contents or + any value from them, since ``config.json`` holds database credentials and + secrets (precedent: ``octop.config`` refuses to log raw env values). + """ + + def __init__(self, path: Path, detail: str) -> None: + self.path = path + self.detail = detail + super().__init__(f"{path} is not valid JSON ({detail})") + + +def read_json_object(path: Path) -> dict[str, Any] | None: + """Return the JSON object at ``path``, or ``None`` when the file is absent. + + Raises :class:`JsonFileCorruptError` when the file exists but is not valid + JSON or is not a JSON object. Callers must **not** fall back to an empty + dict: merging into ``{}`` and writing back is what destroys every other + setting. An absent file is legitimate (first run) and yields ``None``. + """ + if not path.is_file(): + return None + try: + raw = path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise JsonFileCorruptError(path, "not valid UTF-8") from exc + try: + parsed = json.loads(raw) + except json.JSONDecodeError as exc: + raise JsonFileCorruptError(path, f"line {exc.lineno}, column {exc.colno}") from exc + if not isinstance(parsed, dict): + raise JsonFileCorruptError(path, f"expected a JSON object, got {type(parsed).__name__}") + return parsed + + +def write_json_atomic(path: Path, data: dict[str, Any]) -> None: + """Write ``data`` as pretty JSON via temp file + ``os.replace``. + + Readers never observe a partially written file, so an interrupted write + cannot leave the truncated JSON that :func:`read_json_object` then refuses + to merge. The temp file is unique per call (``mkstemp``), so concurrent + writers cannot share one ``.tmp`` name. + + Permissions follow the file being replaced, or the process umask for a new + file — a config write must never silently tighten or loosen the mode of a + file the user already had. + """ + payload = (json.dumps(data, indent=2) + "\n").encode("utf-8") + path.parent.mkdir(parents=True, exist_ok=True) + existing_mode: int | None = None + if os.name == "posix" and path.exists(): + existing_mode = stat.S_IMODE(path.stat().st_mode) + fd, tmp_name = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.", suffix=".tmp") + try: + try: + os.write(fd, payload) + finally: + os.close(fd) + if os.name == "posix": + os.chmod(tmp_name, _default_file_mode() if existing_mode is None else existing_mode) + os.replace(tmp_name, path) + except BaseException: # also covers KeyboardInterrupt / SystemExit + with contextlib.suppress(OSError): + os.unlink(tmp_name) + raise diff --git a/tests/unit/cli/test_run_bind_resolution.py b/tests/unit/cli/test_run_bind_resolution.py index ff7c72ac..a5985647 100644 --- a/tests/unit/cli/test_run_bind_resolution.py +++ b/tests/unit/cli/test_run_bind_resolution.py @@ -4,10 +4,13 @@ import json from pathlib import Path +from unittest.mock import patch import pytest +from click.testing import CliRunner -from octop.cli.commands.run import resolve_bind +from octop.cli.commands.run import _save_configfile_overrides, resolve_bind, run +from octop.infra.errors import ErrorCode, OctopError @pytest.fixture @@ -55,3 +58,76 @@ def test_invalid_env_port_falls_back_to_file( def test_legacy_host_key(octop_home: Path) -> None: _write_config(octop_home, host="192.168.1.10", port=1234) assert resolve_bind(None, None) == ("192.168.1.10", 1234) + + +# --- issue #730: a corrupt config.json must never be treated as empty -------- + + +def _corrupt(text: str) -> str: + """Append a trailing comma before the closing brace (classic hand-edit slip).""" + stripped = text.rstrip() + assert stripped.endswith("}") + return stripped[:-1].rstrip() + ",}" + + +def test_corrupt_config_raises_instead_of_silently_using_defaults(octop_home: Path) -> None: + (octop_home / "config.json").write_text('{"bind_host": "0.0.0.0",}', encoding="utf-8") + with pytest.raises(OctopError) as excinfo: + resolve_bind(None, None) + assert excinfo.value.code is ErrorCode.CONFIG_FILE_CORRUPT + + +def test_save_overrides_on_corrupt_config_preserves_bytes(octop_home: Path) -> None: + cfg = octop_home / "config.json" + corrupt = _corrupt( + json.dumps({"bind_host": "0.0.0.0", "database": {"driver": "postgresql"}}, indent=2) + ) + cfg.write_text(corrupt, encoding="utf-8") + with pytest.raises(OctopError) as excinfo: + _save_configfile_overrides(None, 8088) + assert excinfo.value.code is ErrorCode.CONFIG_FILE_CORRUPT + assert cfg.read_text(encoding="utf-8") == corrupt + + +def test_save_overrides_merges_without_dropping_keys(octop_home: Path) -> None: + cfg = octop_home / "config.json" + cfg.write_text( + json.dumps( + { + "bind_host": "0.0.0.0", + "database": {"driver": "postgresql", "host": "db.internal"}, + "log_level": "debug", + } + ), + encoding="utf-8", + ) + _save_configfile_overrides(None, 8088) + data = json.loads(cfg.read_text(encoding="utf-8")) + assert data["port"] == 8088 + assert data["bind_host"] == "0.0.0.0" + assert data["database"] == {"driver": "postgresql", "host": "db.internal"} + assert data["log_level"] == "debug" + + +def test_run_cli_on_corrupt_config_refuses_and_preserves_file(octop_home: Path) -> None: + """POC regression: ``octop run --port`` used to wipe config.json to one key.""" + cfg = octop_home / "config.json" + cfg.write_text( + json.dumps( + { + "bind_host": "0.0.0.0", + "database": {"driver": "postgresql", "host": "db.internal"}, + }, + indent=2, + ), + encoding="utf-8", + ) + corrupt = _corrupt(cfg.read_text(encoding="utf-8")) + cfg.write_text(corrupt, encoding="utf-8") + + with patch("octop.cli.commands.run._run_uvicorn") as mocked: + result = CliRunner().invoke(run, ["--port", "8088"]) + + assert result.exit_code != 0 + assert not mocked.called + assert cfg.read_text(encoding="utf-8") == corrupt diff --git a/tests/unit/cli/test_run_cmd.py b/tests/unit/cli/test_run_cmd.py index 445e66b8..341b49bc 100644 --- a/tests/unit/cli/test_run_cmd.py +++ b/tests/unit/cli/test_run_cmd.py @@ -2,8 +2,10 @@ from __future__ import annotations +import contextlib import json from pathlib import Path +from typing import Any import pytest from click.testing import CliRunner @@ -182,10 +184,22 @@ def _fake(**_kw: object) -> None: assert data == {"theme": "dark", "bind_host": "0.0.0.0", "port": 80} -def test_run_handles_corrupt_config_without_crashing( +def _all_output(result: Any) -> str: + """stdout + stderr across click versions (8.2+ can separate the streams).""" + text = result.output or "" + with contextlib.suppress(ValueError, AttributeError): + text += result.stderr + return text + + +def test_run_reports_corrupt_config_cleanly( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - """A malformed config.json must not block the run command.""" + """A malformed config.json must fail cleanly, not with a traceback (#730). + + The command cannot guess the ~18 settings the file holds, and boot would die + in ``load_config`` anyway, so the CLI names the file and position and stops. + """ _patch_home(monkeypatch, tmp_path) (tmp_path / "config.json").write_text("{not valid json", encoding="utf-8") @@ -200,22 +214,28 @@ def _fake(**kw: object) -> None: runner = CliRunner() r = runner.invoke(cli, ["run", "--port", "80"]) - assert r.exit_code == 0, r.output - # Falls back to launch defaults for host, CLI for port. - assert captured["port"] == 80 - assert captured["host"] is None + assert r.exit_code == 1, _all_output(r) + # Never starts with the settings it could not read. + assert captured == {} + out = _all_output(r) + assert "Traceback" not in out + assert "not valid JSON" in out -def test_run_overwrites_corrupt_config_with_resolved_values( +def test_run_does_not_overwrite_corrupt_config( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - """A corrupt config.json is replaced with the resolved values on save. + """A corrupt config.json is preserved byte-for-byte, never rewritten as empty. - Documents the deliberate ``read-merge-write`` contract: a malformed file - is treated as empty, so the next run rewrites it cleanly. + Supersedes the old "treat a malformed file as empty so the next run rewrites + it cleanly" contract. That rewrite destroyed every other setting: one + trailing comma plus ``octop run --port`` wiped the ``database`` section and + silently flipped a PostgreSQL instance back to a greenfield SQLite one, with + no warning in the log (issue #730). """ _patch_home(monkeypatch, tmp_path) - (tmp_path / "config.json").write_text("{not valid json", encoding="utf-8") + corrupt = '{"bind_host": "0.0.0.0", "database": {"driver": "postgresql"},}' + (tmp_path / "config.json").write_text(corrupt, encoding="utf-8") def _fake(**_kw: object) -> None: return None @@ -226,11 +246,8 @@ def _fake(**_kw: object) -> None: runner = CliRunner() r = runner.invoke(cli, ["run", "--host", "0.0.0.0", "--port", "80"]) - assert r.exit_code == 0, r.output - - config_path = tmp_path / "config.json" - data = json.loads(config_path.read_text(encoding="utf-8")) - assert data == {"bind_host": "0.0.0.0", "port": 80} + assert r.exit_code == 1, _all_output(r) + assert (tmp_path / "config.json").read_text(encoding="utf-8") == corrupt def test_run_port_zero_not_overridden_by_config( diff --git a/tests/unit/i18n/test_errors.py b/tests/unit/i18n/test_errors.py index a66ff708..377a9848 100644 --- a/tests/unit/i18n/test_errors.py +++ b/tests/unit/i18n/test_errors.py @@ -94,3 +94,12 @@ def test_localized_message_falls_back_when_key_missing(monkeypatch: pytest.Monke lambda *_a, **_k: (_ for _ in ()).throw(KeyError("errors.X")), ) assert err.localized_message("zh") == "custom detail" + + +def test_config_file_corrupt_interpolates_path_and_detail(): + kwargs = {"path": "~/.octop/config.json", "detail": "line 3, column 1"} + assert "~/.octop/config.json" in error_message("CONFIG_FILE_CORRUPT", "en", **kwargs) + assert "line 3, column 1" in error_message("CONFIG_FILE_CORRUPT", "zh", **kwargs) + for locale in ("en", "zh"): + msg = error_message("CONFIG_FILE_CORRUPT", locale, **kwargs) + assert "{path}" not in msg and "{detail}" not in msg diff --git a/tests/unit/infra/setup/test_service.py b/tests/unit/infra/setup/test_service.py index 72129bee..ddcc33a5 100644 --- a/tests/unit/infra/setup/test_service.py +++ b/tests/unit/infra/setup/test_service.py @@ -12,6 +12,7 @@ import pytest +from octop.infra.errors import ErrorCode, OctopError from octop.infra.setup import service as service_mod from octop.infra.setup.service import ( ServiceRuntime, @@ -864,3 +865,15 @@ def access(self, _path: object, _mode: int) -> bool: def unlink(self, _path: object) -> None: # The tmp file is `delete=False`, so this is called in `finally`. return None + + +def test_persist_bind_options_refuses_corrupt_config(tmp_path: Path) -> None: + """issue #730: must not merge into an empty dict and wipe every other key.""" + home = tmp_path / "octop" + home.mkdir() + corrupt = '{"bind_host": "0.0.0.0", "database": {"driver": "postgresql"},}' + (home / "config.json").write_text(corrupt, encoding="utf-8") + with pytest.raises(OctopError) as excinfo: + service_mod.persist_bind_options(home, host="127.0.0.1", port=443) + assert excinfo.value.code is ErrorCode.CONFIG_FILE_CORRUPT + assert (home / "config.json").read_text(encoding="utf-8") == corrupt diff --git a/tests/unit/infra/utils/test_json_file.py b/tests/unit/infra/utils/test_json_file.py new file mode 100644 index 00000000..f4506c27 --- /dev/null +++ b/tests/unit/infra/utils/test_json_file.py @@ -0,0 +1,107 @@ +"""Unit tests for the shared JSON config helpers (issue #730).""" + +from __future__ import annotations + +import json +import os +import stat +from pathlib import Path + +import pytest + +from octop.infra.utils.json_file import ( + JsonFileCorruptError, + read_json_object, + write_json_atomic, +) + +posix_only = pytest.mark.skipif(os.name != "posix", reason="POSIX file modes only") + + +def test_read_absent_file_returns_none(tmp_path: Path) -> None: + assert read_json_object(tmp_path / "missing.json") is None + + +def test_read_valid_object(tmp_path: Path) -> None: + path = tmp_path / "config.json" + path.write_text(json.dumps({"bind_host": "0.0.0.0", "port": 8088}), encoding="utf-8") + assert read_json_object(path) == {"bind_host": "0.0.0.0", "port": 8088} + + +def test_read_corrupt_raises_with_position_and_never_leaks_contents(tmp_path: Path) -> None: + path = tmp_path / "config.json" + # Trailing comma — the classic hand-edit slip. The password must not surface. + path.write_text('{"database": {"password": "s3cret"},}', encoding="utf-8") + with pytest.raises(JsonFileCorruptError) as excinfo: + read_json_object(path) + err = excinfo.value + assert err.path == path + assert "line" in err.detail and "column" in err.detail + assert "s3cret" not in err.detail + assert "s3cret" not in str(err) + + +def test_read_non_object_raises(tmp_path: Path) -> None: + path = tmp_path / "config.json" + path.write_text("[1, 2]", encoding="utf-8") + with pytest.raises(JsonFileCorruptError) as excinfo: + read_json_object(path) + assert "list" in excinfo.value.detail + + +def test_write_is_pretty_newline_terminated_and_leaves_no_temp_files(tmp_path: Path) -> None: + path = tmp_path / "config.json" + write_json_atomic(path, {"a": 1}) + text = path.read_text(encoding="utf-8") + assert json.loads(text) == {"a": 1} + assert text.endswith("\n") + assert [p.name for p in tmp_path.iterdir()] == ["config.json"] + + +def test_write_creates_missing_parent(tmp_path: Path) -> None: + path = tmp_path / "nested" / "config.json" + write_json_atomic(path, {"a": 1}) + assert json.loads(path.read_text(encoding="utf-8")) == {"a": 1} + + +def test_failed_replace_leaves_original_intact_and_cleans_temp( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A torn write must not be observable: the old file survives byte-for-byte.""" + path = tmp_path / "config.json" + original = '{"bind_host": "0.0.0.0", "database": {"driver": "postgresql"}}' + path.write_text(original, encoding="utf-8") + + def boom(*_args: object, **_kwargs: object) -> None: + raise OSError("disk full") + + monkeypatch.setattr("octop.infra.utils.json_file.os.replace", boom) + with pytest.raises(OSError, match="disk full"): + write_json_atomic(path, {"port": 1}) + assert path.read_text(encoding="utf-8") == original + assert [p.name for p in tmp_path.iterdir()] == ["config.json"] + + +@posix_only +def test_write_preserves_existing_file_mode(tmp_path: Path) -> None: + """A config write must never silently change an existing file's mode.""" + path = tmp_path / "config.json" + path.write_text("{}", encoding="utf-8") + os.chmod(path, 0o640) + write_json_atomic(path, {"a": 1}) + assert stat.S_IMODE(path.stat().st_mode) == 0o640 + + +@posix_only +def test_write_new_file_uses_umask_default_not_mkstemp_0600(tmp_path: Path) -> None: + """mkstemp's 0600 must not leak into a new config.json. + + A root-created 0600 file would be unreadable to the service user after + ``octop service start --scope system``; the two pre-existing config.json + writers (``db/rebind.py``, ``backup/auto.py``) produce the umask default. + """ + mask = os.umask(0o022) + os.umask(mask) + path = tmp_path / "fresh.json" + write_json_atomic(path, {"a": 1}) + assert stat.S_IMODE(path.stat().st_mode) == 0o666 & ~mask diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index c5e8cda3..cb24af34 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -408,3 +408,25 @@ def test_loads_mobile_capabilities(tmp_path: Path) -> None: cfg = load_config(cfg_path) assert cfg.capabilities.mobile.enabled is True assert cfg.capabilities.mobile.backend == "physical" + + +def test_corrupt_config_names_file_and_position_without_leaking_contents(tmp_path: Path): + """issue #730: a bare JSONDecodeError never says *which* config file failed.""" + cfg_path = tmp_path / "config.json" + original = '{"database": {"password": "s3cret"},}' # trailing comma + cfg_path.write_text(original, encoding="utf-8") + with pytest.raises(ValueError, match=r"line \d+, column \d+") as excinfo: + load_config(cfg_path) + msg = str(excinfo.value) + assert str(cfg_path) in msg + assert "s3cret" not in msg + # the file is left exactly as the user wrote it + assert cfg_path.read_text(encoding="utf-8") == original + + +def test_non_object_config_raises(tmp_path: Path): + cfg_path = tmp_path / "config.json" + cfg_path.write_text("[1, 2]", encoding="utf-8") + with pytest.raises(ValueError, match="JSON object"): + load_config(cfg_path) + assert cfg_path.read_text(encoding="utf-8") == "[1, 2]" diff --git a/tests/unit/test_plugin_manager.py b/tests/unit/test_plugin_manager.py index 87bb2d2d..70a516de 100644 --- a/tests/unit/test_plugin_manager.py +++ b/tests/unit/test_plugin_manager.py @@ -289,3 +289,38 @@ def test_parse_plugin_icon(tmp_path: Path) -> None: ) assert parse_plugin_icon(plugin_dir) == "🧩" assert parse_plugin_icon(tmp_path / "missing") is None + + +def test_set_enabled_refuses_corrupt_config_and_preserves_bytes(tmp_path: Path) -> None: + """issue #730: the dashboard toggle must not wipe an unparseable config.json.""" + config_path = tmp_path / "config.json" + config_path.write_text("{}", encoding="utf-8") + mgr = PluginManager(plugins_dir=tmp_path / "plugins", config_path=config_path) + mgr.install_path(_FIXTURE, force=True) + + corrupt = '{"bind_host": "0.0.0.0", "database": {"driver": "postgresql"},}' + config_path.write_text(corrupt, encoding="utf-8") + with pytest.raises(OctopError) as excinfo: + mgr.set_enabled("echo-tool", False) + assert excinfo.value.code is ErrorCode.CONFIG_FILE_CORRUPT + assert config_path.read_text(encoding="utf-8") == corrupt + + +def test_set_enabled_preserves_unrelated_keys(tmp_path: Path) -> None: + config_path = tmp_path / "config.json" + config_path.write_text( + json.dumps( + { + "bind_host": "0.0.0.0", + "database": {"driver": "postgresql", "host": "db.internal"}, + } + ), + encoding="utf-8", + ) + mgr = PluginManager(plugins_dir=tmp_path / "plugins", config_path=config_path) + mgr.install_path(_FIXTURE, force=True) + mgr.set_enabled("echo-tool", False) + data = json.loads(config_path.read_text(encoding="utf-8")) + assert data["bind_host"] == "0.0.0.0" + assert data["database"] == {"driver": "postgresql", "host": "db.internal"} + assert data["plugins"]["echo-tool"]["enabled"] is False diff --git a/tests/unit/test_plugin_seed.py b/tests/unit/test_plugin_seed.py index e88dbea4..08468dae 100644 --- a/tests/unit/test_plugin_seed.py +++ b/tests/unit/test_plugin_seed.py @@ -11,6 +11,7 @@ from octop.infra.agents.plugins.manager import PluginManager from octop.infra.agents.plugins.seed import seed_bundled_plugins +from octop.infra.errors import ErrorCode, OctopError def _write_plugin(root: Path, plugin_id: str, *, version: str = "0.1.0") -> Path: @@ -172,3 +173,20 @@ def test_load_installed_clears_stale_registry_entries(tmp_path: Path) -> None: assert [item.manifest.id for item in loaded] == ["weather"] assert PluginRegistry().get("stale") is None assert PluginRegistry().get("weather") is not None + + +def test_seed_refuses_corrupt_config_and_preserves_bytes(tmp_path: Path) -> None: + """issue #730: seeding always writes back, so a corrupt file must not read as ``{}``.""" + bundled = tmp_path / "bundled" + _write_plugin(bundled, "weather") + config_path = tmp_path / "config.json" + original = '{\n "bind_host": "0.0.0.0",\n "database": {"driver": "postgresql"}\n,}' + config_path.write_text(original, encoding="utf-8") + with pytest.raises(OctopError) as excinfo: + seed_bundled_plugins( + bundled_root=bundled, + plugins_dir=tmp_path / "plugins", + config_path=config_path, + ) + assert excinfo.value.code is ErrorCode.CONFIG_FILE_CORRUPT + assert config_path.read_text(encoding="utf-8") == original From 6acc1777d74c816bc0d3ee963022e741760bda4d Mon Sep 17 00:00:00 2001 From: XYAIStudio Date: Wed, 7 Oct 2026 23:06:57 +0800 Subject: [PATCH 04/14] chore(release): prepare FreeOS 0.1.0 --- .github/workflows/release.yml | 34 +- CHANGELOG.md | 16 + CONTRIBUTING.md | 2 +- Makefile | 2 +- dashboard/package-lock.json | 1481 ++++------------- dashboard/package.json | 15 +- .../pages/employees/EmployeesPage.test.tsx | 3 + .../pages/workspace/WorkspacePage.test.tsx | 2 +- .../src/pages/Admin/Users/SsoPanel.test.tsx | 10 +- .../Agent/Memory/ProfileOverview.test.tsx | 2 +- .../Chat/components/ChatDockReviewContent.tsx | 4 +- .../Chat/components/ChatDockStartState.tsx | 8 +- .../Chat/components/ChatDockTasksContent.tsx | 4 +- .../pages/Chat/components/ChatPanelPicker.tsx | 5 +- .../components/TrajectoryInspector.test.tsx | 21 +- .../src/pages/Chat/hooks/useChatDockPanel.ts | 15 +- .../src/pages/Chat/utils/chatMessages.test.ts | 14 +- .../src/pages/Chat/utils/expertMention.ts | 4 +- .../components/modals/CustomProviderModal.tsx | 4 +- dashboard/src/test/setup.ts | 2 + docs/cli.md | 9 +- docs/node-runtime.md | 10 +- docs/node-runtime.zh-CN.md | 8 +- docs/user-guide.md | 4 +- pyproject.toml | 2 +- scripts/README.md | 15 +- scripts/install-octop.sh | 16 +- scripts/install.bat | 14 +- scripts/install.ps1 | 11 +- scripts/install.sh | 16 +- src/octop/__init__.py | 2 +- src/octop/cli/registry.py | 4 +- src/octop/infra/agents/manager.py | 4 + .../middleware/system_message_cleanup.py | 34 + .../unit/agents/test_octop_builtin_skills.py | 8 +- .../agents/test_system_message_cleanup.py | 62 + tests/unit/cli/test_completion_cmd.py | 4 +- tests/unit/db/test_clip_thread_title.py | 2 +- tests/unit/db/test_db_pool.py | 20 +- tests/unit/db/test_published_experts_repo.py | 6 +- tests/unit/db/test_repo_knowledge.py | 2 +- tests/unit/db/test_skill_package_icons.py | 10 +- tests/unit/db/test_skill_packages_repo.py | 6 +- uv.lock | 2 +- 44 files changed, 643 insertions(+), 1276 deletions(-) create mode 100644 src/octop/infra/agents/middleware/system_message_cleanup.py create mode 100644 tests/unit/agents/test_system_message_cleanup.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf626dad..199093df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ name: Release -# 版本标签 v* → 测包、发 PyPI、建 GitHub Release,再级联: +# 版本标签 v* → 测包、构建 GitHub Release 制品,再级联: # - Sync Main Into Develop # - Build Octop FPK(复用本 Release 的 wheel;等待 docker-publish 的 GHCR 镜像) # @@ -18,7 +18,6 @@ on: permissions: contents: write actions: write - id-token: write concurrency: group: release-${{ github.ref }} @@ -43,6 +42,11 @@ jobs: echo "Refusing release: tag $GITHUB_REF_NAME does not match pyproject.toml $package_version" >&2 exit 1 fi + git fetch origin main + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then + echo "Refusing release: production tags must point to a commit on main" >&2 + exit 1 + fi - uses: astral-sh/setup-uv@v4 with: @@ -73,26 +77,9 @@ jobs: name: dist path: dist/ - publish: - name: Publish to PyPI - needs: build - runs-on: ubuntu-latest - environment: pypi - steps: - - name: Download distributions - uses: actions/download-artifact@v4 - with: - name: dist - path: dist/ - - - name: Publish package - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} - github-release: name: Create GitHub Release - needs: publish + needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -128,8 +115,9 @@ jobs: echo "" echo "## Install" echo "" + echo "Download the Python wheel from this GitHub Release and install it with:" echo '```bash' - echo "pip install octop==$version" + echo "uv pip install ./octop-$version-py3-none-any.whl" echo '```' } > .release-notes.md fi @@ -165,7 +153,7 @@ jobs: -f version="${{ github.ref_name }}" # FnOS FPK:复用本 Release 的 wheel + docker-publish 的 GHCR 镜像。 - # 与 sync-develop 并行。Dispatch 失败必须让本 workflow 变红(PyPI 已发布仍如此)。 + # 与 sync-develop 并行。Dispatch 失败必须让本 workflow 变红,以便发现下游构建漏触发。 trigger-fnos: name: Trigger FnOS FPK build needs: github-release @@ -175,7 +163,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # --ref 用版本 tag,保证 FPK 打在与 PyPI / 镜像相同的 commit 上 + # --ref 用版本 tag,保证 FPK 打在与 GitHub Release / 镜像相同的 commit 上 gh workflow run fnos-build-fpk.yml \ --repo "${{ github.repository }}" \ --ref "${{ github.ref_name }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e96071..15367e0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,27 @@ ## [Unreleased] +### Changed + +- Publish Python distributions as GitHub Release assets only; the FreeOS release workflow no longer publishes the colliding `octop` package name to PyPI, and install scripts default to the FreeOS GitHub source. + ### Fixed +- Remove checkpoint-resident system messages before model calls while preserving the active compiled prompt, preventing strict Qwen chat templates from failing when an older conversation contains a stale bootstrap message. - Detect models from an already-running Ollama daemon even when FreeOS-managed Ollama startup is disabled; model discovery never starts Ollama unless the user enabled it. - Preserve `config.json` when it is malformed: startup and all configuration writers now report the file and parser position, avoid leaking stored credentials, and use atomic replacement instead of silently rebuilding an empty configuration. +## [0.1.0] - 2026-10-07 + +### Changed + +- Publish the successor to 0.0.10 as 0.1.0, with Python distributions attached to GitHub Releases only. + +### Fixed + +- Remove stale checkpoint system messages before model calls, detect models from an already-running Ollama daemon without starting it, and preserve malformed `config.json` files instead of silently replacing them. +- Improve desktop update safety by checkpointing SQLite WAL before restart. + ## [0.0.10] - 2026-10-03 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d1cd7ad..cba35a82 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,7 +73,7 @@ See [AGENTS.md](AGENTS.md) for module boundaries and coding conventions. 1. Cut `release/x.y.z` from latest `develop` (version bump + CHANGELOG on that branch) 2. Open PR: `release/x.y.z` → `main` and merge when green -3. Tag `v` on **main tip** and push — GitHub Actions builds, publishes to PyPI, and creates the GitHub Release +3. Tag `v` on **main tip** and push — GitHub Actions builds Python distributions and creates a GitHub Release with the artifacts; FreeOS does not publish the `octop` distribution to PyPI 4. Delete `release/x.y.z`; Actions syncs `main` → `develop` (or opens `chore/sync-develop-after-*` if merge conflicts / branch protection) Agent-assisted publish: `.cursor/skills/publish` (`/publish `). diff --git a/Makefile b/Makefile index cdf862e8..3b73053c 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ RUN := $(if $(UV),uv run,) PYTHON := $(if $(UV),uv run python,python3) PIP := $(if $(UV),uv pip,python3 -m pip) # Parallel workers for make test / make test-fast (override: make test PYTEST_JOBS=4) -PYTEST_JOBS ?= auto +PYTEST_JOBS ?= 4 # ─── Help ──────────────────────────────────────────────────────────────────── diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index ff1fc723..9ef4a83a 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -17,12 +17,11 @@ "ahooks": "^3.9.6", "antd": "^5.29.1", "antd-style": "^3.7.1", - "build": "^0.1.4", "docx-preview": "^0.3.2", "i18next": "^25.8.4", "jszip": "^3.10.1", "lucide-react": "^0.562.0", - "mermaid": "^11.13.0", + "mermaid": "^12.1.0", "qrcode.react": "^4.2.0", "react": "^18", "react-dom": "^18", @@ -30,14 +29,14 @@ "react-json-view-lite": "^2.5.0", "react-markdown": "^10.1.0", "react-pdf": "^10.5.0", - "react-router-dom": "^7.13.0", + "react-router-dom": "^7.18.4", "react-syntax-highlighter": "^16.1.1", "react-virtuoso": "^4.18.10", "recharts": "^3.10.1", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", - "xlsx": "^0.18.5" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" }, "devDependencies": { "@eslint/js": "^9.25.0", @@ -51,7 +50,7 @@ "@types/react-dom": "^18", "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^4.4.1", - "@vitest/coverage-v8": "^3.2.6", + "@vitest/coverage-v8": "^4.1.11", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", @@ -65,7 +64,7 @@ "typescript-eslint": "^8.30.1", "vite": "^6.3.5", "vite-plugin-pwa": "^0.21.2", - "vitest": "^3.2.6" + "vitest": "^4.1.11" } }, "node_modules/@adobe/css-tools": { @@ -93,20 +92,6 @@ } } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@ant-design/colors": { "version": "7.2.1", "resolved": "https://mirrors.tencent.com/npm/@ant-design/colors/-/colors-7.2.1.tgz", @@ -1807,20 +1792,54 @@ "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", "license": "MIT" }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-13.2.0.tgz", + "integrity": "sha512-bA9dvWlhAUcHkNwU+8w9yiUxJPlj/Lm3VqS7MD+NbIR/7LmpNLuK/AWNFdCo/T3SK5ND0/UHf3IJvl/Jn/i6yg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "13.2.0", + "@chevrotain/types": "13.2.0" + } + }, + "node_modules/@chevrotain/cst-dts-gen/node_modules/@chevrotain/types": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-13.2.0.tgz", + "integrity": "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/gast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-13.2.0.tgz", + "integrity": "sha512-bCARdAsG+eEbOveSXcOAC7pVxA04I1ionzeMOwIYf5NX7SaS7zgn4Dgug2BSqoL+iyKeMNSorQjsFSHJHsCNwg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "13.2.0" + } + }, + "node_modules/@chevrotain/gast/node_modules/@chevrotain/types": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-13.2.0.tgz", + "integrity": "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-13.2.0.tgz", + "integrity": "sha512-kxpUkLtiaK09ph+dib+Yvm+3sWSnuwlo25gqfKRgTPNmF6H7uW4gVkMjaRdtnN783S9sM6vUNWh9CsxYX4QwKw==", + "license": "Apache-2.0" + }, "node_modules/@chevrotain/types": { "version": "11.1.2", "resolved": "https://mirrors.tencent.com/npm/@chevrotain/types/-/types-11.1.2.tgz", "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", "license": "Apache-2.0" }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://mirrors.tencent.com/npm/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } + "node_modules/@chevrotain/utils": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-13.2.0.tgz", + "integrity": "sha512-SzKYY68rseOrb9KILVzPsl6SjkQtdWw01SRGB5aCVI5kFcHltQsOYV3JB0Ffst72DgkvHCyDQ0NLwxtmCCf97Q==", + "license": "Apache-2.0" }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", @@ -1939,17 +1958,6 @@ "node": ">=18" } }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.8", - "resolved": "https://mirrors.tencent.com/npm/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", - "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", - "license": "MIT", - "dependencies": { - "@so-ric/colorspace": "^1.1.6", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://mirrors.tencent.com/npm/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -2798,16 +2806,6 @@ "node": ">=18" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://mirrors.tencent.com/npm/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3146,17 +3144,6 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@rc-component/async-validator": { "version": "5.1.0", "resolved": "https://mirrors.tencent.com/npm/@rc-component/async-validator/-/async-validator-5.1.0.tgz", @@ -3805,16 +3792,6 @@ "win32" ] }, - "node_modules/@so-ric/colorspace": { - "version": "1.1.6", - "resolved": "https://mirrors.tencent.com/npm/@so-ric/colorspace/-/colorspace-1.1.6.tgz", - "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", - "license": "MIT", - "dependencies": { - "color": "^5.0.2", - "text-hex": "1.0.x" - } - }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://mirrors.tencent.com/npm/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -4409,12 +4386,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://mirrors.tencent.com/npm/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://mirrors.tencent.com/npm/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -4631,15 +4602,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://mirrors.tencent.com/npm/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -4761,32 +4733,30 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.6.tgz", - "integrity": "sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", - "ast-v8-to-istanbul": "^0.3.3", - "debug": "^4.4.1", + "@vitest/utils": "4.1.11", + "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.2.6", - "vitest": "3.2.6" + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -4795,39 +4765,40 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", - "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.6", - "@vitest/utils": "3.2.6", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", - "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.6", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -4849,42 +4820,42 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", - "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", - "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.6", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", - "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.6", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { @@ -4892,28 +4863,25 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", - "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", - "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.6", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -4967,15 +4935,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/adler-32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", - "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -5207,9 +5166,9 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", - "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.7.tgz", + "integrity": "sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==", "dev": true, "license": "MIT", "dependencies": { @@ -5238,7 +5197,8 @@ "node_modules/async": { "version": "3.2.6", "resolved": "https://mirrors.tencent.com/npm/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true }, "node_modules/async-function": { "version": "1.0.0", @@ -5355,9 +5315,9 @@ "dev": true }, "node_modules/baseline-browser-mapping": { - "version": "2.10.33", - "resolved": "https://mirrors.tencent.com/npm/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", - "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5368,19 +5328,20 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://mirrors.tencent.com/npm/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://mirrors.tencent.com/npm/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", "dev": true, "funding": [ { @@ -5396,13 +5357,14 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -5417,36 +5379,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "node_modules/build": { - "version": "0.1.4", - "resolved": "https://mirrors.tencent.com/npm/build/-/build-0.1.4.tgz", - "integrity": "sha512-KwbDJ/zrsU8KZRRMfoURG14cKIAStUlS8D5jBDvtrZbwO5FEkYqc3oB8HIhRiyD64A48w1lc+sOmQ+mmBw5U/Q==", - "dependencies": { - "cssmin": "0.3.x", - "jsmin": "1.x", - "jxLoader": "*", - "moo-server": "*", - "promised-io": "*", - "timespan": "2.x", - "uglify-js": "1.x", - "walker": "1.x", - "winston": "*", - "wrench": "1.3.x" - }, - "engines": { - "node": ">v0.4.12" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://mirrors.tencent.com/npm/call-bind/-/call-bind-1.0.9.tgz", @@ -5505,9 +5437,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://mirrors.tencent.com/npm/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", "dev": true, "funding": [ { @@ -5535,32 +5467,12 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/cfb": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", - "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", - "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "crc-32": "~1.2.0" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -5618,16 +5530,28 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", + "node_modules/chevrotain": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-13.2.0.tgz", + "integrity": "sha512-GTouk5YzgBxlTPCaDn5q0q2EJKkrYmnPF+HmF8v0Qo4r3iyPG7HJivkN1wthCXZcm3sIcZFpGTRovLPNdJNRCA==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "13.2.0", + "@chevrotain/gast": "13.2.0", + "@chevrotain/regexp-to-ast": "13.2.0", + "@chevrotain/types": "13.2.0", + "@chevrotain/utils": "13.2.0" + }, "engines": { - "node": ">= 16" + "node": ">=22.0.0" } }, + "node_modules/chevrotain/node_modules/@chevrotain/types": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-13.2.0.tgz", + "integrity": "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg==", + "license": "Apache-2.0" + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://mirrors.tencent.com/npm/classnames/-/classnames-2.5.1.tgz", @@ -5657,27 +5581,6 @@ "node": ">=6" } }, - "node_modules/codepage": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", - "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/color": { - "version": "5.0.3", - "resolved": "https://mirrors.tencent.com/npm/color/-/color-5.0.3.tgz", - "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", - "dependencies": { - "color-convert": "^3.1.3", - "color-string": "^2.1.3" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://mirrors.tencent.com/npm/color-convert/-/color-convert-2.0.1.tgz", @@ -5696,44 +5599,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/color-string": { - "version": "2.1.4", - "resolved": "https://mirrors.tencent.com/npm/color-string/-/color-string-2.1.4.tgz", - "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-string/node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://mirrors.tencent.com/npm/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "3.1.3", - "resolved": "https://mirrors.tencent.com/npm/color-convert/-/color-convert-3.1.3.tgz", - "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=14.6" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://mirrors.tencent.com/npm/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", - "engines": { - "node": ">=12.20" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5796,7 +5661,7 @@ }, "node_modules/cookie": { "version": "1.1.1", - "resolved": "https://mirrors.tencent.com/npm/cookie/-/cookie-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { @@ -5877,18 +5742,6 @@ "node": ">= 6" } }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://mirrors.tencent.com/npm/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5920,14 +5773,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cssmin": { - "version": "0.3.2", - "resolved": "https://mirrors.tencent.com/npm/cssmin/-/cssmin-0.3.2.tgz", - "integrity": "sha512-bynxGIAJ8ybrnFobjsQotIjA8HFDDgPwbeUWNXXXfR+B4f9kkxdcUyagJoQCSUOfMV+ZZ6bMn8bvbozlCzUGwQ==", - "bin": { - "cssmin": "bin/cssmin" - } - }, "node_modules/cssstyle": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", @@ -5956,9 +5801,9 @@ "license": "MIT" }, "node_modules/cytoscape": { - "version": "3.33.4", - "resolved": "https://mirrors.tencent.com/npm/cytoscape/-/cytoscape-3.33.4.tgz", - "integrity": "sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww==", + "version": "3.34.3", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.3.tgz", + "integrity": "sha512-yfYGhRcGAntq6YBD583j4n0Eg3jIxvWmZtz/5uz9UYkeIStSlMxuUja+ec5j3iBD8nv1rwaOAYMW09tBdkSeaQ==", "license": "MIT", "peer": true, "engines": { @@ -6604,16 +6449,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://mirrors.tencent.com/npm/deep-is/-/deep-is-0.1.4.tgz", @@ -6733,9 +6568,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.7", - "resolved": "https://mirrors.tencent.com/npm/dompurify/-/dompurify-3.4.7.tgz", - "integrity": "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==", + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", + "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -6756,13 +6591,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/echarts": { "version": "6.1.0", "resolved": "https://mirrors.tencent.com/npm/echarts/-/echarts-6.1.0.tgz", @@ -6795,10 +6623,17 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.364", - "resolved": "https://mirrors.tencent.com/npm/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", - "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", - "dev": true + "version": "1.5.445", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.445.tgz", + "integrity": "sha512-iX7o1nF+qNb3Bv5mLYgqfgF9Znh9e/8MKXdmbb8Gv2Q47OKIzTwEPD1oda1HrgEwYatTLJj0wQjHvIFt8Db53Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "license": "EPL-2.0" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -6806,12 +6641,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://mirrors.tencent.com/npm/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT" - }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://mirrors.tencent.com/npm/entities/-/entities-6.0.1.tgz", @@ -6935,9 +6764,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -7300,9 +7129,9 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://mirrors.tencent.com/npm/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { @@ -7313,7 +7142,8 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/fault": { "version": "1.0.4", @@ -7346,12 +7176,6 @@ } } }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://mirrors.tencent.com/npm/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://mirrors.tencent.com/npm/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -7375,10 +7199,11 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://mirrors.tencent.com/npm/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -7436,12 +7261,6 @@ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://mirrors.tencent.com/npm/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://mirrors.tencent.com/npm/for-each/-/for-each-0.3.5.tgz", @@ -7500,15 +7319,6 @@ "node": ">=0.4.x" } }, - "node_modules/frac": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", - "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://mirrors.tencent.com/npm/fs-extra/-/fs-extra-9.1.0.tgz", @@ -7717,15 +7527,16 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://mirrors.tencent.com/npm/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/glob/node_modules/minimatch": { @@ -8730,6 +8541,7 @@ "version": "2.0.1", "resolved": "https://mirrors.tencent.com/npm/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8924,21 +8736,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -8999,9 +8796,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://mirrors.tencent.com/npm/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -9110,18 +8907,6 @@ "node": ">=6" } }, - "node_modules/jsmin": { - "version": "1.0.1", - "resolved": "https://mirrors.tencent.com/npm/jsmin/-/jsmin-1.0.1.tgz", - "integrity": "sha512-OPuL5X/bFKgVdMvEIX3hnpx3jbVpFCrEM8pKPXjFkZUqg521r41ijdyTz7vACOhW6o1neVlcLyd+wkbK5fNHRg==", - "license": "Doug Crockford's license that allows this module to be used for Good but not for Evil", - "bin": { - "jsmin": "bin/jsmin" - }, - "engines": { - "node": ">=0.1.93" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://mirrors.tencent.com/npm/json-buffer/-/json-buffer-3.0.1.tgz", @@ -9201,51 +8986,29 @@ "setimmediate": "^1.0.5" } }, - "node_modules/jxLoader": { - "version": "0.1.1", - "resolved": "https://mirrors.tencent.com/npm/jxLoader/-/jxLoader-0.1.1.tgz", - "integrity": "sha512-ClEvAj3K68y8uKhub3RgTmcRPo5DfIWvtxqrKQdDPyZ1UVHIIKvVvjrAsJFSVL5wjv0rt5iH9SMCZ0XRKNzeUA==", - "dependencies": { - "js-yaml": "0.3.x", - "moo-server": "1.3.x", - "promised-io": "*", - "walker": "1.x" - }, - "engines": { - "node": ">v0.4.10" - } - }, - "node_modules/jxLoader/node_modules/js-yaml": { - "version": "0.3.7", - "resolved": "https://mirrors.tencent.com/npm/js-yaml/-/js-yaml-0.3.7.tgz", - "integrity": "sha512-/7PsVDNP2tVe2Z1cF9kTEkjamIwz4aooDpRKmN1+g/9eePCgcxsv4QDvEbxO0EH+gdDD7MLyDoR6BASo3hH51g==", - "engines": { - "node": "> 0.4.11" - } - }, "node_modules/katex": { - "version": "0.16.47", - "resolved": "https://mirrors.tencent.com/npm/katex/-/katex-0.16.47.tgz", - "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.19.0.tgz", + "integrity": "sha512-v6Tznz3zJ7u3niRCoDTsumM2+HA2XXcCu+WAacCeHD2z3p9A9Ks987o5FzfTGBN0e8A0vjEgIDvLbICrXpdw/Q==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], "license": "MIT", "dependencies": { - "commander": "^8.3.0" + "commander": "^15.0.0" }, "bin": { "katex": "cli.js" } }, "node_modules/katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://mirrors.tencent.com/npm/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=22.12.0" } }, "node_modules/keyv": { @@ -9262,12 +9025,6 @@ "resolved": "https://mirrors.tencent.com/npm/khroma/-/khroma-2.1.0.tgz", "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://mirrors.tencent.com/npm/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, "node_modules/layout-base": { "version": "1.0.2", "resolved": "https://mirrors.tencent.com/npm/layout-base/-/layout-base-1.0.2.tgz", @@ -9393,23 +9150,6 @@ "dev": true, "license": "MIT" }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://mirrors.tencent.com/npm/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "license": "MIT", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://mirrors.tencent.com/npm/longest-streak/-/longest-streak-3.1.0.tgz", @@ -9431,13 +9171,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lowlight": { "version": "1.20.0", "resolved": "https://mirrors.tencent.com/npm/lowlight/-/lowlight-1.20.0.tgz", @@ -9491,15 +9224,15 @@ } }, "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" } }, "node_modules/make-cancellable-promise": { @@ -9545,14 +9278,6 @@ "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" } }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://mirrors.tencent.com/npm/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dependencies": { - "tmpl": "1.0.5" - } - }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://mirrors.tencent.com/npm/markdown-table/-/markdown-table-3.0.4.tgz", @@ -9902,39 +9627,63 @@ } }, "node_modules/mermaid": { - "version": "11.15.0", - "resolved": "https://mirrors.tencent.com/npm/mermaid/-/mermaid-11.15.0.tgz", - "integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-12.1.0.tgz", + "integrity": "sha512-wlVCp+8eTupfCeeFvoZNNiTuHrvag0P2jz/ILgb/f/6jkVokUefOcujefi8qUe/j2asHiSePncVsz/xzzA80LQ==", + "license": "MIT", "dependencies": { - "@braintree/sanitize-url": "^7.1.1", + "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", - "@mermaid-js/parser": "^1.1.1", + "@mermaid-js/parser": "^2.0.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", - "cytoscape": "^3.33.1", + "chevrotain": "~13.2.0", + "cytoscape": "^3.34.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", - "dayjs": "^1.11.19", - "dompurify": "^3.3.1", + "dayjs": "^1.11.21", + "dompurify": "^3.4.12", + "elkjs": "^0.9.3", "es-toolkit": "^1.45.1", - "katex": "^0.16.25", + "katex": "^0.16.47", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + }, + "engines": { + "node": ">=22.12.0" } }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://mirrors.tencent.com/npm/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { + "node_modules/mermaid/node_modules/@chevrotain/types": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-13.2.0.tgz", + "integrity": "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg==", + "license": "Apache-2.0" + }, + "node_modules/mermaid/node_modules/@mermaid-js/parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-2.0.1.tgz", + "integrity": "sha512-j37dj/h6p6pots1L7pLPPNuABEEu9WP4OYg9pW1g+nYG331j0+3canE9OagtYdytYvdSAMG+2fKU5cxep6LgjA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~13.2.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://mirrors.tencent.com/npm/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, @@ -10579,24 +10328,15 @@ } }, "node_modules/monaco-editor": { - "version": "0.55.1", - "resolved": "https://mirrors.tencent.com/npm/monaco-editor/-/monaco-editor-0.55.1.tgz", - "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.57.0.tgz", + "integrity": "sha512-5BkI9KGoqrNvBGUe15/QlZq3OooZ8WLg1AxTpaqHRCP3HNpzPPZKE2EDz8M7c+VRmCeUw1Brp4cx/PWm3kI/5A==", "license": "MIT", "dependencies": { - "dompurify": "3.2.7", + "dompurify": "3.4.15", "marked": "14.0.0" } }, - "node_modules/monaco-editor/node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://mirrors.tencent.com/npm/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, "node_modules/monaco-editor/node_modules/marked": { "version": "14.0.0", "resolved": "https://mirrors.tencent.com/npm/marked/-/marked-14.0.0.tgz", @@ -10608,23 +10348,15 @@ "node": ">= 18" } }, - "node_modules/moo-server": { - "version": "1.3.0", - "resolved": "https://mirrors.tencent.com/npm/moo-server/-/moo-server-1.3.0.tgz", - "integrity": "sha512-9A8/eor2DXwpv1+a4pZAAydqLFVrWoKoO1fzdzqLUhYVXAO1Kgd1FR2gFZi7YdHzF0s4W8cDNwCfKJQrvLqxDw==", - "engines": { - "node": ">v0.4.10" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://mirrors.tencent.com/npm/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://mirrors.tencent.com/npm/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "dev": true, "funding": [ { @@ -10665,10 +10397,11 @@ } }, "node_modules/node-releases": { - "version": "2.0.46", - "resolved": "https://mirrors.tencent.com/npm/node-releases/-/node-releases-2.0.46.tgz", - "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -10721,13 +10454,18 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://mirrors.tencent.com/npm/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", - "dependencies": { - "fn.name": "1.x.x" + "engines": { + "node": ">=12.20.0" } }, "node_modules/open": { @@ -10978,16 +10716,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/pdfjs-dist": { "version": "5.4.296", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", @@ -11067,9 +10795,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://mirrors.tencent.com/npm/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", "dev": true, "funding": [ { @@ -11085,10 +10813,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.19", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -11181,11 +10910,6 @@ "resolved": "https://mirrors.tencent.com/npm/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "node_modules/promised-io": { - "version": "0.3.6", - "resolved": "https://mirrors.tencent.com/npm/promised-io/-/promised-io-0.3.6.tgz", - "integrity": "sha512-bNwZusuNIW4m0SPR8jooSyndD35ggirHlxVl/UhIaZD/F0OBv9ebfc6tNmbpZts3QXHggkjIBH8lvtnzhtcz0A==" - }, "node_modules/property-information": { "version": "7.2.0", "resolved": "https://mirrors.tencent.com/npm/property-information/-/property-information-7.2.0.tgz", @@ -12011,9 +11735,9 @@ } }, "node_modules/react-router": { - "version": "7.16.0", - "resolved": "https://mirrors.tencent.com/npm/react-router/-/react-router-7.16.0.tgz", - "integrity": "sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", + "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -12033,12 +11757,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.16.0", - "resolved": "https://mirrors.tencent.com/npm/react-router-dom/-/react-router-dom-7.16.0.tgz", - "integrity": "sha512-kMUAbimWB5FVbF4Bce4bJsiKJWLIUHq/mEG8+CFDnCSgltptBiG5nguducmsJeGKytlCvQud9Qhzpn49iduTlA==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz", + "integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==", "license": "MIT", "dependencies": { - "react-router": "7.16.0" + "react-router": "7.18.4" }, "engines": { "node": ">=20.0.0" @@ -12620,15 +12344,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://mirrors.tencent.com/npm/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://mirrors.tencent.com/npm/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -12702,7 +12417,7 @@ }, "node_modules/set-cookie-parser": { "version": "2.7.2", - "resolved": "https://mirrors.tencent.com/npm/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, @@ -12896,10 +12611,11 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://mirrors.tencent.com/npm/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -12935,27 +12651,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/ssf": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", - "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", - "license": "Apache-2.0", - "dependencies": { - "frac": "~1.1.2" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://mirrors.tencent.com/npm/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -12970,9 +12665,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "dev": true, "license": "MIT" }, @@ -13020,22 +12715,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://mirrors.tencent.com/npm/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -13162,20 +12841,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-comments": { "version": "2.0.1", "resolved": "https://mirrors.tencent.com/npm/strip-comments/-/strip-comments-2.0.1.tgz", @@ -13211,26 +12876,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://mirrors.tencent.com/npm/style-to-js/-/style-to-js-1.1.21.tgz", @@ -13341,264 +12986,6 @@ "dev": true, "license": "MIT" }, - "node_modules/test-exclude": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", - "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^10.2.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/test-exclude/node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/test-exclude/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/test-exclude/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/test-exclude/node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/test-exclude/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/test-exclude/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/test-exclude/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://mirrors.tencent.com/npm/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, "node_modules/throttle-debounce": { "version": "5.0.2", "resolved": "https://mirrors.tencent.com/npm/throttle-debounce/-/throttle-debounce-5.0.2.tgz", @@ -13608,14 +12995,6 @@ "node": ">=12.22" } }, - "node_modules/timespan": { - "version": "2.3.0", - "resolved": "https://mirrors.tencent.com/npm/timespan/-/timespan-2.3.0.tgz", - "integrity": "sha512-0Jq9+58T2wbOyLth0EU+AUb6JMGCLaTWIykJFa7hyAybjVH9gpVMTfUAwo5fWAvtFt2Tjh/Elg8JtgNpnMnM8g==", - "engines": { - "node": ">= 0.2.0" - } - }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://mirrors.tencent.com/npm/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -13655,30 +13034,10 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", "dev": true, "license": "MIT", "engines": { @@ -13705,11 +13064,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://mirrors.tencent.com/npm/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" - }, "node_modules/toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", @@ -13748,14 +13102,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://mirrors.tencent.com/npm/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "engines": { - "node": ">= 14.0.0" - } - }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://mirrors.tencent.com/npm/trough/-/trough-2.2.0.tgz", @@ -13932,14 +13278,6 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/uglify-js": { - "version": "1.3.5", - "resolved": "https://mirrors.tencent.com/npm/uglify-js/-/uglify-js-1.3.5.tgz", - "integrity": "sha512-YPX1DjKtom8l9XslmPFQnqWzTBkvI4N0pbkzLuPZZ4QTyig0uQqvZz9NgUdfEV+qccJzi7fVcGWdESvRIjWptQ==", - "bin": { - "uglifyjs": "bin/uglifyjs" - } - }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://mirrors.tencent.com/npm/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -14157,9 +13495,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://mirrors.tencent.com/npm/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "dev": true, "funding": [ { @@ -14373,29 +13711,6 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vite-plugin-pwa": { "version": "0.21.2", "resolved": "https://mirrors.tencent.com/npm/vite-plugin-pwa/-/vite-plugin-pwa-0.21.2.tgz", @@ -14428,66 +13743,80 @@ } }, "node_modules/vitest": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", - "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.6", - "@vitest/mocker": "3.2.6", - "@vitest/pretty-format": "^3.2.6", - "@vitest/runner": "3.2.6", - "@vitest/snapshot": "3.2.6", - "@vitest/spy": "3.2.6", - "@vitest/utils": "3.2.6", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.6", - "@vitest/ui": "3.2.6", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -14498,16 +13827,12 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/vitest/node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://mirrors.tencent.com/npm/void-elements/-/void-elements-3.1.0.tgz", @@ -14530,14 +13855,6 @@ "node": ">=18" } }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://mirrors.tencent.com/npm/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dependencies": { - "makeerror": "1.0.12" - } - }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -14727,85 +14044,6 @@ "node": ">=8" } }, - "node_modules/winston": { - "version": "3.19.0", - "resolved": "https://mirrors.tencent.com/npm/winston/-/winston-3.19.0.tgz", - "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.8", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://mirrors.tencent.com/npm/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://mirrors.tencent.com/npm/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/winston/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://mirrors.tencent.com/npm/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/wmf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", - "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/word": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", - "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://mirrors.tencent.com/npm/word-wrap/-/word-wrap-1.2.5.tgz", @@ -15107,34 +14345,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrench": { - "version": "1.3.9", - "resolved": "https://mirrors.tencent.com/npm/wrench/-/wrench-1.3.9.tgz", - "integrity": "sha512-srTJQmLTP5YtW+F5zDuqjMEZqLLr/eJOZfDI5ibfPfRMeDh3oBUefAscuH0q5wBKE339ptH/S/0D18ZkfOfmKQ==", - "deprecated": "wrench.js is deprecated! You should check out fs-extra (https://github.com/jprichardson/node-fs-extra) for any operations you were using wrench for. Thanks for all the usage over the years.", - "engines": { - "node": ">=0.1.97" - } - }, "node_modules/ws": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", @@ -15158,19 +14368,10 @@ } }, "node_modules/xlsx": { - "version": "0.18.5", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", - "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "version": "0.20.3", + "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "cfb": "~1.2.1", - "codepage": "~1.15.0", - "crc-32": "~1.2.1", - "ssf": "~0.11.2", - "wmf": "~1.0.1", - "word": "~0.3.0" - }, "bin": { "xlsx": "bin/xlsx.njs" }, diff --git a/dashboard/package.json b/dashboard/package.json index 4d62cdec..e0e6d198 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -29,12 +29,11 @@ "ahooks": "^3.9.6", "antd": "^5.29.1", "antd-style": "^3.7.1", - "build": "^0.1.4", "docx-preview": "^0.3.2", "i18next": "^25.8.4", "jszip": "^3.10.1", "lucide-react": "^0.562.0", - "mermaid": "^11.13.0", + "mermaid": "^12.1.0", "qrcode.react": "^4.2.0", "react": "^18", "react-dom": "^18", @@ -42,18 +41,20 @@ "react-json-view-lite": "^2.5.0", "react-markdown": "^10.1.0", "react-pdf": "^10.5.0", - "react-router-dom": "^7.13.0", + "react-router-dom": "^7.18.4", "react-syntax-highlighter": "^16.1.1", "react-virtuoso": "^4.18.10", "recharts": "^3.10.1", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", - "xlsx": "^0.18.5" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" }, "overrides": { "serialize-javascript": "^7.0.3", - "@rollup/plugin-terser": "^1.0.0" + "@rollup/plugin-terser": "^1.0.0", + "katex": "^0.19.0", + "dompurify": "^3.4.16" }, "devDependencies": { "@eslint/js": "^9.25.0", @@ -67,7 +68,7 @@ "@types/react-dom": "^18", "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^4.4.1", - "@vitest/coverage-v8": "^3.2.6", + "@vitest/coverage-v8": "^4.1.11", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", @@ -81,6 +82,6 @@ "typescript-eslint": "^8.30.1", "vite": "^6.3.5", "vite-plugin-pwa": "^0.21.2", - "vitest": "^3.2.6" + "vitest": "^4.1.11" } } diff --git a/dashboard/src/org-ui/pages/employees/EmployeesPage.test.tsx b/dashboard/src/org-ui/pages/employees/EmployeesPage.test.tsx index ce5f2998..5c3db84c 100644 --- a/dashboard/src/org-ui/pages/employees/EmployeesPage.test.tsx +++ b/dashboard/src/org-ui/pages/employees/EmployeesPage.test.tsx @@ -68,6 +68,9 @@ describe("EmployeesPage", () => { onOpenEmployee={onOpenEmployee} />, ); + await waitFor(() => expect(client.listDepartments).toHaveBeenCalledOnce()); + await client.listDepartments.mock.results[0]?.value; + await screen.findByTestId("org-employees-empty"); fireEvent.click(await screen.findByTestId("org-employees-create")); fireEvent.change(screen.getByTestId("org-employees-name"), { target: { value: "Ada" }, diff --git a/dashboard/src/org-ui/pages/workspace/WorkspacePage.test.tsx b/dashboard/src/org-ui/pages/workspace/WorkspacePage.test.tsx index 7ea315e3..50302c56 100644 --- a/dashboard/src/org-ui/pages/workspace/WorkspacePage.test.tsx +++ b/dashboard/src/org-ui/pages/workspace/WorkspacePage.test.tsx @@ -118,7 +118,7 @@ describe("WorkspacePage", () => { it("hides governance and settings from non-admins", async () => { const client = mockClient(); render(); - await screen.findByTestId("org-ui-workspace"); + await screen.findByTestId("org-workspace-module-employees"); expect(screen.queryByTestId("org-workspace-module-governance")).toBeNull(); expect(screen.queryByTestId("org-workspace-module-settings")).toBeNull(); expect( diff --git a/dashboard/src/pages/Admin/Users/SsoPanel.test.tsx b/dashboard/src/pages/Admin/Users/SsoPanel.test.tsx index d563d94c..df92b9f1 100644 --- a/dashboard/src/pages/Admin/Users/SsoPanel.test.tsx +++ b/dashboard/src/pages/Admin/Users/SsoPanel.test.tsx @@ -64,9 +64,15 @@ describe("", () => { render(); await waitFor(() => expect(getOidcConfig).toHaveBeenCalledOnce()); - await user.click( - screen.getByRole("button", { name: "adminSso.presetGoogle" }), + const googlePreset = screen.getByRole("button", { + name: "adminSso.presetGoogle", + }); + await waitFor(() => + expect(window.getComputedStyle(googlePreset).pointerEvents).not.toBe( + "none", + ), ); + await user.click(googlePreset); expect(screen.getByDisplayValue("Google")).toBeInTheDocument(); }); }); diff --git a/dashboard/src/pages/Agent/Memory/ProfileOverview.test.tsx b/dashboard/src/pages/Agent/Memory/ProfileOverview.test.tsx index e5604b82..7f85a0d6 100644 --- a/dashboard/src/pages/Agent/Memory/ProfileOverview.test.tsx +++ b/dashboard/src/pages/Agent/Memory/ProfileOverview.test.tsx @@ -108,7 +108,7 @@ describe("", () => { await waitFor(() => { expect(screen.getAllByText(/用户画像/).length).toBeGreaterThan(0); }); - expect(screen.getByText(/Octop 还在了解你/)).toBeInTheDocument(); + expect(await screen.findByText(/Octop 还在了解你/)).toBeInTheDocument(); }); it("caps a section at 6 rows and shows 查看全部 that calls onViewAll", async () => { diff --git a/dashboard/src/pages/Chat/components/ChatDockReviewContent.tsx b/dashboard/src/pages/Chat/components/ChatDockReviewContent.tsx index 5e9adcf2..5f23d75b 100644 --- a/dashboard/src/pages/Chat/components/ChatDockReviewContent.tsx +++ b/dashboard/src/pages/Chat/components/ChatDockReviewContent.tsx @@ -63,9 +63,7 @@ export default function ChatDockReviewContent({ -
- {t("chat.rightRail.reviewEmpty", "还没有可审查的操作")} -
+
{t("chat.rightRail.reviewEmpty", "还没有可审查的操作")}

{t( "chat.rightRail.reviewEmptyHint", diff --git a/dashboard/src/pages/Chat/components/ChatDockStartState.tsx b/dashboard/src/pages/Chat/components/ChatDockStartState.tsx index 3702c964..1ccd9680 100644 --- a/dashboard/src/pages/Chat/components/ChatDockStartState.tsx +++ b/dashboard/src/pages/Chat/components/ChatDockStartState.tsx @@ -23,7 +23,9 @@ interface ChatDockStartStateProps { onOpen: (kind: WorkspacePanelKind) => void; } -export default function ChatDockStartState({ onOpen }: ChatDockStartStateProps) { +export default function ChatDockStartState({ + onOpen, +}: ChatDockStartStateProps) { const { t } = useTranslation(); const title = (kind: WorkspacePanelKind) => { @@ -89,7 +91,9 @@ export default function ChatDockStartState({ onOpen }: ChatDockStartStateProps) {title(kind)} - {hint(kind)} + + {hint(kind)} + -

- {t("chat.rightRail.tasksEmpty", "当前没有后台任务")} -
+
{t("chat.rightRail.tasksEmpty", "当前没有后台任务")}

{t( "chat.rightRail.tasksEmptyHint", diff --git a/dashboard/src/pages/Chat/components/ChatPanelPicker.tsx b/dashboard/src/pages/Chat/components/ChatPanelPicker.tsx index d8d2f02b..3045d46d 100644 --- a/dashboard/src/pages/Chat/components/ChatPanelPicker.tsx +++ b/dashboard/src/pages/Chat/components/ChatPanelPicker.tsx @@ -10,10 +10,7 @@ import type { WorkspacePanelKind } from "../utils/workspacePanels"; import { WORKSPACE_PANEL_KINDS } from "../utils/workspacePanels"; import styles from "../index.module.less"; -const ICONS: Record< - WorkspacePanelKind, - typeof FileText -> = { +const ICONS: Record = { files: FileText, review: ScanSearch, tasks: ListChecks, diff --git a/dashboard/src/pages/Chat/components/TrajectoryInspector.test.tsx b/dashboard/src/pages/Chat/components/TrajectoryInspector.test.tsx index f35af7d4..134f5963 100644 --- a/dashboard/src/pages/Chat/components/TrajectoryInspector.test.tsx +++ b/dashboard/src/pages/Chat/components/TrajectoryInspector.test.tsx @@ -181,17 +181,16 @@ describe("TrajectoryInspector", () => { fireEvent.click(screen.getByRole("tab", { name: "Preview" })); - await waitFor(() => { - expect(eventMock).toHaveBeenCalledWith("A1", "T1", "ctx-1"); - const preview = screen.getByTestId("trajectory-preview"); - expect(preview).toHaveTextContent("Prefer BackendWorkspace paths"); - // Rendered markdown — not the raw ## / ** markers. - expect(preview.querySelector("h2")).toHaveTextContent("Rules"); - expect(preview.querySelector("strong")).toHaveTextContent( - "BackendWorkspace", - ); - expect(preview).not.toHaveTextContent("summary: meta"); - }); + await screen.findByRole("heading", { name: "Rules" }, { timeout: 5000 }); + expect(eventMock).toHaveBeenCalledWith("A1", "T1", "ctx-1"); + const preview = screen.getByTestId("trajectory-preview"); + expect(preview).toHaveTextContent("Prefer BackendWorkspace paths"); + // Rendered markdown — not the raw ## / ** markers. + expect(preview.querySelector("h2")).toHaveTextContent("Rules"); + expect(preview.querySelector("strong")).toHaveTextContent( + "BackendWorkspace", + ); + expect(preview).not.toHaveTextContent("summary: meta"); }); it("labels the middle tab Result for tool events", () => { diff --git a/dashboard/src/pages/Chat/hooks/useChatDockPanel.ts b/dashboard/src/pages/Chat/hooks/useChatDockPanel.ts index d28cb929..03caf059 100644 --- a/dashboard/src/pages/Chat/hooks/useChatDockPanel.ts +++ b/dashboard/src/pages/Chat/hooks/useChatDockPanel.ts @@ -327,7 +327,12 @@ export function useChatDockPanel(isMobile: boolean, agentId?: string | null) { /** Toggle dock open/closed around a dedicated tab (browser / terminal). */ const toggleDockTab = useCallback( - (tab: Extract) => { + ( + tab: Extract< + DockTab, + { kind: "browser" | "terminal" | "review" | "tasks" } + >, + ) => { setDockOpen((prevOpen) => { if (prevOpen && activeTabId === tab.id) { persistRailOpen(false); @@ -376,7 +381,13 @@ export function useChatDockPanel(isMobile: boolean, agentId?: string | null) { } openTerminalTab(); }, - [openBrowserTab, openFileList, openReviewTab, openTasksTab, openTerminalTab], + [ + openBrowserTab, + openFileList, + openReviewTab, + openTasksTab, + openTerminalTab, + ], ); const closeTab = useCallback((id: DockTabId) => { diff --git a/dashboard/src/pages/Chat/utils/chatMessages.test.ts b/dashboard/src/pages/Chat/utils/chatMessages.test.ts index 3b0f3c94..aee173e5 100644 --- a/dashboard/src/pages/Chat/utils/chatMessages.test.ts +++ b/dashboard/src/pages/Chat/utils/chatMessages.test.ts @@ -31,13 +31,15 @@ describe("formatRunUsage", () => { describe("composer permissionMode", () => { it("stamps and normalizes permissionMode", () => { + expect(buildComposerContext({ permissionMode: "auto" })).toEqual({ + permissionMode: "auto", + }); + expect(normalizeComposerContext({ permissionMode: "full" })).toEqual({ + permissionMode: "full", + }); expect( - buildComposerContext({ permissionMode: "auto" }), - ).toEqual({ permissionMode: "auto" }); - expect( - normalizeComposerContext({ permissionMode: "full" }), - ).toEqual({ permissionMode: "full" }); - expect(normalizeComposerContext({ permissionMode: "yolo" })).toBeUndefined(); + normalizeComposerContext({ permissionMode: "yolo" }), + ).toBeUndefined(); }); }); diff --git a/dashboard/src/pages/Chat/utils/expertMention.ts b/dashboard/src/pages/Chat/utils/expertMention.ts index c6764291..856d2c84 100644 --- a/dashboard/src/pages/Chat/utils/expertMention.ts +++ b/dashboard/src/pages/Chat/utils/expertMention.ts @@ -30,7 +30,9 @@ export function textHasExpertMention(text: string, name: string): boolean { export const EVERYONE_MENTION_NAMES = ["所有人", "everyone"] as const; export function textHasEveryoneMention(text: string): boolean { - return EVERYONE_MENTION_NAMES.some((name) => textHasExpertMention(text, name)); + return EVERYONE_MENTION_NAMES.some((name) => + textHasExpertMention(text, name), + ); } export function mentionedExpertIds( diff --git a/dashboard/src/pages/Settings/Models/components/modals/CustomProviderModal.tsx b/dashboard/src/pages/Settings/Models/components/modals/CustomProviderModal.tsx index b0673971..ef0c49bf 100644 --- a/dashboard/src/pages/Settings/Models/components/modals/CustomProviderModal.tsx +++ b/dashboard/src/pages/Settings/Models/components/modals/CustomProviderModal.tsx @@ -263,8 +263,8 @@ export function CustomProviderModal({ k.value === "openai" ? t("models.kindOpenaiCompat") : k.value === "anthropic" - ? "Anthropic" - : "AWS Bedrock", + ? "Anthropic" + : "AWS Bedrock", }))} /> diff --git a/dashboard/src/test/setup.ts b/dashboard/src/test/setup.ts index 72ae7d13..f2b1ad82 100644 --- a/dashboard/src/test/setup.ts +++ b/dashboard/src/test/setup.ts @@ -14,6 +14,7 @@ import "@testing-library/jest-dom/vitest"; import { afterEach, vi } from "vitest"; import { cleanup } from "@testing-library/react"; +import { message } from "@/utils/antdMessage"; // Auto-mock react-i18next so components' ``t(key, fallback)`` calls // resolve synchronously to ``fallback`` without needing the real @@ -56,6 +57,7 @@ vi.mock("react-i18next", async (importOriginal) => { afterEach(() => { cleanup(); + message.destroy(); }); if (typeof window !== "undefined") { diff --git a/docs/cli.md b/docs/cli.md index b2a78e48..ae5249eb 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,6 +1,6 @@ # CLI Reference -`octop` is a single Click app installed as `octop` on `pip install octop`. +`octop` and `freeos` are compatibility command names for the FreeOS CLI. The supported installer obtains FreeOS from the XYAIStudio GitHub repository; Python release artifacts are attached to GitHub Releases. It is registered in `src/octop/cli/main.py`; commands are loaded lazily from `src/octop/cli/registry.py`. @@ -363,8 +363,7 @@ retention only deletes those files and never touches manual ## `octop update` -Check for and install a newer Octop release from the configured -channel (PyPI by default). +Check for and install a newer FreeOS desktop release from GitHub Releases. ``` Usage: octop update [OPTIONS] @@ -373,7 +372,9 @@ Usage: octop update [OPTIONS] Options: --check / --no-check Only check; do not install. - --channel TEXT Release channel (pypi, testpypi, custom index). + --allow-prerelease Include alpha / beta / rc / dev releases. + --yes, -y Skip upgrade confirmation. + --verbose, -v Show installer output. -h, --help Show this message and exit. ``` diff --git a/docs/node-runtime.md b/docs/node-runtime.md index 78aaa7c2..c9f6cd51 100644 --- a/docs/node-runtime.md +++ b/docs/node-runtime.md @@ -65,15 +65,15 @@ expanding `SHIP_OPENXYOS_RUNTIME=1` to new flavors. | GHCR image | `ghcr.io/xyaistudio/freeos:{version\|latest}` | | Local Compose tag | `octop:latest` (compatibility alias; one Python process) | | Security advisories | [XYAIStudio/FreeOS](https://github.com/XYAIStudio/FreeOS/security/advisories/new) | -| PyPI package name | `octop` (intentional remaining identifier; CLI is `freeos`) | +| Python distribution identifier | `octop` (internal compatibility name; distributed through GitHub Releases only) | -### Release / PyPI / Docker gates +### Release / Docker gates - **Docker Publish** pushes GHCR always. Docker Hub login is **optional** (`DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN`). Missing Hub secrets must not fail the workflow. -- **PyPI** still publishes the `octop` distribution (`PYPI_API_TOKEN` on the - `pypi` environment). That name is the Python package, not a pointer at - Tencent Cloud advisories. +- **Python distributions** are attached to GitHub Releases. Do not publish the + `octop` distribution to PyPI: that name is already used by the upstream + Octop project and would create an installation identity collision. - **FnOS Docker `.fpk`** compose must pull `ghcr.io/xyaistudio/freeos`, not `ghcr.io/tencentcloud/octop`. diff --git a/docs/node-runtime.zh-CN.md b/docs/node-runtime.zh-CN.md index 73647c4a..b24736fe 100644 --- a/docs/node-runtime.zh-CN.md +++ b/docs/node-runtime.zh-CN.md @@ -60,13 +60,13 @@ GitHub 工作流 **FreeOS Desktop Package**(`.github/workflows/octop-desktop.y | GHCR 镜像 | `ghcr.io/xyaistudio/freeos:{version\|latest}` | | 本地 Compose 标签 | `octop:latest`(兼容别名;单 Python 进程) | | 安全公告 | [XYAIStudio/FreeOS](https://github.com/XYAIStudio/FreeOS/security/advisories/new) | -| PyPI 包名 | `octop`(有意保留的标识;CLI 是 `freeos`) | +| Python 发行包标识 | `octop`(内部兼容名;仅通过 GitHub Releases 分发) | -### 发版 / PyPI / Docker 门禁 +### 发版 / Docker 门禁 - **Docker Publish** 始终推 GHCR。Docker Hub 登录是 **可选** (`DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN`)。没有 Hub secrets 不得让工作流变红。 -- **PyPI** 仍发布名为 `octop` 的发行版(`pypi` 环境的 `PYPI_API_TOKEN`)。这是 - Python 包名,不是腾讯云安全公告地址。 +- **Python 发行包**随 GitHub Release 附件发布。不要将 `octop` 发行包发布到 + PyPI:该名称已被上游 Octop 项目使用,会造成安装身份冲突。 - **飞牛 Docker `.fpk`** 的 compose 必须拉 `ghcr.io/xyaistudio/freeos`,不是 `ghcr.io/tencentcloud/octop`。 diff --git a/docs/user-guide.md b/docs/user-guide.md index 960404d8..1f3bd784 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -100,7 +100,7 @@ source ~/.bashrc # Bash 安装脚本会把 `octop` 命令放入 `~/.octop/bin` 并加入 PATH,并在 `~/.octop/venv` 创建隔离环境;**不会改动系统 Python**。 -> **可选附加组件**:安装脚本支持通过 `--extras` 追加能力,例如浏览器自动化 `--extras browser`、飞书通道 `--extras channels-feishu`;也可用 `--version` 指定版本、`--mirror ` 使用国内 PyPI 镜像。更多选项见 [scripts/README.md](scripts/README.md)。 +> **可选附加组件**:安装脚本默认从 FreeOS GitHub 仓库安装,支持通过 `--extras` 追加能力,例如浏览器自动化 `--extras browser`、飞书通道 `--extras channels-feishu`;也可用 `--version` 指定 GitHub 版本标签、`--mirror ` 使用国内 PyPI 镜像下载 Python 依赖。更多选项见 [scripts/README.md](../scripts/README.md)。 ### 2.3 验证安装 @@ -466,7 +466,7 @@ Octop 支持两个方向的 ACP 集成: ``` **Q:如何升级?** -- `octop update`(若通过一键安装);或从 PyPI / 源码重新安装后重启服务。 +- `octop update`(若通过一键安装);或下载 GitHub Release 中的 Python 制品 / 从 FreeOS 源码重新安装后重启服务。 **Q:如何加入客户企业微信服务群?** - 请扫描下方二维码加入: diff --git a/pyproject.toml b/pyproject.toml index 63fae65b..0b9c75a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "octop" -version = "0.0.10" +version = "0.1.0" description = "FreeOS — self-hosted multi-user multi-agent assistant with an optional organization OS module (Octop-compatible package name)" readme = "README.md" license = { text = "MIT" } diff --git a/scripts/README.md b/scripts/README.md index 72997813..556c04cb 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -25,12 +25,15 @@ ### 常用选项 ```bash -# 从 PyPI 安装最新版(默认) +# 从 FreeOS GitHub 源码安装最新版(默认) bash scripts/install.sh -# 指定版本 +# 从 FreeOS GitHub v0.1.0 标签安装 bash scripts/install.sh --version 0.1.0 +# 显式安装 PyPI 上游 Octop(不包含 FreeOS 下游修改) +bash scripts/install.sh --from-pypi + # 安装可选附加组件 bash scripts/install.sh --extras browser bash scripts/install.sh --extras browser,channels-feishu @@ -39,11 +42,11 @@ bash scripts/install.sh --extras browser,channels-feishu bash scripts/install.sh --from-source bash scripts/install.sh --from-source /path/to/orca -# 使用国内 PyPI 镜像加速依赖 +# 使用国内 PyPI 镜像加速 Python 依赖下载 bash scripts/install.sh --mirror https://mirrors.cloud.tencent.com/pypi/simple ``` -Windows PowerShell 等价参数:`-Version`、`-FromSource`、`-SourceDir`、`-Extras`。 +Windows PowerShell 等价参数:`-Version`、`-FromSource`、`-FromPyPI`、`-SourceDir`、`-Extras`。 ### 环境变量 @@ -65,7 +68,7 @@ octop run # 启动服务 → http://127.0.0.1:8088 --- -## 构建 PyPI wheel +## 构建 Python wheel(随 GitHub Release 分发) 先构建前端(产物写入 `src/octop/dashboard/`,与 `dashboard/vite.config.ts` 的 `outDir` 一致),再打包 wheel。 @@ -81,7 +84,7 @@ powershell -File scripts/wheel_build.ps1 输出:`dist/*.whl`、`dist/*.tar.gz` -发布前请确认 `pyproject.toml` 中的 `orcakit-harness-agent`、`harness-gateway` 等依赖已发布到 PyPI(`[tool.uv.sources]` 仅对 uv 本地开发生效,pip/PyPI 不读取)。 +Release workflow 将构建出的 wheel 和源码包附加到 GitHub Release。`octop` 是内部兼容用的发行包名,不要把 FreeOS 制品发布到 PyPI;安装脚本默认从 FreeOS GitHub 仓库安装。 --- diff --git a/scripts/install-octop.sh b/scripts/install-octop.sh index 493da44e..17703d34 100644 --- a/scripts/install-octop.sh +++ b/scripts/install-octop.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Octop 安装脚本 (macOS / Linux) -# 用法: bash scripts/install.sh # 从 PyPI 安装(默认) -# 或: bash scripts/install.sh --from-source # 从本地源码安装 +# 用法: bash scripts/install-octop.sh # 从 FreeOS GitHub 源码安装(默认) +# 或: bash scripts/install-octop.sh --from-pypi # 从 PyPI 安装 Octop(不含 FreeOS 下游改动) # 或: curl -fsSL /install.sh | bash # 远程安装 # # 将 Octop 安装到 ~/.octop,使用 uv 管理 Python 环境。 @@ -34,7 +34,7 @@ else fi VERSION="" -FROM_SOURCE=false +FROM_SOURCE=true EXTRAS="" PYPI_MIRROR="" @@ -81,9 +81,9 @@ Octop installer (macOS / Linux) Usage: bash install.sh [OPTIONS] Options: - --version Install a specific version (e.g. 0.1.0) [PyPI only] + --version Install a specific GitHub tag (e.g. 0.1.0; tag v0.1.0) --from-source [DIR] Install from source; clones the git repo if DIR is omitted - --from-pypi Install from PyPI (default) + --from-pypi Install upstream Octop from PyPI (does not include FreeOS changes) --extras Extra optional components (e.g. desktop, browser) --mirror Use a specific PyPI mirror (e.g. https://mirrors.aliyun.com/pypi/simple) -h, --help Show this help @@ -648,7 +648,11 @@ _clone_source_workspace() { git clone --depth 1 "$HARNESS_AGENT_REPO" "$workdir/harness-agent" git clone --depth 1 "$HARNESS_GATEWAY_REPO" "$workdir/harness-gateway" git clone --depth 1 "$HARNESS_BROWSER_REPO" "$workdir/harness-browser" - git clone --depth 1 "$OCTOP_REPO" "$workdir/orca" + if [ -n "$VERSION" ]; then + git clone --depth 1 --branch "v$VERSION" "$OCTOP_REPO" "$workdir/orca" + else + git clone --depth 1 "$OCTOP_REPO" "$workdir/orca" + fi } if [ "$FROM_SOURCE" = true ]; then diff --git a/scripts/install.bat b/scripts/install.bat index 8c500688..3980721e 100644 --- a/scripts/install.bat +++ b/scripts/install.bat @@ -2,16 +2,16 @@ setlocal EnableDelayedExpansion REM Octop Installer for Windows (cmd.exe) -REM Usage: install.bat [-Version X.Y.Z] [-FromSource] [-SourceDir DIR] [-Extras browser] [-Help] +REM Usage: install.bat [-Version X.Y.Z] [-FromSource | -FromPyPI] [-SourceDir DIR] [-Extras browser] [-Help] if defined OCTOP_HOME (set "OCTOP_HOME=%OCTOP_HOME%") else (set "OCTOP_HOME=%USERPROFILE%\.octop") set "OCTOP_VENV=%OCTOP_HOME%\venv" set "OCTOP_BIN=%OCTOP_HOME%\bin" set "PYTHON_VERSION=3.12" -if defined OCTOP_REPO (set "OCTOP_REPO=%OCTOP_REPO%") else (set "OCTOP_REPO=https://github.com/TencentCloud/Octop.git") +if defined OCTOP_REPO (set "OCTOP_REPO=%OCTOP_REPO%") else (set "OCTOP_REPO=https://github.com/XYAIStudio/FreeOS.git") set "ARG_VERSION=" -set "ARG_FROM_SOURCE=0" +set "ARG_FROM_SOURCE=1" set "ARG_SOURCE_DIR=" set "ARG_EXTRAS=" set "ARG_UV_PATH=" @@ -21,6 +21,7 @@ set "CONSOLE_AVAILABLE=0" if "%~1"=="" goto :done_args if /i "%~1"=="-Version" (set "ARG_VERSION=%~2" & shift & shift & goto :parse_args) if /i "%~1"=="-FromSource" (set "ARG_FROM_SOURCE=1" & shift & goto :parse_args) +if /i "%~1"=="-FromPyPI" (set "ARG_FROM_SOURCE=0" & shift & goto :parse_args) if /i "%~1"=="-SourceDir" (set "ARG_SOURCE_DIR=%~2" & shift & shift & goto :parse_args) if /i "%~1"=="-Extras" (set "ARG_EXTRAS=%~2" & shift & shift & goto :parse_args) if /i "%~1"=="-UvPath" (set "ARG_UV_PATH=%~2" & shift & shift & goto :parse_args) @@ -34,6 +35,7 @@ echo. echo Usage: install.bat [OPTIONS] echo -Version ^ Install specific version echo -FromSource Install from source +echo -FromPyPI Install upstream Octop from PyPI (does not include FreeOS changes) echo -SourceDir ^ Local source directory echo -Extras ^ Extra components (e.g. desktop, browser) echo -UvPath ^ Pre-installed uv.exe @@ -86,7 +88,11 @@ goto :install_verify where git >nul 2>&1 if errorlevel 1 (echo [octop] ERROR: git required & exit /b 1) set "CLONE_DIR=%TEMP%\octop-install-%RANDOM%" -git clone --depth 1 %OCTOP_REPO% "%CLONE_DIR%" +if defined ARG_VERSION ( + git clone --depth 1 --branch "v%ARG_VERSION%" %OCTOP_REPO% "%CLONE_DIR%" +) else ( + git clone --depth 1 %OCTOP_REPO% "%CLONE_DIR%" +) if errorlevel 1 exit /b 1 call :prepare_console "%CLONE_DIR%" uv pip install "%CLONE_DIR%%EXTRAS_SUFFIX%" --python "%VENV_PYTHON%" diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 48c4cb1c..4b891c1d 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -8,6 +8,7 @@ param( [string]$Version = "", [switch]$FromSource, + [switch]$FromPyPI, [string]$SourceDir = "", [string]$Extras = "", [string]$UvPath = "", @@ -20,12 +21,15 @@ $OctopHome = if ($env:OCTOP_HOME) { $env:OCTOP_HOME } else { Join-Path $HOME $OctopVenv = Join-Path $OctopHome "venv" $OctopBin = Join-Path $OctopHome "bin" $PythonVersion = "3.12" -$OctopRepo = if ($env:OCTOP_REPO) { $env:OCTOP_REPO } else { "https://github.com/TencentCloud/Octop.git" } +$OctopRepo = if ($env:OCTOP_REPO) { $env:OCTOP_REPO } else { "https://github.com/XYAIStudio/FreeOS.git" } function Write-Info { param([string]$Message) Write-Host "[octop] " -ForegroundColor Green -NoNewline; Write-Host $Message } function Write-Warn { param([string]$Message) Write-Host "[octop] " -ForegroundColor Yellow -NoNewline; Write-Host $Message } function Stop-WithError { param([string]$Message) Write-Host "[octop] ERROR: $Message" -ForegroundColor Red; exit 1 } +if ($FromSource -and $FromPyPI) { Stop-WithError "Choose only one of -FromSource or -FromPyPI." } +if (-not $FromPyPI) { $FromSource = $true } + if ($Help) { @" Octop Installer for Windows @@ -35,6 +39,7 @@ Usage: .\install.ps1 [OPTIONS] Options: -Version Install a specific version (e.g. 0.1.0) -FromSource Install from source (requires git, or use -SourceDir) + -FromPyPI Install upstream Octop from PyPI (does not include FreeOS changes) -SourceDir

Local source directory (used with -FromSource) -Extras Extra optional components (e.g. desktop, browser) -UvPath Path to a pre-installed uv.exe @@ -226,7 +231,11 @@ if ($FromSource) { } $cloneDir = Join-Path $env:TEMP "octop-install-$(Get-Random)" try { + if ($Version) { + git clone --depth 1 --branch "v$Version" $OctopRepo $cloneDir + } else { git clone --depth 1 $OctopRepo $cloneDir + } Prepare-Console $cloneDir uv pip install "${cloneDir}${ExtrasSuffix}" --python $VenvPython } finally { diff --git a/scripts/install.sh b/scripts/install.sh index 15138c8c..c90570ca 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Octop 安装脚本 (macOS / Linux) -# 用法: bash scripts/install.sh # 从 PyPI 安装(默认) -# 或: bash scripts/install.sh --from-source # 从本地源码安装 +# 用法: bash scripts/install.sh # 从 FreeOS GitHub 源码安装(默认) +# 或: bash scripts/install.sh --from-pypi # 从 PyPI 安装 Octop(不含 FreeOS 下游改动) # 或: curl -fsSL /install.sh | bash # 远程安装 # # 将 Octop 安装到 ~/.octop,使用 uv 管理 Python 环境。 @@ -34,7 +34,7 @@ else fi VERSION="" -FROM_SOURCE=false +FROM_SOURCE=true EXTRAS="" PYPI_MIRROR="" @@ -81,9 +81,9 @@ Octop installer (macOS / Linux) Usage: bash install.sh [OPTIONS] Options: - --version Install a specific version (e.g. 0.1.0) [PyPI only] + --version Install a specific GitHub tag (e.g. 0.1.0; tag v0.1.0) --from-source [DIR] Install from source; clones the git repo if DIR is omitted - --from-pypi Install from PyPI (default) + --from-pypi Install upstream Octop from PyPI (does not include FreeOS changes) --extras Extra optional components (e.g. desktop, browser) --mirror Use a specific PyPI mirror (e.g. https://mirrors.cloud.tencent.com/pypi/simple) -h, --help Show this help @@ -648,7 +648,11 @@ _clone_source_workspace() { git clone --depth 1 "$HARNESS_AGENT_REPO" "$workdir/harness-agent" git clone --depth 1 "$HARNESS_GATEWAY_REPO" "$workdir/harness-gateway" git clone --depth 1 "$HARNESS_BROWSER_REPO" "$workdir/harness-browser" - git clone --depth 1 "$OCTOP_REPO" "$workdir/orca" + if [ -n "$VERSION" ]; then + git clone --depth 1 --branch "v$VERSION" "$OCTOP_REPO" "$workdir/orca" + else + git clone --depth 1 "$OCTOP_REPO" "$workdir/orca" + fi } if [ "$FROM_SOURCE" = true ]; then diff --git a/src/octop/__init__.py b/src/octop/__init__.py index 307eb09e..d7857b72 100644 --- a/src/octop/__init__.py +++ b/src/octop/__init__.py @@ -2,4 +2,4 @@ from __future__ import annotations -__version__ = "0.0.10" +__version__ = "0.1.0" diff --git a/src/octop/cli/registry.py b/src/octop/cli/registry.py index 815890b7..d3887746 100644 --- a/src/octop/cli/registry.py +++ b/src/octop/cli/registry.py @@ -23,7 +23,7 @@ "admin": (".commands.admin", "admin", "Admin commands."), "version": (".commands.version", "version", "Show the installed octop version."), "completion": (".commands.completion", "completion", "Shell completion utilities."), - "update": (".commands.update", "update", "Check for and install a newer Octop release."), + "update": (".commands.update", "update", "Check for and install a newer FreeOS release."), "clean": (".commands.clean", "clean", "Remove CLI state or wipe all of ~/.octop."), "backup": (".commands.backup", "backup", "Export and restore Octop backups."), "acp": (".commands.acp", "acp_cmd", "Run Octop agent as ACP server (stdio)."), @@ -31,6 +31,6 @@ "org": ( ".commands.org", "org", - "Organization self-growth loop, governance, and module↔skill bridge.", + "Organization self-growth loop, governance, and module/skill bridge.", ), } diff --git a/src/octop/infra/agents/manager.py b/src/octop/infra/agents/manager.py index c9d42eb9..6764813e 100644 --- a/src/octop/infra/agents/manager.py +++ b/src/octop/infra/agents/manager.py @@ -2787,6 +2787,9 @@ def _build_harness_config(self, row: AgentRow) -> HarnessAgentConfig: ) from octop.infra.agents.middleware.org_governance import OrgGovernanceMiddleware from octop.infra.agents.middleware.reasoning import ReasoningRequestMiddleware + from octop.infra.agents.middleware.system_message_cleanup import ( + SystemMessageCleanupMiddleware, + ) from octop.infra.agents.middleware.thread_artifacts import ThreadArtifactsMiddleware from octop.infra.agents.middleware.token_quota import TokenQuotaMiddleware from octop.infra.agents.middleware.workspace_image import ( @@ -2807,6 +2810,7 @@ def _build_harness_config(self, row: AgentRow) -> HarnessAgentConfig: ), ReasoningRequestMiddleware(), OllamaToolFallbackMiddleware(), + SystemMessageCleanupMiddleware(), GroupMentionMiddleware(lambda: self._harness_manager), KnowledgeSearchHintMiddleware(), BrowserProfileMiddleware(), diff --git a/src/octop/infra/agents/middleware/system_message_cleanup.py b/src/octop/infra/agents/middleware/system_message_cleanup.py new file mode 100644 index 00000000..8485fa12 --- /dev/null +++ b/src/octop/infra/agents/middleware/system_message_cleanup.py @@ -0,0 +1,34 @@ +"""Keep checkpoint-resident system messages out of model requests.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from typing import Any + +from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse +from langchain_core.messages import AnyMessage, SystemMessage + + +class SystemMessageCleanupMiddleware(AgentMiddleware[Any, Any]): + """Remove stale system messages while preserving the compiled system prompt. + + Harness injects the active prompt through ``request.system_message``. Older + checkpoints may still contain ``SystemMessage`` entries in ``messages``; + strict chat templates such as Qwen reject those entries when they appear + after conversation messages. + """ + + async def awrap_model_call( + self, + request: ModelRequest[Any], + handler: Callable[[ModelRequest[Any]], Awaitable[ModelResponse[Any]]], + ) -> ModelResponse[Any]: + messages: list[AnyMessage] = [ + message for message in request.messages if not isinstance(message, SystemMessage) + ] + if len(messages) != len(request.messages): + request = request.override(messages=messages) + return await handler(request) + + +__all__ = ["SystemMessageCleanupMiddleware"] diff --git a/tests/unit/agents/test_octop_builtin_skills.py b/tests/unit/agents/test_octop_builtin_skills.py index 65e7b79c..447412a4 100644 --- a/tests/unit/agents/test_octop_builtin_skills.py +++ b/tests/unit/agents/test_octop_builtin_skills.py @@ -175,11 +175,15 @@ def test_manager_refuses_overwrite_without_force(tmp_path: Path) -> None: refused = _run_manager(tmp_path, "install", str(source)) assert refused.returncode == 1 assert "ask before using --force" in refused.stdout - assert "First version" in (tmp_path / "skills" / "stable" / "SKILL.md").read_text() + assert "First version" in (tmp_path / "skills" / "stable" / "SKILL.md").read_text( + encoding="utf-8" + ) replaced = _run_manager(tmp_path, "install", str(source), "--force") assert replaced.returncode == 0, replaced.stderr or replaced.stdout - assert "Second version" in (tmp_path / "skills" / "stable" / "SKILL.md").read_text() + assert "Second version" in (tmp_path / "skills" / "stable" / "SKILL.md").read_text( + encoding="utf-8" + ) def test_manager_refuses_to_replace_itself(tmp_path: Path) -> None: diff --git a/tests/unit/agents/test_system_message_cleanup.py b/tests/unit/agents/test_system_message_cleanup.py new file mode 100644 index 00000000..a961f0b9 --- /dev/null +++ b/tests/unit/agents/test_system_message_cleanup.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from typing import Any +from unittest.mock import Mock + +import pytest +from langchain.agents.middleware import ModelRequest, ModelResponse +from langchain_core.messages import AIMessage, HumanMessage, SystemMessage + +from octop.infra.agents.middleware.system_message_cleanup import ( + SystemMessageCleanupMiddleware, +) + + +@pytest.mark.asyncio +async def test_removes_checkpoint_system_messages_and_keeps_compiled_prompt() -> None: + middleware = SystemMessageCleanupMiddleware() + compiled = SystemMessage(content="current prompt") + request = ModelRequest( + model=Mock(), + system_message=compiled, + messages=[ + HumanMessage(content="first"), + SystemMessage(content="stale bootstrap"), + AIMessage(content="reply"), + HumanMessage(content="next"), + ], + tools=[], + ) + seen: list[ModelRequest[Any]] = [] + expected = Mock(spec=ModelResponse) + + async def handler(value: ModelRequest[Any]) -> ModelResponse[Any]: + seen.append(value) + return expected + + result = await middleware.awrap_model_call(request, handler) + + assert result is expected + assert seen[0].system_message is compiled + assert [type(message) for message in seen[0].messages] == [ + HumanMessage, + AIMessage, + HumanMessage, + ] + + +@pytest.mark.asyncio +async def test_forwards_clean_request_without_replacing_it() -> None: + middleware = SystemMessageCleanupMiddleware() + request = ModelRequest(model=Mock(), messages=[HumanMessage(content="hello")], tools=[]) + seen: list[ModelRequest[Any]] = [] + expected = Mock(spec=ModelResponse) + + async def handler(value: ModelRequest[Any]) -> ModelResponse[Any]: + seen.append(value) + return expected + + result = await middleware.awrap_model_call(request, handler) + + assert result is expected + assert seen == [request] diff --git a/tests/unit/cli/test_completion_cmd.py b/tests/unit/cli/test_completion_cmd.py index d913958a..ebf7aeb7 100644 --- a/tests/unit/cli/test_completion_cmd.py +++ b/tests/unit/cli/test_completion_cmd.py @@ -28,13 +28,13 @@ def test_completion_install_appends_eval_line_idempotent( runner = CliRunner() r1 = runner.invoke(cli, ["completion", "install", "--shell", "bash", "--rc-file", str(rc)]) assert r1.exit_code == 0, r1.output - contents = rc.read_text() + contents = rc.read_text(encoding="utf-8") assert "_OCTOP_COMPLETE" in contents line_count_first = contents.count("_OCTOP_COMPLETE") r2 = runner.invoke(cli, ["completion", "install", "--shell", "bash", "--rc-file", str(rc)]) assert r2.exit_code == 0 - assert rc.read_text().count("_OCTOP_COMPLETE") == line_count_first + assert rc.read_text(encoding="utf-8").count("_OCTOP_COMPLETE") == line_count_first def test_completion_in_help() -> None: diff --git a/tests/unit/db/test_clip_thread_title.py b/tests/unit/db/test_clip_thread_title.py index c8eb211d..f78aa440 100644 --- a/tests/unit/db/test_clip_thread_title.py +++ b/tests/unit/db/test_clip_thread_title.py @@ -56,7 +56,7 @@ def test_migration_003_repairs_stored_hard_cuts(tmp_path: Path) -> None: ( Path(__file__).resolve().parents[3] / "src/octop/infra/db/migrations/001_initial.sql" - ).read_text() + ).read_text(encoding="utf-8") ) # Partial v2 columns so full migrate can finish. conn.execute("ALTER TABLE cron_jobs ADD COLUMN mcp_servers TEXT NOT NULL DEFAULT '[]'") diff --git a/tests/unit/db/test_db_pool.py b/tests/unit/db/test_db_pool.py index f6be371b..ce0f1e4e 100644 --- a/tests/unit/db/test_db_pool.py +++ b/tests/unit/db/test_db_pool.py @@ -136,7 +136,7 @@ def test_repair_legacy_schema_ensures_columns(tmp_path: Path) -> None: ( Path(__file__).resolve().parents[3] / "src/octop/infra/db/migrations/001_initial.sql" - ).read_text() + ).read_text(encoding="utf-8") ) # Simulate a schema version ahead of what the columns reflect conn.execute("UPDATE _schema_version SET version = 2") @@ -156,7 +156,7 @@ def test_migration_002_idempotent_when_column_already_present(tmp_path: Path) -> ( Path(__file__).resolve().parents[3] / "src/octop/infra/db/migrations/001_initial.sql" - ).read_text() + ).read_text(encoding="utf-8") ) conn.execute("ALTER TABLE cron_jobs ADD COLUMN mcp_servers TEXT NOT NULL DEFAULT '[]'") run_migrations(pool) @@ -179,7 +179,7 @@ def test_migration_005_preserves_populated_users_and_constraints(tmp_path: Path) Path(__file__).resolve().parents[3] / "src/octop/infra/db/migrations/001_initial.sql" ) with pool.connect() as conn: - conn.executescript(initial_migration.read_text()) + conn.executescript(initial_migration.read_text(encoding="utf-8")) conn.execute("UPDATE _schema_version SET version = 4") conn.execute( """ @@ -293,7 +293,7 @@ def test_stuck_version_6_without_permissions_column_is_repaired(tmp_path: Path) ( Path(__file__).resolve().parents[3] / "src/octop/infra/db/migrations/001_initial.sql" - ).read_text() + ).read_text(encoding="utf-8") ) conn.execute("UPDATE _schema_version SET version = 6") run_migrations(pool) @@ -313,7 +313,7 @@ def test_schema_v10_without_projection_tables_is_repaired(tmp_path: Path) -> Non ( Path(__file__).resolve().parents[3] / "src/octop/infra/db/migrations/001_initial.sql" - ).read_text() + ).read_text(encoding="utf-8") ) conn.execute("UPDATE _schema_version SET version = 10") run_migrations(pool) @@ -346,7 +346,7 @@ def test_ahead_of_max_schema_version_clamps_to_max(tmp_path: Path) -> None: ( Path(__file__).resolve().parents[3] / "src/octop/infra/db/migrations/001_initial.sql" - ).read_text() + ).read_text(encoding="utf-8") ) conn.execute("UPDATE _schema_version SET version = 8") run_migrations(pool) @@ -374,7 +374,7 @@ def test_current_watermark_repairs_pre_v13_connectors_schema(tmp_path: Path) -> ( Path(__file__).resolve().parents[3] / "src/octop/infra/db/migrations/001_initial.sql" - ).read_text() + ).read_text(encoding="utf-8") ) conn.execute("UPDATE _schema_version SET version = 13") conn.execute( @@ -427,7 +427,7 @@ def test_pre_squash_schema_version_clamped_and_knowledge_tables_filled( ( Path(__file__).resolve().parents[3] / "src/octop/infra/db/migrations/001_initial.sql" - ).read_text() + ).read_text(encoding="utf-8") ) # Simulate a develop DB whose recorded version is ahead of the # consolidated max, so reconcile clamps and repairs knowledge tables. @@ -591,7 +591,9 @@ def test_v7_sqlite_sql_upgrades_legacy_text_pks(tmp_path: Path) -> None: "id, kb_id, filename, content_type, byte_size, created_at, updated_at) " "VALUES ('doc1', 'kb1', 'a.md', 'text/markdown', 1, 1, 1)" ) - conn.executescript((migrations / "007_resource_identity_and_profile.sql").read_text()) + conn.executescript( + (migrations / "007_resource_identity_and_profile.sql").read_text(encoding="utf-8") + ) with pool.connect() as conn: version = conn.execute("SELECT version FROM _schema_version").fetchone()[0] diff --git a/tests/unit/db/test_published_experts_repo.py b/tests/unit/db/test_published_experts_repo.py index ddf33df4..2b872893 100644 --- a/tests/unit/db/test_published_experts_repo.py +++ b/tests/unit/db/test_published_experts_repo.py @@ -114,8 +114,10 @@ def test_legacy_text_pk_published_experts_keep_public_id(tmp_path: Path) -> None pool = SqlitePool(tmp_path / "octop.db") migrations = Path(__file__).resolve().parents[3] / "src/octop/infra/db/migrations" with pool.connect() as conn: - conn.executescript((migrations / "001_initial.sql").read_text()) - conn.executescript((migrations / "005_shared_experts_sso_knowledge.sql").read_text()) + conn.executescript((migrations / "001_initial.sql").read_text(encoding="utf-8")) + conn.executescript( + (migrations / "005_shared_experts_sso_knowledge.sql").read_text(encoding="utf-8") + ) conn.execute( "INSERT INTO published_experts(" "id, slug, name, description, created_by, source_agent_id, " diff --git a/tests/unit/db/test_repo_knowledge.py b/tests/unit/db/test_repo_knowledge.py index ddb95d02..84b5859f 100644 --- a/tests/unit/db/test_repo_knowledge.py +++ b/tests/unit/db/test_repo_knowledge.py @@ -169,7 +169,7 @@ def test_migration_007_rebuilds_text_primary_keys(tmp_path: Path) -> None: ( Path(__file__).resolve().parents[3] / "src/octop/infra/db/migrations/001_initial.sql" - ).read_text() + ).read_text(encoding="utf-8") ) conn.execute("UPDATE _schema_version SET version = 6") conn.executescript( diff --git a/tests/unit/db/test_skill_package_icons.py b/tests/unit/db/test_skill_package_icons.py index 46f0cff0..746a8f01 100644 --- a/tests/unit/db/test_skill_package_icons.py +++ b/tests/unit/db/test_skill_package_icons.py @@ -58,8 +58,10 @@ def test_repair_legacy_schema_ensures_name_unique_index(tmp_path: Path) -> None: db_path = tmp_path / "octop.db" pool = SqlitePool(db_path) with pool.connect() as conn: - conn.executescript((_MIGRATIONS / "001_initial.sql").read_text()) - conn.executescript((_MIGRATIONS / "002_cron_mcp_and_skill_packages.sql").read_text()) + conn.executescript((_MIGRATIONS / "001_initial.sql").read_text(encoding="utf-8")) + conn.executescript( + (_MIGRATIONS / "002_cron_mcp_and_skill_packages.sql").read_text(encoding="utf-8") + ) conn.execute("DROP INDEX IF EXISTS idx_skill_packages_name") run_migrations(pool) with pool.connect() as conn: @@ -77,7 +79,7 @@ def test_migration_002_idempotent_when_icon_columns_already_present(tmp_path: Pa db_path = tmp_path / "octop.db" pool = SqlitePool(db_path) with pool.connect() as conn: - conn.executescript((_MIGRATIONS / "001_initial.sql").read_text()) + conn.executescript((_MIGRATIONS / "001_initial.sql").read_text(encoding="utf-8")) conn.executescript(_LEGACY_SKILL_PACKAGES_WITHOUT_ICONS) conn.execute("ALTER TABLE skill_packages ADD COLUMN icon_name TEXT NOT NULL DEFAULT ''") conn.execute("ALTER TABLE skill_packages ADD COLUMN icon_url TEXT NOT NULL DEFAULT ''") @@ -103,7 +105,7 @@ def test_repair_legacy_schema_adds_icon_columns_at_version_2(tmp_path: Path) -> db_path = tmp_path / "octop.db" pool = SqlitePool(db_path) with pool.connect() as conn: - conn.executescript((_MIGRATIONS / "001_initial.sql").read_text()) + conn.executescript((_MIGRATIONS / "001_initial.sql").read_text(encoding="utf-8")) conn.executescript(_LEGACY_SKILL_PACKAGES_WITHOUT_ICONS) conn.execute("UPDATE _schema_version SET version = 2") run_migrations(pool) diff --git a/tests/unit/db/test_skill_packages_repo.py b/tests/unit/db/test_skill_packages_repo.py index f340b39d..4d4354e9 100644 --- a/tests/unit/db/test_skill_packages_repo.py +++ b/tests/unit/db/test_skill_packages_repo.py @@ -52,8 +52,10 @@ def test_skill_package_repo_create_get(db: SqlitePool) -> None: def test_legacy_text_pk_skill_packages_keep_public_id(tmp_path: Path) -> None: pool = SqlitePool(tmp_path / "octop.db") with pool.connect() as conn: - conn.executescript((_MIGRATIONS / "001_initial.sql").read_text()) - conn.executescript((_MIGRATIONS / "002_cron_mcp_and_skill_packages.sql").read_text()) + conn.executescript((_MIGRATIONS / "001_initial.sql").read_text(encoding="utf-8")) + conn.executescript( + (_MIGRATIONS / "002_cron_mcp_and_skill_packages.sql").read_text(encoding="utf-8") + ) conn.execute( "INSERT INTO skill_packages(" "id, name, description, created_by, skill_count, icon_name, icon_url, " diff --git a/uv.lock b/uv.lock index 75ee7d32..6b273614 100644 --- a/uv.lock +++ b/uv.lock @@ -2503,7 +2503,7 @@ wheels = [ [[package]] name = "octop" -version = "0.0.10" +version = "0.1.0" source = { editable = "." } dependencies = [ { name = "acme" }, From 61ee8c2b0ec8116e2683bae94acc3a4437af7736 Mon Sep 17 00:00:00 2001 From: XYAIStudio Date: Sat, 10 Oct 2026 09:20:39 +0800 Subject: [PATCH 05/14] fix(release): enforce native checks and repair organization dependency drift --- .github/workflows/octop-desktop.yml | 39 +- .github/workflows/release.yml | 2 +- Makefile | 3 +- desktop/portable/bundle-org-sidecar.sh | 2 +- desktop/portable/verify_imports.py | 12 +- .../openxyos/backend/services/doc-parser.ts | 12 +- modules/openxyos/package-lock.json | 461 ++++++++---------- modules/openxyos/package.json | 16 +- modules/openxyos/scripts/test-doc-parser.ts | 47 ++ tests/unit/desktop/test_verify_imports.py | 38 ++ 10 files changed, 361 insertions(+), 271 deletions(-) create mode 100644 modules/openxyos/scripts/test-doc-parser.ts create mode 100644 tests/unit/desktop/test_verify_imports.py diff --git a/.github/workflows/octop-desktop.yml b/.github/workflows/octop-desktop.yml index 65d5c623..25633a9d 100644 --- a/.github/workflows/octop-desktop.yml +++ b/.github/workflows/octop-desktop.yml @@ -77,6 +77,16 @@ jobs: - name: Build frontend → src/octop/dashboard run: make build-frontend + - name: Check dashboard dependencies + working-directory: dashboard + run: npm audit --registry=https://registry.npmjs.org + + - name: Test dashboard + working-directory: dashboard + env: + NODE_OPTIONS: "--no-experimental-webstorage" + run: npm run lint && npm test + - uses: actions/upload-artifact@v7 with: name: dashboard-dist @@ -84,6 +94,27 @@ jobs: if-no-files-found: error retention-days: 7 + organization-quality: + name: Verify organization module + if: github.event_name != 'workflow_dispatch' || github.event.inputs.attach_from_run == '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: "24" + cache: npm + cache-dependency-path: modules/openxyos/package-lock.json + - name: Install organization dependencies + working-directory: modules/openxyos + run: npm ci + - name: Check organization module + working-directory: modules/openxyos + run: npm run ci + - name: Check organization dependencies + working-directory: modules/openxyos + run: npm audit --registry=https://registry.npmjs.org + # Job-level `if` cannot read `matrix.*` (GitHub 422). Filter here so only # selected runners start — PRs → darwin-* + windows-*; dispatch honors `platforms`. select-platforms: @@ -134,7 +165,7 @@ jobs: package: name: "${{ matrix.plat }}" - needs: [frontend, select-platforms] + needs: [frontend, select-platforms, organization-quality] runs-on: ${{ matrix.os }} # Matrix runners are native for each plat; pin host detection so x64 Git Bash # on windows-11-arm does not mis-classify the job as windows-amd64 cross-build. @@ -177,7 +208,7 @@ jobs: - uses: actions/setup-node@v5 with: - node-version: "20" + node-version: "24" cache: npm cache-dependency-path: modules/openxyos/package-lock.json @@ -260,6 +291,10 @@ jobs: - name: Install Wails v3 CLI run: go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-beta.13 + - name: Test native desktop lifecycle and updater + working-directory: desktop/src + run: go test ./... + - name: Install NSIS if: runner.os == 'Windows' shell: pwsh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 199093df..b23978e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,7 +58,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "24" cache: npm cache-dependency-path: dashboard/package-lock.json diff --git a/Makefile b/Makefile index 3b73053c..811bf76f 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ REPO_ROOT := $(shell pwd) DASHBOARD_DIR := $(REPO_ROOT)/dashboard DASHBOARD_DEST := $(REPO_ROOT)/src/octop/dashboard DIST_DIR := $(REPO_ROOT)/dist +FRONTEND_NODE_OPTIONS ?= --max-old-space-size=4096 # PyPI repository (override: make publish PYPI_REPO=testpypi) PYPI_REPO ?= pypi @@ -98,7 +99,7 @@ build-frontend: @echo "[build-frontend] Installing npm dependencies..." cd $(DASHBOARD_DIR) && npm ci @echo "[build-frontend] Building dashboard (output: $(DASHBOARD_DEST))..." - cd $(DASHBOARD_DIR) && NODE_ENV=production NODE_OPTIONS="--max-old-space-size=2048" npm run build + cd $(DASHBOARD_DIR) && NODE_ENV=production NODE_OPTIONS="$(FRONTEND_NODE_OPTIONS)" npm run build @echo "[build-frontend] Done." .PHONY: build-wheel diff --git a/desktop/portable/bundle-org-sidecar.sh b/desktop/portable/bundle-org-sidecar.sh index d275c458..d688c518 100755 --- a/desktop/portable/bundle-org-sidecar.sh +++ b/desktop/portable/bundle-org-sidecar.sh @@ -17,7 +17,7 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # shellcheck source=desktop/portable/_common.sh source "${REPO_ROOT}/desktop/portable/_common.sh" -NODE_VERSION="${NODE_VERSION:-20.19.5}" +NODE_VERSION="${NODE_VERSION:-24.21.0}" NODE_BASE_URL="${NODE_BASE_URL:-https://nodejs.org/dist/v${NODE_VERSION}}" node_archive_name() { diff --git a/desktop/portable/verify_imports.py b/desktop/portable/verify_imports.py index 7f08cf8c..a5179a0c 100755 --- a/desktop/portable/verify_imports.py +++ b/desktop/portable/verify_imports.py @@ -27,6 +27,9 @@ "cryptography.fernet", "langchain_core", "langchain_openai", + "octop.infra.agents.middleware.system_message_cleanup", + "octop.infra.server", + "octop.api.app", ) @@ -117,11 +120,12 @@ def main() -> int: except Exception as exc: errors.append(f"import {mod}: {type(exc).__name__}: {exc}") - # Sanity: launch.py must stay importable as a file (no compile errors). + # Compile the launcher shipped alongside packages/, not this checker. + launcher = packages.parent / "launch.py" try: - ast.parse(Path(__file__).read_text(encoding="utf-8")) - except SyntaxError as exc: - errors.append(f"self-parse: {exc}") + ast.parse(launcher.read_text(encoding="utf-8"), filename=str(launcher)) + except (OSError, SyntaxError) as exc: + errors.append(f"launch.py: {exc}") if errors: print("verify_imports FAILED:", file=sys.stderr) diff --git a/modules/openxyos/backend/services/doc-parser.ts b/modules/openxyos/backend/services/doc-parser.ts index 8c322942..c2aed9f0 100644 --- a/modules/openxyos/backend/services/doc-parser.ts +++ b/modules/openxyos/backend/services/doc-parser.ts @@ -5,11 +5,15 @@ export async function extractText(filePath: string, fileType: string): Promise=20.19.0" }, @@ -1896,6 +1898,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1917,6 +1920,7 @@ "resolved": "https://registry.npmmirror.com/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1953,29 +1957,6 @@ "react": ">=16.8.0" } }, - "node_modules/@emnapi/core": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", - "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.3", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", @@ -2752,9 +2733,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", - "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -2771,13 +2752,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.3" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", - "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -2794,13 +2775,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.3" + "@img/sharp-libvips-darwin-x64": "1.3.4" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", - "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -2808,7 +2789,7 @@ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -2818,9 +2799,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", - "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -2835,9 +2816,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", - "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -2852,9 +2833,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", - "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], @@ -2869,9 +2850,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", - "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], @@ -2886,9 +2867,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", - "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], @@ -2903,9 +2884,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", - "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], @@ -2920,9 +2901,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", - "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], @@ -2937,9 +2918,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", - "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], @@ -2954,9 +2935,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", - "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], @@ -2971,9 +2952,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", - "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], @@ -2988,9 +2969,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", - "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], @@ -3007,13 +2988,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.3" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", - "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], @@ -3030,13 +3011,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.3" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", - "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], @@ -3053,13 +3034,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.3" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", - "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], @@ -3076,13 +3057,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.3" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", - "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], @@ -3099,13 +3080,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.3" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", - "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], @@ -3122,13 +3103,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.3" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", - "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], @@ -3145,13 +3126,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", - "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], @@ -3168,13 +3149,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", - "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, @@ -3189,9 +3170,9 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", - "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], @@ -3199,7 +3180,7 @@ "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -3209,9 +3190,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", - "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -3229,9 +3210,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", - "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -3249,9 +3230,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", - "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -4791,6 +4772,7 @@ "resolved": "https://registry.npmmirror.com/@types/express/-/express-5.0.6.tgz", "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -4929,6 +4911,7 @@ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5048,6 +5031,7 @@ "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.64.0", "@typescript-eslint/types": "8.64.0", @@ -5202,9 +5186,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5358,6 +5342,7 @@ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5393,6 +5378,7 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5506,13 +5492,10 @@ } }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", @@ -5617,13 +5600,13 @@ } }, "node_modules/axios": { - "version": "1.18.1", - "resolved": "https://registry.npmmirror.com/axios/-/axios-1.18.1.tgz", - "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } @@ -5835,9 +5818,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -5888,6 +5871,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.11.20", "caniuse-lite": "^1.0.30001810", @@ -5945,6 +5929,7 @@ "version": "1.1.2", "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, "license": "MIT" }, "node_modules/buffer-indexof-polyfill": { @@ -6228,31 +6213,16 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, "node_modules/concurrently": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", - "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.5.tgz", + "integrity": "sha512-YMZlvWrTPQEhjKp60XSmMGt60+pffbv6DyRnOMvVO98+J7aF8w8Ui9FhdcOwvSixc/kyR6SoVvmejcn/2lZBqg==", "dev": true, "license": "MIT", "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", - "shell-quote": "1.9.0", + "shell-quote": "1.12.0", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" @@ -6484,6 +6454,7 @@ "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -6941,6 +6912,7 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -7324,9 +7296,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz", - "integrity": "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==", + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", + "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -7738,6 +7710,7 @@ "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8007,15 +7980,6 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/exceljs/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/express": { "version": "4.22.2", "resolved": "https://registry.npmmirror.com/express/-/express-4.22.2.tgz", @@ -8140,9 +8104,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { @@ -8214,9 +8178,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { @@ -9017,9 +8981,9 @@ "license": "MIT" }, "node_modules/ip-address": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", - "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", + "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", "license": "MIT", "engines": { "node": ">= 12" @@ -9582,13 +9546,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/js-yaml/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/jsdom": { "version": "29.1.1", "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-29.1.1.tgz", @@ -9881,9 +9838,9 @@ } }, "node_modules/katex": { - "version": "0.16.47", - "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.47.tgz", - "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "version": "0.18.10", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.18.10.tgz", + "integrity": "sha512-/B6p9eY9DX7aHBfpkHdpirDTZ5QH9xTwL9y837PWuzuv41O5jFdqNQwVQEQsimY0/iVNkwBY4+4hMfhQ7d4Dbw==", "dev": true, "funding": [ "https://opencollective.com/katex", @@ -9891,20 +9848,20 @@ ], "license": "MIT", "dependencies": { - "commander": "^8.3.0" + "commander": "^15.0.0" }, "bin": { "katex": "cli.js" } }, "node_modules/katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=22.12.0" } }, "node_modules/keyv": { @@ -10486,19 +10443,17 @@ } }, "node_modules/mammoth": { - "version": "1.12.0", - "resolved": "https://registry.npmmirror.com/mammoth/-/mammoth-1.12.0.tgz", - "integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.13.0.tgz", + "integrity": "sha512-2cawcYFcP7ISfXbBcL9lmVgTNWo88v/52ND1gnywHtVT/pX0//puPnf0iMPV9ZgmuvNOE+zkIhDgZ9M2c2dDDg==", "license": "BSD-2-Clause", "dependencies": { "@xmldom/xmldom": "^0.8.6", "argparse": "~1.0.3", "base64-js": "^1.5.1", - "bluebird": "~3.4.0", "dingbat-to-unicode": "^1.0.1", "jszip": "^3.7.1", "lop": "^0.4.2", - "path-is-absolute": "^1.0.0", "underscore": "^1.13.1", "xmlbuilder": "^10.0.0" }, @@ -10699,14 +10654,13 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.3.0.tgz", - "integrity": "sha512-cjNbm3sttszgZeGfJR124D+jFEfkXCVAsoPBmFn9X7UxmDSFHWqE2CoEj0vrmSpuAFnqWR1Szcm9QTsiHr60Xw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz", + "integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", - "concat-stream": "^2.0.0", "type-is": "^1.6.18" }, "engines": { @@ -11129,6 +11083,7 @@ "resolved": "https://registry.npmmirror.com/pg/-/pg-8.23.0.tgz", "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", @@ -11386,9 +11341,9 @@ "license": "MIT" }, "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -11396,6 +11351,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/proxy-from-env": { @@ -11484,6 +11443,7 @@ "resolved": "https://registry.npmmirror.com/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11493,6 +11453,7 @@ "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -11588,9 +11549,9 @@ } }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -12145,9 +12106,9 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", - "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -12162,31 +12123,31 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.4", - "@img/sharp-darwin-x64": "0.35.4", - "@img/sharp-freebsd-wasm32": "0.35.4", - "@img/sharp-libvips-darwin-arm64": "1.3.3", - "@img/sharp-libvips-darwin-x64": "1.3.3", - "@img/sharp-libvips-linux-arm": "1.3.3", - "@img/sharp-libvips-linux-arm64": "1.3.3", - "@img/sharp-libvips-linux-ppc64": "1.3.3", - "@img/sharp-libvips-linux-riscv64": "1.3.3", - "@img/sharp-libvips-linux-s390x": "1.3.3", - "@img/sharp-libvips-linux-x64": "1.3.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", - "@img/sharp-libvips-linuxmusl-x64": "1.3.3", - "@img/sharp-linux-arm": "0.35.4", - "@img/sharp-linux-arm64": "0.35.4", - "@img/sharp-linux-ppc64": "0.35.4", - "@img/sharp-linux-riscv64": "0.35.4", - "@img/sharp-linux-s390x": "0.35.4", - "@img/sharp-linux-x64": "0.35.4", - "@img/sharp-linuxmusl-arm64": "0.35.4", - "@img/sharp-linuxmusl-x64": "0.35.4", - "@img/sharp-webcontainers-wasm32": "0.35.4", - "@img/sharp-win32-arm64": "0.35.4", - "@img/sharp-win32-ia32": "0.35.4", - "@img/sharp-win32-x64": "0.35.4" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" }, "peerDependenciesMeta": { "@types/node": { @@ -12231,9 +12192,9 @@ } }, "node_modules/shell-quote": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", - "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", "dev": true, "license": "MIT", "engines": { @@ -12353,9 +12314,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -12392,12 +12353,6 @@ "node": ">= 10.x" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, "node_modules/sql.js": { "version": "1.14.1", "resolved": "https://registry.npmmirror.com/sql.js/-/sql.js-1.14.1.tgz", @@ -12757,6 +12712,7 @@ "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -12928,6 +12884,7 @@ "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.28.0" }, @@ -13064,18 +13021,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13347,16 +13299,16 @@ } }, "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/vary": { @@ -13374,6 +13326,7 @@ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -14203,9 +14156,9 @@ } }, "node_modules/workbox-build/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/modules/openxyos/package.json b/modules/openxyos/package.json index 9a9bf822..84671b5b 100644 --- a/modules/openxyos/package.json +++ b/modules/openxyos/package.json @@ -11,7 +11,7 @@ "postinstall": "node scripts/patch-express-types.cjs", "typecheck": "tsc --noEmit", "lint": "eslint . --ext .ts,.tsx --max-warnings 0", - "test": "npm run test:db-persistence && npm run test:runtime-config && npm run test:cors-origins && npm run test:embed-trap && npm run test:hero-mascot && npm run test:r0-access-control && npm run test:module-settings && npm run test:llm-onboarding && npm run test:agent-studio && npm run test:freeos-ingest && npm run test:file-security && npm run test:download-token && npm run test:ws-ticket && npm run test:auth-provider && npm run test:pep && npm run test:audit-bundle && npm run test:tool-gateway && npm run test:v100", + "test": "npm run test:db-persistence && npm run test:runtime-config && npm run test:cors-origins && npm run test:embed-trap && npm run test:hero-mascot && npm run test:r0-access-control && npm run test:module-settings && npm run test:llm-onboarding && npm run test:agent-studio && npm run test:freeos-ingest && npm run test:file-security && npm run test:doc-parser && npm run test:download-token && npm run test:ws-ticket && npm run test:auth-provider && npm run test:pep && npm run test:audit-bundle && npm run test:tool-gateway && npm run test:v100", "test:db-persistence": "tsx scripts/test-db-persistence.ts", "test:runtime-config": "tsx scripts/test-runtime-config.ts", "test:cors-origins": "tsx scripts/test-cors-origins.ts", @@ -35,7 +35,8 @@ "test:module-settings": "tsx scripts/test-module-settings.ts", "test:agent-studio": "tsx scripts/test-agent-studio.ts", "test:llm-onboarding": "tsx scripts/test-llm-onboarding.ts", - "test:freeos-ingest": "tsx scripts/test-freeos-ingest.ts" + "test:freeos-ingest": "tsx scripts/test-freeos-ingest.ts", + "test:doc-parser": "tsx scripts/test-doc-parser.ts" }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -65,7 +66,7 @@ "sql.js": "^1.11.0", "ssh2": "^1.17.0", "unzipper": "^0.12.3", - "uuid": "^10.0.0", + "uuid": "^11.1.1", "ws": "^8.21.0" }, "devDependencies": { @@ -108,6 +109,13 @@ "node": ">=20.19" }, "overrides": { - "qs": "6.16.0" + "qs": "6.16.0", + "katex": "^0.18.2", + "exceljs": { + "uuid": "^11.1.1" + }, + "mammoth": { + "argparse": "^2.0.1" + } } } diff --git a/modules/openxyos/scripts/test-doc-parser.ts b/modules/openxyos/scripts/test-doc-parser.ts new file mode 100644 index 00000000..eb6791d3 --- /dev/null +++ b/modules/openxyos/scripts/test-doc-parser.ts @@ -0,0 +1,47 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import PDFDocument from "pdfkit"; +import { Document, Packer, Paragraph } from "docx"; +import ExcelJS from "exceljs"; +import { extractText } from "../backend/services/doc-parser"; + +async function main() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "freeos-doc-parser-")); + try { + const text = "FreeOS document import acceptance"; + const pdf = new PDFDocument(); + const chunks: Buffer[] = []; + const finished = new Promise((resolve, reject) => { + pdf.on("data", (chunk: Buffer) => chunks.push(chunk)); + pdf.on("end", () => resolve(Buffer.concat(chunks))); + pdf.on("error", reject); + }); + pdf.text(text); + pdf.end(); + const pdfPath = path.join(root, "sample.pdf"); + await fs.writeFile(pdfPath, await finished); + assert.ok((await extractText(pdfPath, ".pdf")).includes(text)); + + const docxPath = path.join(root, "sample.docx"); + const docx = new Document({ sections: [{ children: [new Paragraph(text)] }] }); + await fs.writeFile(docxPath, await Packer.toBuffer(docx)); + assert.equal(await extractText(docxPath, ".docx"), text); + + // Exercise ExcelJS with the patched, CommonJS-compatible UUID dependency. + const workbook = new ExcelJS.Workbook(); + workbook.addWorksheet("Import").addRow([text]); + const restored = new ExcelJS.Workbook(); + await restored.xlsx.load(await workbook.xlsx.writeBuffer()); + assert.equal(restored.getWorksheet("Import")?.getCell("A1").value, text); + console.log("PDF, DOCX and XLSX import/export acceptance passed"); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } +} + +main().catch((error: unknown) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/tests/unit/desktop/test_verify_imports.py b/tests/unit/desktop/test_verify_imports.py new file mode 100644 index 00000000..22a2a3c2 --- /dev/null +++ b/tests/unit/desktop/test_verify_imports.py @@ -0,0 +1,38 @@ +"""The portable smoke gate must inspect the launcher that users will run.""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +import pytest + + +@pytest.mark.parametrize("launcher", ["print('ready')\n", "def broken(\n", None]) +def test_shipped_launcher_is_checked(tmp_path: Path, monkeypatch, capsys, launcher): + checker = Path(__file__).parents[3] / "desktop/portable/verify_imports.py" + spec = importlib.util.spec_from_file_location("portable_verify_imports", checker) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + packages = tmp_path / "packages" + packages.mkdir() + requirements = tmp_path / "requirements.txt" + requirements.write_text("", encoding="utf-8") + if launcher is not None: + (tmp_path / "launch.py").write_text(launcher, encoding="utf-8") + monkeypatch.setattr(module, "_SMOKE_IMPORTS", ()) + monkeypatch.setattr(module.site, "addsitedir", lambda _: None) + monkeypatch.setattr(module, "_installed_version", lambda _: None) + monkeypatch.setattr( + sys, + "argv", + [str(checker), "--packages", str(packages), "--requirements", str(requirements)], + ) + result = module.main() + if launcher == "print('ready')\n": + assert result == 0 + else: + assert result == 1 + assert "launch.py:" in capsys.readouterr().err From 75e9e517268432208fb8ce44d57cb2f5e874eb03 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 21 Oct 2026 21:37:53 +0000 Subject: [PATCH 06/14] fix(release): close 0.1.0 CI and sidecar security blockers Refresh the openXYOS lock so npm ci resolves on Node 24 and require Node >=22.12. Wait for the SearchConfig source label before asserting it; that was the Node 24 dashboard failure while the spinner was still mounted. Bind the organization sidecar to 127.0.0.1 from the server default and every desktop launcher. Seed local accounts only under FREEOS_ORG_LOCAL_TEST, with a random password stored beside the database and a forced change on first sign-in. The UI no longer shows a shared demo password. Mark rc/a/b/dev tags as GitHub prereleases in the release, desktop attach, and FNOS attach workflows. Collapse the changelog to a single 0.1.0 section and stop README_CN from directing installs at PyPI octop. Order desktop version compare so a final release sorts above its rc. Co-authored-by: haoboy007-sudo --- .github/workflows/fnos-build-fpk.yml | 6 + .github/workflows/octop-desktop.yml | 9 ++ .github/workflows/release.yml | 6 + CHANGELOG.md | 28 ++-- README.md | 4 +- README_CN.md | 14 +- .../Settings/SearchConfig/index.test.tsx | 12 +- desktop/portable/bundle-org-sidecar.sh | 6 + .../src/build/windows/nsis/start-sidecar.ps1 | 8 +- desktop/src/download.go | 83 ++++++++++- desktop/src/download_test.go | 7 + desktop/src/process.go | 28 ++-- desktop/src/process_sidecar.go | 3 + desktop/src/process_sidecar_test.go | 6 + docs/adr/001-single-process-model.md | 2 +- modules/openxyos/backend/db.ts | 6 + modules/openxyos/backend/middleware.ts | 24 +++ modules/openxyos/backend/routes/auth.ts | 40 +++++ modules/openxyos/backend/seed.ts | 139 ++++++++++++++---- modules/openxyos/backend/server.ts | 2 +- .../openxyos/frontend/src/pages/AuthPage.tsx | 73 +++++++-- .../frontend/src/pages/OpenHomePage.tsx | 36 +---- modules/openxyos/frontend/src/stores/auth.ts | 40 ++++- modules/openxyos/package-lock.json | 51 +++---- modules/openxyos/package.json | 2 +- scripts/release_prerelease.py | 32 ++++ src/octop/modules/org_os/sidecar_launch.py | 1 + .../unit/desktop/test_openxyos_provisioner.py | 2 + tests/unit/test_release_prerelease.py | 27 ++++ tests/unit/test_sidecar_launch.py | 1 + 30 files changed, 549 insertions(+), 149 deletions(-) create mode 100644 scripts/release_prerelease.py create mode 100644 tests/unit/test_release_prerelease.py diff --git a/.github/workflows/fnos-build-fpk.yml b/.github/workflows/fnos-build-fpk.yml index 6786038e..f8c06e06 100644 --- a/.github/workflows/fnos-build-fpk.yml +++ b/.github/workflows/fnos-build-fpk.yml @@ -273,6 +273,10 @@ jobs: exit 1 fi + - name: Classify pre-release + id: channel + run: python3 scripts/release_prerelease.py "${{ needs.version.outputs.tag }}" >> "$GITHUB_OUTPUT" + - name: Attach fpk to GitHub Release uses: softprops/action-gh-release@v2 with: @@ -280,5 +284,7 @@ jobs: files: dist/*.fpk fail_on_unmatched_files: true generate_release_notes: false + prerelease: ${{ steps.channel.outputs.prerelease }} + make_latest: ${{ steps.channel.outputs.prerelease == 'true' && 'false' || 'true' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/octop-desktop.yml b/.github/workflows/octop-desktop.yml index 25633a9d..1c508e60 100644 --- a/.github/workflows/octop-desktop.yml +++ b/.github/workflows/octop-desktop.yml @@ -360,6 +360,8 @@ jobs: contents: write actions: read steps: + - uses: actions/checkout@v4 + - name: Resolve release tag id: rel run: | @@ -374,6 +376,10 @@ jobs: fi echo "tag=${tag}" >> "$GITHUB_OUTPUT" + - name: Classify pre-release + id: channel + run: python3 scripts/release_prerelease.py "${{ steps.rel.outputs.tag }}" >> "$GITHUB_OUTPUT" + # v8 required for archive:false artifacts. skip-decompress keeps the # uploaded .zip / .tar.gz / .dmg / .exe intact — default unzip turns portable # zips into directories, so files: release-assets/* would skip them. @@ -404,4 +410,7 @@ jobs: files: release-assets/* fail_on_unmatched_files: true # Upsert: works whether Release workflow already created the release. + # Keep rc/a/b/dev tags as prereleases so attaching assets cannot mark them latest. generate_release_notes: false + prerelease: ${{ steps.channel.outputs.prerelease }} + make_latest: ${{ steps.channel.outputs.prerelease == 'true' && 'false' || 'true' }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b23978e7..95ffa435 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -125,12 +125,18 @@ jobs: python3 scripts/release_download_links.py "$version" >> .release-notes.md fi + - name: Classify pre-release + id: channel + run: python3 scripts/release_prerelease.py "${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + - name: Create release uses: softprops/action-gh-release@v2 with: body_path: .release-notes.md files: dist/* generate_release_notes: true + prerelease: ${{ steps.channel.outputs.prerelease }} + make_latest: ${{ steps.channel.outputs.prerelease == 'true' && 'false' || 'true' }} # workflow_run / release:published are unreliable when Release itself was # started via GITHUB_TOKEN workflow_dispatch (Auto Tag). Explicitly cascade. diff --git a/CHANGELOG.md b/CHANGELOG.md index 15367e0a..09aa70ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,28 +4,32 @@ 格式遵循 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),版本号遵循 [语义化版本规范](https://semver.org/spec/v2.0.0.html)。 -## [Unreleased] +## [0.1.0] - TBD + +FreeOS **0.1.0** is an independent semver baseline. It does not track or align with Octop 1.0.x. Python wheels stay on GitHub Releases; this release does not publish the colliding `octop` name to PyPI. + +### Upgrade from 0.0.10 + +- In-app update and a manual reinstall both keep `~/.freeos` (legacy `~/.octop` is still recognized): sessions, local models, knowledge bases, organization data, and `config.json`. +- Desktop portable upgrades copy a SQLite backup under `$FREEOS_HOME/backups` before replacing the runtime, and checkpoint the WAL before the process restarts. +- A `config.json` that cannot be parsed is left in place and reported. It is not rewritten as an empty file. +- Roll back by installing the **v0.0.10** desktop package again, or restore a backup with `octop backup restore`. Do not point install scripts at PyPI `octop`; that package is upstream Octop, not this release. Use [GitHub Releases](https://github.com/XYAIStudio/FreeOS/releases) or `scripts/install.sh` (add `--from-pypi` only when you explicitly want upstream Octop). ### Changed -- Publish Python distributions as GitHub Release assets only; the FreeOS release workflow no longer publishes the colliding `octop` package name to PyPI, and install scripts default to the FreeOS GitHub source. +- Publish Python distributions as GitHub Release assets only. The release workflow no longer publishes to PyPI, and install scripts default to the FreeOS GitHub source. +- The bundled openXYOS sidecar listens on `127.0.0.1` by default. Local bootstrap accounts are created only when `FREEOS_ORG_LOCAL_TEST=1`, with a random password in `{data}/bootstrap-credentials.txt` that must be changed on first sign-in. The UI no longer embeds a shared demo password. ### Fixed - Remove checkpoint-resident system messages before model calls while preserving the active compiled prompt, preventing strict Qwen chat templates from failing when an older conversation contains a stale bootstrap message. - Detect models from an already-running Ollama daemon even when FreeOS-managed Ollama startup is disabled; model discovery never starts Ollama unless the user enabled it. -- Preserve `config.json` when it is malformed: startup and all configuration writers now report the file and parser position, avoid leaking stored credentials, and use atomic replacement instead of silently rebuilding an empty configuration. - -## [0.1.0] - 2026-10-07 +- Preserve `config.json` when it is malformed: startup and configuration writers now report the file and parser position, avoid leaking stored credentials, and use atomic replacement instead of silently rebuilding an empty configuration. Absorbed from upstream Octop #730 / #738. +- Checkpoint the SQLite WAL before the desktop process restarts so an in-app update does not drop the latest writes. Absorbed from upstream Octop #918. -### Changed - -- Publish the successor to 0.0.10 as 0.1.0, with Python distributions attached to GitHub Releases only. - -### Fixed +### Not in 0.1.0 -- Remove stale checkpoint system messages before model calls, detect models from an already-running Ollama daemon without starting it, and preserve malformed `config.json` files instead of silently replacing them. -- Improve desktop update safety by checkpointing SQLite WAL before restart. +Harness migration, cloud bridge default-on, default LDAP/SSO, Teams, and a whole-repo catch-up to Octop are out of scope. ## [0.0.10] - 2026-10-03 diff --git a/README.md b/README.md index 55df7923..237b8afa 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

Python 3.12+ - Version + Version License: MIT + Apache-2.0 Upstream: Octop Organization module: openXYOS @@ -136,7 +136,7 @@ Operator detail: [docs/asset-loop.md](docs/asset-loop.md). `FreeOS-desktop-windows-arm64-.exe`(ARM 电脑)。 2. 双击安装包。安装程序会放到「程序文件」并创建开始菜单和桌面快捷方式。 3. 打开 **FreeOS**。第一次启动会解压内置运行环境(可能要一两分钟),然后直接进入可用会话,无需先登录。 -4. 保存、导出或发布到账号时再在本机完成注册或登录(更广的服务或授权不挡起步)。侧栏 **Organization** 当前默认走宿主内组织能力;桌面 0.0.6 托管 Node + iframe(延续 0.0.3 过渡桥)、以及可选的完整 openXYOS Node 栈(`127.0.0.1:3780`,导出/同步/高级部署)都是 **过渡桥**,终态是把 openXYOS 迁入宿主。组织能力像工作室里另一间可独立布置的房间,与日常对话、助手协作同在一个 FreeOS,两种进入方式不捏成一种。 +4. 保存、导出或发布到账号时再在本机完成注册或登录(更广的服务或授权不挡起步)。侧栏 **Organization** 当前默认走宿主内组织能力;桌面安装包附带的托管 Node + iframe(延续 0.0.3 过渡桥)、以及可选的完整 openXYOS Node 栈(`127.0.0.1:3780`,导出/同步/高级部署)都是 **过渡桥**,终态是把 openXYOS 迁入宿主。组织能力像工作室里另一间可独立布置的房间,与日常对话、助手协作同在一个 FreeOS,两种进入方式不捏成一种。 数据目录默认是 `%USERPROFILE%\.freeos`(可用环境变量 `FREEOS_HOME` 改)。旧版 Octop 的 `~/.octop` 仍会被识别。卸载安装包会清空安装目录(默认为 `Program Files\FreeOS`)并删除快捷方式,但**不会**删除该用户数据目录;详见 [desktop/README.md](desktop/README.md#windows-uninstall)。 diff --git a/README_CN.md b/README_CN.md index fd7b3dcc..93505529 100644 --- a/README_CN.md +++ b/README_CN.md @@ -11,10 +11,9 @@

Python 3.12+ License: MIT - Version + Version 上游:Octop 组织模块:openXYOS - PyPI Code Style: Ruff GitHub stars GitHub forks @@ -248,14 +247,7 @@ curl -fsSL https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install 桌面客户端说明见 [desktop/README.md](desktop/README.md),飞牛打包指南见 [fnos/README.md](fnos/README.md)。独立商业组织站见 [docs/org-export.md](docs/org-export.md)(`freeos org export-standalone`,默认导出完整 openXYOS 源码树),不是默认安装器里的第二套 Node。 -**备选 — PyPI**(若你已自行管理 Python 环境): - -```bash -pip install octop -# 可选:pip install "octop[browser]" -# 可选本地 ONNX Embedding 模型缓存(设置 → 模型 → 本地):pip install "octop[local-embedding]" -# 仅下载目录模型到 ~/.octop/embedding_models;不用于对话,也不接入 Memory。 -``` +**不要**从 PyPI 安装 `octop`。PyPI 上的 `octop` 是上游 Octop,不是 FreeOS 0.1.0。桌面安装包从 [GitHub Releases](https://github.com/XYAIStudio/FreeOS/releases) 下载;终端安装用上面的安装脚本(默认拉取 FreeOS GitHub 源码)。只有在你明确要上游 Octop 时才加 `--from-pypi`。可选组件仍通过安装脚本传递,例如 `--extras browser`。 从源码用 uv 开发时: @@ -343,7 +335,7 @@ docker run -d \ | 远程一键安装 | Windows | `irm …/octop/install.ps1 \| iex` 或 `install.bat` | | 本地脚本 | macOS / Linux | `bash scripts/install.sh` | | 本地脚本 | Windows | `scripts\install.bat` 或 `install.ps1` | -| PyPI | 全平台 | `pip install octop` 或 `pip install "octop[browser]"` | +| GitHub Releases | 全平台 | 桌面安装包与 wheel。不要安装 PyPI 上的上游 `octop` | | Docker | 全平台 | `docker/docker-compose.yml` | 所有安装脚本均在 `~/.octop/venv` 创建隔离环境,并通过 `~/.octop/bin/octop` 包装 CLI,不会影响系统 Python。 diff --git a/dashboard/src/pages/Settings/SearchConfig/index.test.tsx b/dashboard/src/pages/Settings/SearchConfig/index.test.tsx index d743f693..4f1ebec2 100644 --- a/dashboard/src/pages/Settings/SearchConfig/index.test.tsx +++ b/dashboard/src/pages/Settings/SearchConfig/index.test.tsx @@ -35,17 +35,21 @@ describe("", () => { api.listEnvs.mockResolvedValue(envResp([])); render(); - await waitFor(() => expect(api.listEnvs).toHaveBeenCalled()); - expect(screen.getByText("当前搜索源:内置搜索")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText("当前搜索源:内置搜索")).toBeInTheDocument(); + }); + expect(api.listEnvs).toHaveBeenCalled(); }); it("shows the configured provider as the current search source", async () => { api.listEnvs.mockResolvedValue(envResp(["TAVILY_API_KEY"])); render(); - await waitFor(() => expect(api.listEnvs).toHaveBeenCalled()); - expect(screen.getByText("当前搜索源:Tavily")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText("当前搜索源:Tavily")).toBeInTheDocument(); + }); + expect(api.listEnvs).toHaveBeenCalled(); }); }); diff --git a/desktop/portable/bundle-org-sidecar.sh b/desktop/portable/bundle-org-sidecar.sh index d688c518..f8172c22 100755 --- a/desktop/portable/bundle-org-sidecar.sh +++ b/desktop/portable/bundle-org-sidecar.sh @@ -213,11 +213,14 @@ ENV fi export NODE_ENV=production export PORT +export HOST=127.0.0.1 export DB_DIALECT=sqlite export DATABASE_PATH="${DATA_DIR}/xiongyuan.db" export AIR_GAP_MODE=true export ALLOW_PUBLIC_REGISTRATION="${ALLOW_PUBLIC_REGISTRATION:-false}" export SEED_DEMO_DATA="${SEED_DEMO_DATA:-false}" +export FREEOS_ORG_INTEGRATED="${FREEOS_ORG_INTEGRATED:-1}" +export FREEOS_ORG_LOCAL_TEST="${FREEOS_ORG_LOCAL_TEST:-1}" export JWT_SECRET COOKIE_SECRET export CORS_ORIGIN="${CORS_ORIGIN:-http://127.0.0.1:8088,http://localhost:8088,http://127.0.0.1:18900,http://localhost:18900}" cd "${APP}" @@ -252,11 +255,14 @@ if not defined COOKIE_SECRET ( >> "%FREEOS_ORG_DATA%\sidecar.env" echo COOKIE_SECRET=%COOKIE_SECRET% set "NODE_ENV=production" set "PORT=%FREEOS_ORG_SIDECAR_PORT%" +set "HOST=127.0.0.1" set "DB_DIALECT=sqlite" set "DATABASE_PATH=%FREEOS_ORG_DATA%\xiongyuan.db" set "AIR_GAP_MODE=true" if not defined ALLOW_PUBLIC_REGISTRATION set "ALLOW_PUBLIC_REGISTRATION=false" if not defined SEED_DEMO_DATA set "SEED_DEMO_DATA=false" +if not defined FREEOS_ORG_INTEGRATED set "FREEOS_ORG_INTEGRATED=1" +if not defined FREEOS_ORG_LOCAL_TEST set "FREEOS_ORG_LOCAL_TEST=1" if not defined CORS_ORIGIN set "CORS_ORIGIN=http://127.0.0.1:8088,http://localhost:8088,http://127.0.0.1:18900,http://localhost:18900" cd /d "%APP%" "%NODE%" --import tsx backend/server.ts diff --git a/desktop/src/build/windows/nsis/start-sidecar.ps1 b/desktop/src/build/windows/nsis/start-sidecar.ps1 index 81cb9563..84aed5d9 100644 --- a/desktop/src/build/windows/nsis/start-sidecar.ps1 +++ b/desktop/src/build/windows/nsis/start-sidecar.ps1 @@ -234,6 +234,7 @@ if (-not $ingest) { $ingest = [guid]::NewGuid().ToString('N') + [guid]::NewGuid( $env:NODE_ENV = 'production' $env:PORT = '3780' +$env:HOST = '127.0.0.1' $env:DB_DIALECT = 'sqlite' $env:DATABASE_PATH = Join-Path $data 'xiongyuan.db' $env:AIR_GAP_MODE = 'true' @@ -249,6 +250,8 @@ $env:CORS_ORIGIN = 'http://127.0.0.1:8088,http://localhost:8088,http://127.0.0.1 $env:FREEOS_HOME = $freeosHome $env:OCTOP_HOME = $freeosHome $env:FREEOS_ORG_SIDECAR_PORT = '3780' +$env:FREEOS_ORG_INTEGRATED = '1' +$env:FREEOS_ORG_LOCAL_TEST = '1' $compiled = Join-Path $app 'backend-dist\server.js' if (Test-Path -LiteralPath $compiled) { $argv = @('backend-dist/server.js') @@ -273,6 +276,7 @@ if ($app -eq $live) { $layout = 'top-level' } "argv=$($argv -join ' ')" "NODE_ENV=$($env:NODE_ENV)" "PORT=$($env:PORT)" + "HOST=$($env:HOST)" "JWT_SECRET set=$([bool]$env:JWT_SECRET)" "COOKIE_SECRET set=$([bool]$env:COOKIE_SECRET)" "CORS_ORIGIN=$($env:CORS_ORIGIN)" @@ -294,10 +298,10 @@ $psi.Arguments = [string]::Join(' ', $quoted) $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true foreach ($name in @( - 'NODE_ENV', 'PORT', 'DB_DIALECT', 'DATABASE_PATH', 'AIR_GAP_MODE', + 'NODE_ENV', 'PORT', 'HOST', 'DB_DIALECT', 'DATABASE_PATH', 'AIR_GAP_MODE', 'SEED_DEMO_DATA', 'ALLOW_PUBLIC_REGISTRATION', 'JWT_SECRET', 'COOKIE_SECRET', 'FREEOS_INGEST_TOKEN', 'CORS_ORIGIN', 'FREEOS_HOME', 'OCTOP_HOME', - 'FREEOS_ORG_SIDECAR_PORT' + 'FREEOS_ORG_SIDECAR_PORT', 'FREEOS_ORG_INTEGRATED', 'FREEOS_ORG_LOCAL_TEST' )) { $val = [Environment]::GetEnvironmentVariable($name, 'Process') if ($null -ne $val -and $val -ne '') { diff --git a/desktop/src/download.go b/desktop/src/download.go index 194f4db2..49c2122d 100644 --- a/desktop/src/download.go +++ b/desktop/src/download.go @@ -546,7 +546,88 @@ func compareVersions(left, right string) int { return 1 } } - return 0 + return compareVersionSuffix(versionSuffix(left), versionSuffix(right)) +} + +// versionSuffix is the non-numeric tail of a PEP 440-ish version (rc1, a1, dev1). +func versionSuffix(version string) string { + var suffix strings.Builder + for _, segment := range strings.Split(version, ".") { + index := 0 + for index < len(segment) && segment[index] >= '0' && segment[index] <= '9' { + index++ + } + if index < len(segment) { + suffix.WriteString(segment[index:]) + } + } + return suffix.String() +} + +func compareVersionSuffix(left, right string) int { + if left == right { + return 0 + } + // 0.1.0rc1 < 0.1.0. The Python update client does full PEP 440 ordering; + // this tie-break only keeps a downloaded pre-release behind its final. + if left == "" { + return 1 + } + if right == "" { + return -1 + } + leftKind, leftN, leftOK := preReleaseRank(left) + rightKind, rightN, rightOK := preReleaseRank(right) + if leftOK && rightOK { + if leftKind != rightKind { + if leftKind < rightKind { + return -1 + } + return 1 + } + if leftN < rightN { + return -1 + } + if leftN > rightN { + return 1 + } + return 0 + } + if left < right { + return -1 + } + return 1 +} + +func preReleaseRank(suffix string) (kind int, n int, ok bool) { + for _, item := range []struct { + prefix string + kind int + }{ + {"alpha", 1}, + {"beta", 2}, + {"preview", 2}, + {"pre", 2}, + {"rc", 3}, + {"dev", 0}, + {"a", 1}, + {"b", 2}, + {"c", 2}, + } { + if !strings.HasPrefix(suffix, item.prefix) { + continue + } + rest := strings.TrimLeft(suffix[len(item.prefix):], "._-") + if rest == "" { + return item.kind, 0, true + } + value, err := strconv.Atoi(rest) + if err != nil { + return 0, 0, false + } + return item.kind, value, true + } + return 0, 0, false } func versionPart(segment string) int { diff --git a/desktop/src/download_test.go b/desktop/src/download_test.go index aeb62c9e..34a70e1e 100644 --- a/desktop/src/download_test.go +++ b/desktop/src/download_test.go @@ -693,6 +693,13 @@ func TestCompareVersions(t *testing.T) { {"0.9.31", "0.9.32", -1}, {"1.0", "1.0.0", 0}, {"0.9.32rc1", "0.9.31", 1}, + {"0.1.0rc1", "0.0.10", 1}, + {"0.1.0rc1", "0.1.0", -1}, + {"0.1.0", "0.1.0rc1", 1}, + {"0.1.0rc1", "0.1.0rc2", -1}, + {"0.1.0rc2", "0.1.0rc10", -1}, + {"0.1.0a1", "0.1.0rc1", -1}, + {"0.1.0.dev1", "0.1.0a1", -1}, } { if got := compareVersions(test.left, test.right); got != test.want { t.Fatalf("compareVersions(%q, %q) = %d, want %d", test.left, test.right, got, test.want) diff --git a/desktop/src/process.go b/desktop/src/process.go index fc85cfac..e1beda64 100644 --- a/desktop/src/process.go +++ b/desktop/src/process.go @@ -12,10 +12,18 @@ import ( ) func mustEnv(cmd *exec.Cmd, extra map[string]string) { - cmd.Env = os.Environ() + filtered := make([]string, 0, len(os.Environ())+len(extra)) + for _, item := range os.Environ() { + key, _, _ := strings.Cut(item, "=") + if _, override := extra[key]; override { + continue + } + filtered = append(filtered, item) + } for key, value := range extra { - cmd.Env = append(cmd.Env, key+"="+value) + filtered = append(filtered, key+"="+value) } + cmd.Env = filtered } const defaultSidecarPort = 3780 @@ -23,14 +31,14 @@ const defaultSidecarPort = 3780 func hostLaunchEnv(root string, port int) map[string]string { home := productHome() env := map[string]string{ - "FREEOS_HOME": home, - "OCTOP_HOME": home, - "OCTOP_GREEN_PACKAGES": filepath.Join(root, "packages"), - "PYTHONNOUSERSITE": "1", - "PYTHONUTF8": "1", - "PYTHONIOENCODING": "utf-8", - "PYTHONPATH": "", - "FREEOS_ORG_ENABLE": "1", + "FREEOS_HOME": home, + "OCTOP_HOME": home, + "OCTOP_GREEN_PACKAGES": filepath.Join(root, "packages"), + "PYTHONNOUSERSITE": "1", + "PYTHONUTF8": "1", + "PYTHONIOENCODING": "utf-8", + "PYTHONPATH": "", + "FREEOS_ORG_ENABLE": "1", // Desktop always claims the openXYOS module. Missing Node is a // packaging/startup failure, not a reason to unset this flag. "FREEOS_ORG_INTEGRATED": "1", diff --git a/desktop/src/process_sidecar.go b/desktop/src/process_sidecar.go index 76684392..e6540a18 100644 --- a/desktop/src/process_sidecar.go +++ b/desktop/src/process_sidecar.go @@ -304,6 +304,7 @@ func sidecarLaunchEnv(home string, dashboardPort int) (map[string]string, error) return map[string]string{ "NODE_ENV": "production", "PORT": strconv.Itoa(defaultSidecarPort), + "HOST": "127.0.0.1", "DB_DIALECT": "sqlite", "DATABASE_PATH": filepath.Join(sidecarDataDir(home), "xiongyuan.db"), "AIR_GAP_MODE": "true", @@ -316,6 +317,8 @@ func sidecarLaunchEnv(home string, dashboardPort int) (map[string]string, error) "FREEOS_HOME": home, "OCTOP_HOME": home, "FREEOS_ORG_SIDECAR_PORT": strconv.Itoa(defaultSidecarPort), + "FREEOS_ORG_INTEGRATED": "1", + "FREEOS_ORG_LOCAL_TEST": "1", }, nil } diff --git a/desktop/src/process_sidecar_test.go b/desktop/src/process_sidecar_test.go index 62fa0f3b..52265cda 100644 --- a/desktop/src/process_sidecar_test.go +++ b/desktop/src/process_sidecar_test.go @@ -58,6 +58,12 @@ func TestSidecarLaunchEnvPointsAtHomeDatabase(t *testing.T) { if !strings.Contains(env["CORS_ORIGIN"], "http://[::1]:3780") { t.Fatalf("CORS_ORIGIN missing ipv6 self origin: %q", env["CORS_ORIGIN"]) } + if env["HOST"] != "127.0.0.1" { + t.Fatalf("HOST=%q want 127.0.0.1", env["HOST"]) + } + if env["FREEOS_ORG_LOCAL_TEST"] != "1" { + t.Fatalf("FREEOS_ORG_LOCAL_TEST=%q", env["FREEOS_ORG_LOCAL_TEST"]) + } } func TestSidecarReadyRequiresFrontendBuild(t *testing.T) { diff --git a/docs/adr/001-single-process-model.md b/docs/adr/001-single-process-model.md index aa4539aa..09fbfcbd 100644 --- a/docs/adr/001-single-process-model.md +++ b/docs/adr/001-single-process-model.md @@ -15,7 +15,7 @@ Everything runs in a single Python process served by uvicorn. There is no extern ## Rationale -- **Self-hosted target:** Operators install with `pip install octop && octop init`. Adding Redis or a process supervisor doubles the ops burden for the primary audience. +- **Self-hosted target:** Operators install with `pip install octop && octop init`. Adding Redis or a process supervisor doubles the ops burden for the primary audience. Historical note (2026-10): that command describes the upstream-era install path. FreeOS 0.1.0 is installed from GitHub Releases or `scripts/install.sh`, not from the PyPI `octop` package. - **Workload profile:** Agent calls are LLM-bound (seconds to minutes of I/O wait). Python's asyncio handles this fan-out efficiently without threads or processes. - **SQLite is sufficient:** Concurrent writes are rare (one writer per agent at a time); WAL mode handles the load. Migrating to Postgres requires only a new `SqlitePool` implementation. - **Restart semantics are simple:** `OctopServer.start()` rebuilds the entire runtime tree from the SQLite file. No external state to reconcile. diff --git a/modules/openxyos/backend/db.ts b/modules/openxyos/backend/db.ts index 4bc0c5a2..ace2776f 100644 --- a/modules/openxyos/backend/db.ts +++ b/modules/openxyos/backend/db.ts @@ -49,6 +49,7 @@ export async function initDatabase(): Promise { role TEXT DEFAULT 'user', tenant_id INTEGER DEFAULT 1, token_version INTEGER NOT NULL DEFAULT 0, + must_change_password INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS companies ( @@ -327,6 +328,11 @@ export async function initDatabase(): Promise { } catch (error: any) { if (!error.message?.includes("duplicate column")) throw error; } + try { + db.run("ALTER TABLE users ADD COLUMN must_change_password INTEGER NOT NULL DEFAULT 0"); + } catch (error: any) { + if (!error.message?.includes("duplicate column")) throw error; + } saveDb(); console.log("[数据库] Schema 初始化完成(含扩展表)"); diff --git a/modules/openxyos/backend/middleware.ts b/modules/openxyos/backend/middleware.ts index 5cb566f1..49727a4d 100644 --- a/modules/openxyos/backend/middleware.ts +++ b/modules/openxyos/backend/middleware.ts @@ -25,6 +25,15 @@ export interface AuthRequest extends Request { user?: AuthUser; } +function passwordChangeAllowed(req: AuthRequest): boolean { + const path = (req.originalUrl || req.url || "").split("?")[0]; + return ( + path === "/api/auth/change-password" + || path === "/api/auth/me" + || path === "/api/auth/revoke" + ); +} + export async function authenticate(req: AuthRequest, res: Response, next: NextFunction) { const authHeader = req.headers.authorization; if (!authHeader?.startsWith("Bearer ")) { @@ -37,6 +46,21 @@ export async function authenticate(req: AuthRequest, res: Response, next: NextFu return res.status(401).json({ success: false, error: result.error || localizedError(req, "登录已过期", "Your session has expired") }); } req.user = result.user; + const gate = dbGet( + "SELECT must_change_password AS flag FROM users WHERE id = ?", + [result.user.id], + ) as { flag?: number } | undefined; + if (Number(gate?.flag) === 1 && !passwordChangeAllowed(req)) { + return res.status(403).json({ + success: false, + code: "PASSWORD_CHANGE_REQUIRED", + error: localizedError( + req, + "请先修改本机引导密码", + "Change the local bootstrap password before continuing", + ), + }); + } runWithRequestContext( { tenantId: result.user.tenant_id, userId: result.user.id }, next, diff --git a/modules/openxyos/backend/routes/auth.ts b/modules/openxyos/backend/routes/auth.ts index 37020015..357098c0 100644 --- a/modules/openxyos/backend/routes/auth.ts +++ b/modules/openxyos/backend/routes/auth.ts @@ -94,11 +94,16 @@ authRoutes.post("/login", async (req, res) => { return res.status(status).json({ success: false, error: authError(req, result.error || "") }); } + const gate = dbGet( + "SELECT must_change_password AS flag FROM users WHERE email = ?", + [email], + ) as { flag?: number } | undefined; res.json({ success: true, data: { user: result.user, tokens: result.tokens, + must_change_password: Number(gate?.flag) === 1, }, }); } catch (err: unknown) { @@ -136,6 +141,41 @@ authRoutes.post("/refresh", async (req, res) => { } }); +// POST /change-password — required before other APIs when the bootstrap flag is set +authRoutes.post("/change-password", authenticate, (req: AuthRequest, res) => { + try { + const current = String(req.body?.current_password || ""); + const next = String(req.body?.new_password || ""); + if (next.length < 12) { + return res.status(400).json({ + success: false, + error: localizedError(req, "新密码至少 12 位", "New password must be at least 12 characters"), + }); + } + const row = dbGet("SELECT password_hash FROM users WHERE id = ?", [req.user!.id]) as + | { password_hash: string } + | undefined; + if (!row || !bcrypt.compareSync(current, row.password_hash)) { + return res.status(401).json({ + success: false, + error: localizedError(req, "当前密码不正确", "Current password is incorrect"), + }); + } + dbRun( + "UPDATE users SET password_hash = ?, must_change_password = 0 WHERE id = ?", + [bcrypt.hashSync(next, 10), req.user!.id], + ); + res.json({ success: true }); + } catch (err: unknown) { + const tid = logError("POST /change-password", err); + res.status(500).json({ + success: false, + error: localizedError(req, "暂时无法修改密码", "Unable to change the password right now"), + traceId: tid, + }); + } +}); + // GET /me — 当前用户信息 authRoutes.get("/me", authenticate, (req: AuthRequest, res) => { const user = dbGet("SELECT id, email, nickname, role, tenant_id FROM users WHERE id = ?", [req.user!.id]); diff --git a/modules/openxyos/backend/seed.ts b/modules/openxyos/backend/seed.ts index 77176a70..638fa01b 100644 --- a/modules/openxyos/backend/seed.ts +++ b/modules/openxyos/backend/seed.ts @@ -1,4 +1,7 @@ import bcrypt from "bcryptjs"; +import { randomBytes } from "crypto"; +import fs from "fs"; +import path from "path"; import { dbAll, dbGet, dbRun, getDb, saveDb } from "./db"; function requiredSeedSecret(name: "SEED_ADMIN_PASSWORD" | "SEED_DEMO_PASSWORD"): string { @@ -13,12 +16,79 @@ function seedAdminEmail(): string { return process.env.SEED_ADMIN_EMAIL?.trim() || "admin@example.com"; } -const LOCAL_BOOTSTRAP_PASSWORD = "openxyos-demo-2026"; - -function localBootstrapPassword(name: "SEED_ADMIN_PASSWORD" | "SEED_DEMO_PASSWORD"): string { +function explicitSeedPassword(name: "SEED_ADMIN_PASSWORD" | "SEED_DEMO_PASSWORD"): string | null { const value = process.env[name]?.trim(); - if (value && value.length >= 12) return value; - return LOCAL_BOOTSTRAP_PASSWORD; + if (!value) return null; + if (value.length < 12) { + throw new Error(`${name} must be at least 12 characters`); + } + return value; +} + +function bootstrapCredentialsPath(): string { + const dbPath = process.env.DATABASE_PATH || path.join(__dirname, "data/xiongyuan.db"); + return path.join(path.dirname(dbPath), "bootstrap-credentials.txt"); +} + +function readBootstrapPassword(file: string): string | null { + if (!fs.existsSync(file)) return null; + const match = /^password=(.+)$/m.exec(fs.readFileSync(file, "utf8")); + const value = match?.[1]?.trim() ?? ""; + return value.length >= 12 ? value : null; +} + +/** Random local password, stored once beside the database. Never logged. */ +function generatedBootstrapPassword(): { password: string; created: boolean } { + const file = bootstrapCredentialsPath(); + const existing = readBootstrapPassword(file); + if (existing) return { password: existing, created: false }; + const password = randomBytes(24).toString("base64url"); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync( + file, + [ + "# Local bootstrap credentials for this machine only.", + "# Sign in once, then change the password. Do not commit this file.", + `password=${password}`, + "", + ].join("\n"), + { encoding: "utf8", mode: 0o600 }, + ); + try { + fs.chmodSync(file, 0o600); + } catch { + // Windows ACLs are not POSIX modes; the file still stays under the user profile. + } + console.log(`[seed] local bootstrap credentials written to ${file}`); + return { password, created: true }; +} + +function upsertBootstrapUser(opts: { + email: string; + password: string; + nickname: string; + role: string; + tenantId: number; + mustChange: boolean; + rotate: boolean; +}): void { + const hash = bcrypt.hashSync(opts.password, 10); + const existing = dbGet("SELECT id FROM users WHERE email = ?", [opts.email]); + if (!existing) { + dbRun( + "INSERT INTO users (email, password_hash, nickname, role, tenant_id, must_change_password) VALUES (?, ?, ?, ?, ?, ?)", + [opts.email, hash, opts.nickname, opts.role, opts.tenantId, opts.mustChange ? 1 : 0], + ); + console.log(`[seed] local bootstrap account ${opts.email}`); + return; + } + if (opts.rotate) { + dbRun( + "UPDATE users SET password_hash = ?, must_change_password = 1 WHERE email = ?", + [hash, opts.email], + ); + console.log(`[seed] rotated bootstrap password for ${opts.email}; change it on next sign-in`); + } } /** First-run local accounts when demo seed is off (FreeOS desktop sidecar). */ @@ -35,30 +105,42 @@ export function ensureLocalBootstrap(): void { "UPDATE tenants SET status = 'active' WHERE id IN (1, 2) AND status NOT IN ('active', 'trial')", ); - const demoHash = bcrypt.hashSync(localBootstrapPassword("SEED_DEMO_PASSWORD"), 10); - const adminHash = bcrypt.hashSync(localBootstrapPassword("SEED_ADMIN_PASSWORD"), 10); + const demoExplicit = explicitSeedPassword("SEED_DEMO_PASSWORD"); + const adminExplicit = explicitSeedPassword("SEED_ADMIN_PASSWORD"); + const generated = demoExplicit && adminExplicit ? null : generatedBootstrapPassword(); + const demoPassword = demoExplicit ?? generated!.password; + const adminPassword = adminExplicit ?? generated!.password; + const rotateGenerated = Boolean(generated?.created); const adminEmail = seedAdminEmail(); - if (!dbGet("SELECT id FROM users WHERE email = ?", ["demo@demo.com"])) { - dbRun( - "INSERT INTO users (email, password_hash, nickname, role, tenant_id) VALUES (?, ?, ?, 'admin', 2)", - ["demo@demo.com", demoHash, "管理员"], - ); - console.log("[seed] local bootstrap admin demo@demo.com"); - } - if (!dbGet("SELECT id FROM users WHERE email = ?", ["user@demo.com"])) { - dbRun( - "INSERT INTO users (email, password_hash, nickname, role, tenant_id) VALUES (?, ?, ?, 'user', 2)", - ["user@demo.com", demoHash, "同事"], - ); - console.log("[seed] local bootstrap user user@demo.com"); - } - if (adminEmail !== "demo@demo.com" && !dbGet("SELECT id FROM users WHERE email = ?", [adminEmail])) { - dbRun( - "INSERT INTO users (email, password_hash, nickname, role, tenant_id) VALUES (?, ?, ?, 'super_admin', 1)", - [adminEmail, adminHash, "超级管理员"], - ); - console.log(`[seed] local bootstrap super admin ${adminEmail}`); + upsertBootstrapUser({ + email: "demo@demo.com", + password: demoPassword, + nickname: "管理员", + role: "admin", + tenantId: 2, + mustChange: demoExplicit === null, + rotate: demoExplicit === null && rotateGenerated, + }); + upsertBootstrapUser({ + email: "user@demo.com", + password: demoPassword, + nickname: "同事", + role: "user", + tenantId: 2, + mustChange: demoExplicit === null, + rotate: demoExplicit === null && rotateGenerated, + }); + if (adminEmail !== "demo@demo.com") { + upsertBootstrapUser({ + email: adminEmail, + password: adminPassword, + nickname: "超级管理员", + role: "super_admin", + tenantId: 1, + mustChange: adminExplicit === null, + rotate: adminExplicit === null && rotateGenerated, + }); } } @@ -79,8 +161,7 @@ export function seedDatabase() { return; } if (process.env.SEED_DEMO_DATA !== "true") { - console.log("[seed] Demo data is disabled. Seeding local first-login accounts only."); - ensureLocalBootstrap(); + console.log("[seed] Demo data is disabled. Local accounts are seeded only when FREEOS_ORG_LOCAL_TEST=1."); return; } diff --git a/modules/openxyos/backend/server.ts b/modules/openxyos/backend/server.ts index c8b9c1c5..39052c49 100644 --- a/modules/openxyos/backend/server.ts +++ b/modules/openxyos/backend/server.ts @@ -282,7 +282,7 @@ app.get("/admin/database", (req, res) => { setupWebSocket(server); - server.listen(PORT, process.env.HOST || '0.0.0.0', () => { + server.listen(PORT, process.env.HOST || '127.0.0.1', () => { console.log(`\n 🌐 openXYOS Community — Human + Agent OS`); console.log(` ─────────────────────────────────────`); console.log(` Server: http://localhost:${PORT}`); diff --git a/modules/openxyos/frontend/src/pages/AuthPage.tsx b/modules/openxyos/frontend/src/pages/AuthPage.tsx index 15e27d01..7700352e 100644 --- a/modules/openxyos/frontend/src/pages/AuthPage.tsx +++ b/modules/openxyos/frontend/src/pages/AuthPage.tsx @@ -8,7 +8,7 @@ const R = "[border-radius:1.5px]"; export default function AuthPage() { const navigate = useNavigate(); - const { login, register } = useAuthStore(); + const { login, register, changePassword } = useAuthStore(); const { isEnglish } = useLocale(); const tx = (zh: string, en: string) => (isEnglish ? en : zh); const [tab, setTab] = useState<"login" | "register">("login"); @@ -18,6 +18,9 @@ export default function AuthPage() { const [error, setError] = useState(""); const [loading, setLoading] = useState(false); const [agreed, setAgreed] = useState(false); + const [mustChange, setMustChange] = useState(false); + const [currentPassword, setCurrentPassword] = useState(""); + const [nextPassword, setNextPassword] = useState(""); const enterAfterAuthentication = async () => { try { @@ -34,14 +37,22 @@ export default function AuthPage() { } }; - const enterTestAccount = async () => { + const handlePasswordChange = async (e: React.FormEvent) => { + e.preventDefault(); setLoading(true); setError(""); try { - await login("demo@demo.com", "openxyos-demo-2026"); + if (nextPassword.length < 12) { + setError(tx("新密码至少 12 位", "New password must be at least 12 characters.")); + return; + } + await changePassword(currentPassword, nextPassword); + setMustChange(false); + setCurrentPassword(""); + setNextPassword(""); await enterAfterAuthentication(); } catch (err: any) { - setError(err.message || tx("测试账号暂时不可用", "The test account is unavailable.")); + setError(err.message || tx("暂时无法修改密码", "Unable to change the password right now.")); } finally { setLoading(false); } @@ -53,7 +64,13 @@ export default function AuthPage() { setError(""); try { if (tab === "login") { - await login(email, password); + const needsChange = await login(email, password); + if (needsChange) { + setMustChange(true); + setCurrentPassword(password); + setPassword(""); + return; + } } else { if (!agreed) { setError( @@ -186,19 +203,11 @@ export default function AuthPage() {
{tx( - "试用、布置这里的流程;钥匙只开这扇门。", - "Try and arrange the workflows here. This key opens only this door.", + "本机引导口令只写在数据目录的 bootstrap-credentials.txt,登录后必须修改。页面不再内置演示口令。", + "The local bootstrap password is written only to bootstrap-credentials.txt in the data directory. Change it after signing in. This page does not embed a demo password.", )} -

@@ -213,6 +222,39 @@ export default function AuthPage() { ))}
+ {mustChange ? ( +
void handlePasswordChange(e)}> +

+ {tx( + "这是本机引导账号。请先设置一个新密码,然后才能继续。", + "This is a local bootstrap account. Set a new password before continuing.", + )} +

+
+ + setNextPassword(e.target.value)} + placeholder={tx("至少 12 位", "At least 12 characters")} + required + autoComplete="new-password" + className={`w-full px-3.5 py-2.5 border-[1.5px] border-border ${R} text-sm bg-bg-card text-text outline-none transition-all focus:border-primary`} + /> +
+ {error &&

{error}

} + +
+ ) : (
diff --git a/modules/openxyos/frontend/src/pages/OpenHomePage.tsx b/modules/openxyos/frontend/src/pages/OpenHomePage.tsx index f160ae10..42d9021e 100644 --- a/modules/openxyos/frontend/src/pages/OpenHomePage.tsx +++ b/modules/openxyos/frontend/src/pages/OpenHomePage.tsx @@ -1,8 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { ArrowRight, Blocks, Bot, Braces, Building2, Check, ChevronDown, CircleDot, Code2, Copy, Eye, EyeOff, GitBranch, Github, KeyRound, Layers3, LockKeyhole, Menu, MessageSquareMore, Network, Play, ShieldCheck, Sparkles, Terminal, Users, X } from "lucide-react"; -import { useAuthStore } from "../stores/auth"; -import { authFetch } from "../api/authFetch"; import { EXPERIENCE_MODELS, type ExperienceModelId } from "../llm-providers"; import "../openxyos.css"; import AgentJourneyDemo from "../components/AgentJourneyDemo"; @@ -385,37 +383,19 @@ function HeroRippleField() { export default function OpenHomePage() { const { isEnglish } = useLocale(); const tx = (zh: string, en: string) => isEnglish ? en : zh; - const navigate = useNavigate(), { login } = useAuthStore(); + const navigate = useNavigate(); const [demoOpen, setDemoOpen] = useState(false), [demoPhase, setDemoPhase] = useState<"model" | "account">("model"), [loginState, setLoginState] = useState<"idle" | "admin" | "user">("idle"), [loginError, setLoginError] = useState(""), [copied, setCopied] = useState(false), [navOpen, setNavOpen] = useState(false); const [providerId, setProviderId] = useState("ollama"), [apiKey, setApiKey] = useState(""), [showKey, setShowKey] = useState(false); const openDemo = (phase: "model" | "account") => { setDemoPhase(phase); setLoginError(""); setLoginState("idle"); setDemoOpen(true); }; - const enterDemo = async (kind: "admin" | "user") => { - setLoginState(kind); setLoginError(""); - try { await login(kind === "admin" ? "demo@demo.com" : "user@demo.com", "openxyos-demo-2026"); navigate("/app"); } - catch (error) { setLoginError(error instanceof Error ? error.message : tx("演示环境暂时不可用,请使用登录页进入。", "The demo is temporarily unavailable. Please use the sign-in page.")); setLoginState("idle"); } + const enterDemo = (_kind: "admin" | "user") => { + setDemoOpen(false); + navigate("/auth"); }; - const saveModelAndEnter = async () => { - const selected = EXPERIENCE_MODELS.find(model => model.id === providerId); - const isLocal = Boolean(selected && "local" in selected && selected.local); - if (!isLocal && (apiKey.trim().length < 8 || /\s/.test(apiKey.trim()))) { setLoginError(tx("请输入有效的 API Key(不能包含空格)", "Enter a valid API key without spaces.")); return; } - setLoginState("admin"); setLoginError(""); - try { - await login("demo@demo.com", "openxyos-demo-2026"); - const response = await authFetch("/api/settings/ai/onboarding", { - method: "PUT", - body: JSON.stringify({ providerId, apiKey: isLocal ? (apiKey.trim() || "ollama") : apiKey.trim() }), - }); - const body = await response.text(); - const result = body ? JSON.parse(body) : null; - if (!response.ok || !result?.success) throw new Error(result?.error || tx("模型配置保存失败", "Unable to save model configuration.")); - setApiKey(""); - navigate("/app"); - } catch (error) { - setLoginError(error instanceof Error ? error.message : tx("模型配置保存失败", "Unable to save model configuration.")); - setLoginState("idle"); - } + const saveModelAndEnter = () => { + setDemoOpen(false); + navigate("/auth"); }; const copyInstall = async () => { await navigator.clipboard.writeText("npm ci && npm run dev"); setCopied(true); window.setTimeout(() => setCopied(false), 1600); }; const closeNav = () => setNavOpen(false); @@ -465,6 +445,6 @@ export default function OpenHomePage() {
BUILD WITH US

{tx("不只使用系统,一起定义人机组织的未来", "Do more than use the system—define the future of human–AI organizations")}

{tx("欢迎从组织模型、智能体协作、治理策略、模块生态和工程质量开始贡献。", "Contribute through organization models, agent collaboration, governance policy, module ecosystems, and engineering quality.")}

{GITHUB_URL ? {tx("查看 Issues", "View issues")} : {tx("阅读源码边界", "Read source boundary")}}
openXYOS

Open-source operating system for human–agent organizations.

Apache License 2.0 · Built in the open
- {demoOpen &&
setDemoOpen(false)}>
e.stopPropagation()}>{demoPhase === "model" ? <>

{tx("接入大模型,体验完整智能能力", "Connect a model to experience full intelligence")}

{tx("优先使用本机 Ollama;云厂商需要你自己的 API Key,安装包不会预填密钥。密钥保存到当前本地演示租户,页面不会回显原文;之后可在“系统设置 → AI 大模型”中更换。", "Prefer local Ollama first. Cloud providers need your own API key — installers never prefill one. The key is stored only in the current demo tenant and is never shown back in plain text; change it later in System Settings → AI Models.")}

{EXPERIENCE_MODELS.map(model => )}
{tx("Key 仅发送到当前 openXYOS 后端,不写入浏览器日志,也不会再次明文返回。", "The key is sent only to the current openXYOS backend, never logged in the browser, and never returned in plain text.")}
{loginState !== "idle" &&

{tx("正在登录演示租户并保存模型配置…", "Signing in to the demo tenant and saving model configuration…")}

}{loginError &&

{loginError}

}
: <>

{tx("选择测试视角", "Choose a demo perspective")}

{tx("使用本地演示租户进入真实系统。管理员可体验模块开关和名称编辑,普通员工只看到已授权能力。", "Enter the live system with the local demo tenant. Administrators can manage modules and labels; users see only authorized capabilities.")}

{loginState !== "idle" &&

{tx("正在连接演示环境…", "Connecting to the demo environment…")}

}{loginError &&

{loginError}

}}
} + {demoOpen &&
setDemoOpen(false)}>
e.stopPropagation()}>{demoPhase === "model" ? <>

{tx("接入大模型,体验完整智能能力", "Connect a model to experience full intelligence")}

{tx("优先使用本机 Ollama;云厂商需要你自己的 API Key,安装包不会预填密钥。密钥保存到当前本地演示租户,页面不会回显原文;之后可在“系统设置 → AI 大模型”中更换。", "Prefer local Ollama first. Cloud providers need your own API key — installers never prefill one. The key is stored only in the current demo tenant and is never shown back in plain text; change it later in System Settings → AI Models.")}

{EXPERIENCE_MODELS.map(model => )}
{tx("Key 仅发送到当前 openXYOS 后端,不写入浏览器日志,也不会再次明文返回。", "The key is sent only to the current openXYOS backend, never logged in the browser, and never returned in plain text.")}
{loginState !== "idle" &&

{tx("正在登录演示租户并保存模型配置…", "Signing in to the demo tenant and saving model configuration…")}

}{loginError &&

{loginError}

}
: <>

{tx("选择测试视角", "Choose a demo perspective")}

{tx("使用本地演示租户进入真实系统。管理员可体验模块开关和名称编辑,普通员工只看到已授权能力。", "Enter the live system with the local demo tenant. Administrators can manage modules and labels; users see only authorized capabilities.")}

{loginState !== "idle" &&

{tx("正在连接演示环境…", "Connecting to the demo environment…")}

}{loginError &&

{loginError}

}}
} ; } diff --git a/modules/openxyos/frontend/src/stores/auth.ts b/modules/openxyos/frontend/src/stores/auth.ts index a447b041..8c2cff83 100644 --- a/modules/openxyos/frontend/src/stores/auth.ts +++ b/modules/openxyos/frontend/src/stores/auth.ts @@ -13,7 +13,8 @@ interface AuthState { user: User | null; token: string | null; loading: boolean; - login: (email: string, password: string) => Promise; + login: (email: string, password: string) => Promise; + changePassword: (currentPassword: string, newPassword: string) => Promise; register: ( email: string, password: string, @@ -47,16 +48,41 @@ export const useAuthStore = create((set) => ({ localStorage.setItem("token", token); localStorage.setItem("user", JSON.stringify(data.data.user)); set({ user: data.data.user, token }); - // 记录登录事件 - authFetch("/api/admin/visitor-log", { + const mustChange = Boolean(data.data?.must_change_password); + if (!mustChange) { + // 记录登录事件 + authFetch("/api/admin/visitor-log", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + event_type: "login", + user_id: data.data.user.id, + tenant_id: data.data.user.tenant_id, + }), + }).catch(() => {}); + } + return mustChange; + }, + + changePassword: async (currentPassword, newPassword) => { + const res = await authFetch("/api/auth/change-password", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - event_type: "login", - user_id: data.data.user.id, - tenant_id: data.data.user.tenant_id, + current_password: currentPassword, + new_password: newPassword, }), - }).catch(() => {}); + }); + const body = await res.text(); + let data: any = {}; + if (body) { + try { + data = JSON.parse(body); + } catch { + throw new Error("后端返回了无法识别的响应"); + } + } + if (!res.ok || !data.success) throw new Error(data.error || "修改密码失败"); }, register: async (email, password, nickname) => { diff --git a/modules/openxyos/package-lock.json b/modules/openxyos/package-lock.json index af9c00c1..19d726a1 100644 --- a/modules/openxyos/package-lock.json +++ b/modules/openxyos/package-lock.json @@ -75,7 +75,7 @@ "zustand": "^5.0.0" }, "engines": { - "node": ">=20.19" + "node": ">=22.12" } }, "node_modules/@antfu/install-pkg": { @@ -191,7 +191,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1849,7 +1848,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1898,7 +1896,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -1920,7 +1917,6 @@ "resolved": "https://registry.npmmirror.com/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1957,10 +1953,10 @@ "react": ">=16.8.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", - "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "dev": true, "license": "MIT", "optional": true, @@ -4247,6 +4243,27 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "dev": true, @@ -4772,7 +4789,6 @@ "resolved": "https://registry.npmmirror.com/@types/express/-/express-5.0.6.tgz", "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -4911,7 +4927,6 @@ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5031,7 +5046,6 @@ "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.64.0", "@typescript-eslint/types": "8.64.0", @@ -5342,7 +5356,6 @@ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5378,7 +5391,6 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5871,7 +5883,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.11.20", "caniuse-lite": "^1.0.30001810", @@ -6454,7 +6465,6 @@ "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -6912,7 +6922,6 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -7710,7 +7719,6 @@ "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -11083,7 +11091,6 @@ "resolved": "https://registry.npmmirror.com/pg/-/pg-8.23.0.tgz", "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", @@ -11443,7 +11450,6 @@ "resolved": "https://registry.npmmirror.com/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11453,7 +11459,6 @@ "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -12712,7 +12717,6 @@ "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -12884,7 +12888,6 @@ "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.28.0" }, @@ -13027,7 +13030,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13326,7 +13328,6 @@ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/modules/openxyos/package.json b/modules/openxyos/package.json index 84671b5b..6f9d77ba 100644 --- a/modules/openxyos/package.json +++ b/modules/openxyos/package.json @@ -106,7 +106,7 @@ }, "description": "Open-source operating system for governed human-agent organizations", "engines": { - "node": ">=20.19" + "node": ">=22.12" }, "overrides": { "qs": "6.16.0", diff --git a/scripts/release_prerelease.py b/scripts/release_prerelease.py new file mode 100644 index 00000000..3a93ed5f --- /dev/null +++ b/scripts/release_prerelease.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Print whether a FreeOS tag must be a GitHub prerelease. + +After a leading ``v`` is stripped, PEP 440 pre-releases match +``(a|b|rc)[0-9]+$`` or ``.dev[0-9]+$``. ``v0.1.0rc1`` is a prerelease; +``v0.1.0`` is not. Output is a GitHub Actions assignment: ``prerelease=true``. +""" + +from __future__ import annotations + +import re +import sys + +_PRE = re.compile(r"(?:(?:a|b|rc)[0-9]+|\.dev[0-9]+)$") + + +def is_release_prerelease(tag: str) -> bool: + version = tag[1:] if tag.startswith("v") else tag + return _PRE.search(version) is not None + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print("usage: release_prerelease.py ", file=sys.stderr) + return 2 + flag = "true" if is_release_prerelease(argv[1]) else "false" + print(f"prerelease={flag}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/src/octop/modules/org_os/sidecar_launch.py b/src/octop/modules/org_os/sidecar_launch.py index 4556b33a..1e0bfe91 100644 --- a/src/octop/modules/org_os/sidecar_launch.py +++ b/src/octop/modules/org_os/sidecar_launch.py @@ -374,6 +374,7 @@ def sidecar_launch_env(home: Path, *, dashboard_port: int | None = None) -> dict { "NODE_ENV": "production", "PORT": port, + "HOST": "127.0.0.1", "DB_DIALECT": "sqlite", "DATABASE_PATH": str(sidecar_data_dir(home) / "xiongyuan.db"), "AIR_GAP_MODE": "true", diff --git a/tests/unit/desktop/test_openxyos_provisioner.py b/tests/unit/desktop/test_openxyos_provisioner.py index 01d16323..52d25d80 100644 --- a/tests/unit/desktop/test_openxyos_provisioner.py +++ b/tests/unit/desktop/test_openxyos_provisioner.py @@ -159,6 +159,8 @@ def test_start_sidecar_cors_origin_is_parseorigins_safe() -> None: assert origins assert "*" not in origins assert "http://127.0.0.1:3780" in origins + assert "$env:HOST = '127.0.0.1'" in text + assert "$env:FREEOS_ORG_LOCAL_TEST = '1'" in text assert "http://localhost:3780" in origins for origin in origins: parsed = urlparse(origin) diff --git a/tests/unit/test_release_prerelease.py b/tests/unit/test_release_prerelease.py new file mode 100644 index 00000000..de64da09 --- /dev/null +++ b/tests/unit/test_release_prerelease.py @@ -0,0 +1,27 @@ +"""GitHub Release prerelease classification for FreeOS tags.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +_SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "release_prerelease.py" +_spec = importlib.util.spec_from_file_location("release_prerelease", _SCRIPT) +assert _spec is not None and _spec.loader is not None +_mod = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_mod) + + +def test_rc_alpha_beta_and_dev_are_prereleases() -> None: + assert _mod.is_release_prerelease("v0.1.0rc1") is True + assert _mod.is_release_prerelease("0.1.0rc1") is True + assert _mod.is_release_prerelease("v0.1.0a1") is True + assert _mod.is_release_prerelease("v0.1.0b2") is True + assert _mod.is_release_prerelease("v0.1.0.dev1") is True + assert _mod.is_release_prerelease("0.1.0.dev3") is True + + +def test_production_tags_are_not_prereleases() -> None: + assert _mod.is_release_prerelease("v0.1.0") is False + assert _mod.is_release_prerelease("0.0.10") is False + assert _mod.is_release_prerelease("v0.1.0-rc.1") is False diff --git a/tests/unit/test_sidecar_launch.py b/tests/unit/test_sidecar_launch.py index 6a170536..b15fe657 100644 --- a/tests/unit/test_sidecar_launch.py +++ b/tests/unit/test_sidecar_launch.py @@ -305,6 +305,7 @@ def test_sidecar_launch_env_writes_secrets(tmp_path: Path) -> None: assert env["FREEOS_INGEST_TOKEN"] assert env["FREEOS_ORG_INTEGRATED"] == "1" assert env["FREEOS_ORG_LOCAL_TEST"] == "1" + assert env["HOST"] == "127.0.0.1" def test_sidecar_launch_env_merges_existing_cors( From 17bc806d7fe63eddf7d370b15809a6adde771a63 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 21 Oct 2026 22:00:13 +0000 Subject: [PATCH 07/14] fix(release): brand new FnOS packages and clear npm audit Name new FnOS artifacts FreeOS-fnos-*.fpk in the workflow, build script, release links, and user docs. Older Release assets may still be named Octop-fnos-*. Remove the expired WeCom QR from the user guide and README_CN. Readers go to GitHub Discussions or the existing Discord link. Bump vite to 6.4.5, vitest to 4.1.12, and follow-redirects to 1.16.1 so Desktop Package npm audit exits 0. Co-authored-by: haoboy007-sudo --- .github/workflows/fnos-build-fpk.yml | 12 +- README_CN.md | 10 +- dashboard/package-lock.json | 154 ++++++++++------------ dashboard/package.json | 9 +- docs/assets/qrcode.png | Bin 4766 -> 0 bytes docs/user-guide.md | 9 +- fnos/README.md | 16 +-- modules/openxyos/package-lock.json | 14 +- modules/openxyos/package.json | 4 +- scripts/build-fpk.sh | 16 +-- scripts/release_download_links.py | 4 +- tests/unit/test_release_download_links.py | 4 +- 12 files changed, 115 insertions(+), 137 deletions(-) delete mode 100644 docs/assets/qrcode.png diff --git a/.github/workflows/fnos-build-fpk.yml b/.github/workflows/fnos-build-fpk.yml index f8c06e06..7b14e4e1 100644 --- a/.github/workflows/fnos-build-fpk.yml +++ b/.github/workflows/fnos-build-fpk.yml @@ -25,7 +25,7 @@ permissions: packages: read concurrency: - group: build-octop-fnos + group: build-freeos-fnos cancel-in-progress: false jobs: @@ -99,14 +99,14 @@ jobs: - name: Build Docker FPK env: - FPK_NAME_PREFIX: Octop-fnos + FPK_NAME_PREFIX: FreeOS-fnos run: bash scripts/build-fpk.sh docker - name: Upload docker fpk artifact uses: actions/upload-artifact@v4 with: name: docker-fpk - path: dist/Octop-fnos-docker-*.fpk + path: dist/FreeOS-fnos-docker-*.fpk if-no-files-found: error native: @@ -203,18 +203,18 @@ jobs: - name: Build native FPK id: build_native env: - FPK_NAME_PREFIX: Octop-fnos + FPK_NAME_PREFIX: FreeOS-fnos run: bash scripts/build-fpk.sh native - name: Show native fpk size - run: ls -lh dist/Octop-fnos-native-*.fpk + run: ls -lh dist/FreeOS-fnos-native-*.fpk - name: Upload native fpk artifact id: upload_native uses: actions/upload-artifact@v4 with: name: native-fpk - path: dist/Octop-fnos-native-*.fpk + path: dist/FreeOS-fnos-native-*.fpk if-no-files-found: error - name: Upload native install log (for debugging) diff --git a/README_CN.md b/README_CN.md index 93505529..a708ad06 100644 --- a/README_CN.md +++ b/README_CN.md @@ -243,7 +243,7 @@ curl -fsSL https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install | Windows | `FreeOS-desktop-windows-amd64-.exe`(64 位)/ `FreeOS-desktop-windows-arm64-.exe`(ARM64)— NSIS 安装程序,单进程 FreeOS + 宿主内 Organization;不捆绑也不自动拉起 openXYOS Node | | macOS | `FreeOS-desktop-darwin-arm64-.dmg`(Apple 芯片)/ `FreeOS-desktop-darwin-amd64-.dmg`(Intel) | | Linux | `FreeOS-desktop-linux-amd64-.tar.gz` / `FreeOS-desktop-linux-arm64-.tar.gz` | -| 飞牛 NAS(FnOS) | `Octop-fnos-docker-.fpk`(依赖 Docker)/ `Octop-fnos-native-.fpk`(无需 Docker)— 通过应用中心安装 | +| 飞牛 NAS(FnOS) | `FreeOS-fnos-docker-.fpk`(依赖 Docker)/ `FreeOS-fnos-native-.fpk`(无需 Docker)— 通过应用中心安装。更早的 Release 资产可能仍叫 `Octop-fnos-*` | 桌面客户端说明见 [desktop/README.md](desktop/README.md),飞牛打包指南见 [fnos/README.md](fnos/README.md)。独立商业组织站见 [docs/org-export.md](docs/org-export.md)(`freeos org export-standalone`,默认导出完整 openXYOS 源码树),不是默认安装器里的第二套 Node。 @@ -545,13 +545,7 @@ cd dashboard && npx tsc --noEmit ### 💬 客户企业微信群 -如需加入客户企业微信服务群,请扫码: - -

- 客户企业微信服务群二维码 -

- -> 请扫码进入工作群,如有任何疑问或需求,请直接联系群管理员对接处理。 +客户企业微信群二维码已失效(有效期至 2026-08-03),本文不再提供该图。请到 [GitHub Discussions](https://github.com/XYAIStudio/FreeOS/discussions) 讨论,或使用页首 [Discord](https://discord.gg/jPas5J8Ua) 徽章加入社区。 ### 📄 许可证 diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 9ef4a83a..b53fed4f 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -50,7 +50,7 @@ "@types/react-dom": "^18", "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^4.4.1", - "@vitest/coverage-v8": "^4.1.11", + "@vitest/coverage-v8": "^4.1.12", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", @@ -62,9 +62,9 @@ "rollup-plugin-visualizer": "^5.14.0", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", - "vite": "^6.3.5", + "vite": "^6.4.5", "vite-plugin-pwa": "^0.21.2", - "vitest": "^4.1.11" + "vitest": "^4.1.12" } }, "node_modules/@adobe/css-tools": { @@ -253,7 +253,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1929,7 +1928,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1953,7 +1951,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -3915,7 +3912,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4353,7 +4351,6 @@ "resolved": "https://mirrors.tencent.com/npm/@types/react/-/react-18.3.30.tgz", "integrity": "sha512-3ek6mwJL5/VBewBcY4S66cqlCtK3qi4WIq37Z0m/NHw1hjhI7274Mx1qz/+ggSzyBCOEf7eHjBN6INjPAWYfYw==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -4364,7 +4361,6 @@ "resolved": "https://mirrors.tencent.com/npm/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4448,7 +4444,6 @@ "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", @@ -4733,15 +4728,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", - "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.12.tgz", + "integrity": "sha512-6S7/iVknKglLAxMFxQGNppBI0iqJS2NgM6FBxavitj7KaDq/vywOWGf6dGu7F2L1Qy0G6S3uTp7aGshHrzK8JA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.11", + "@vitest/utils": "4.1.12", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -4755,8 +4749,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.11", - "vitest": "4.1.11" + "@vitest/browser": "4.1.12", + "vitest": "4.1.12" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -4765,16 +4759,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", - "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.12.tgz", + "integrity": "sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", + "@vitest/spy": "4.1.12", + "@vitest/utils": "4.1.12", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -4783,13 +4777,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", - "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.12.tgz", + "integrity": "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.11", + "@vitest/spy": "4.1.12", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -4820,9 +4814,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", - "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.12.tgz", + "integrity": "sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==", "dev": true, "license": "MIT", "dependencies": { @@ -4833,13 +4827,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", - "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.12.tgz", + "integrity": "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.11", + "@vitest/utils": "4.1.12", "pathe": "^2.0.3" }, "funding": { @@ -4847,14 +4841,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", - "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.12.tgz", + "integrity": "sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.11", - "@vitest/utils": "4.1.11", + "@vitest/pretty-format": "4.1.12", + "@vitest/utils": "4.1.12", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -4863,9 +4857,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", - "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.12.tgz", + "integrity": "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==", "dev": true, "license": "MIT", "funding": { @@ -4873,13 +4867,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", - "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.12.tgz", + "integrity": "sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.11", + "@vitest/pretty-format": "4.1.12", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -4909,8 +4903,7 @@ "version": "5.5.0", "resolved": "https://mirrors.tencent.com/npm/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/acorn": { "version": "8.16.0", @@ -4918,7 +4911,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5358,7 +5350,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.11.26", "caniuse-lite": "^1.0.30001813", @@ -5805,7 +5796,6 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.3.tgz", "integrity": "sha512-yfYGhRcGAntq6YBD583j4n0Eg3jIxvWmZtz/5uz9UYkeIStSlMxuUja+ec5j3iBD8nv1rwaOAYMW09tBdkSeaQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -6203,7 +6193,6 @@ "resolved": "https://mirrors.tencent.com/npm/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -6565,7 +6554,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dompurify": { "version": "3.4.16", @@ -6890,7 +6880,6 @@ "resolved": "https://mirrors.tencent.com/npm/eslint/-/eslint-9.39.4.tgz", "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7984,7 +7973,6 @@ "url": "https://www.locize.com" } ], - "peer": true, "dependencies": { "@babel/runtime": "^7.29.2" }, @@ -9209,6 +9197,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -10332,6 +10321,7 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.57.0.tgz", "integrity": "sha512-5BkI9KGoqrNvBGUe15/QlZq3OooZ8WLg1AxTpaqHRCP3HNpzPPZKE2EDz8M7c+VRmCeUw1Brp4cx/PWm3kI/5A==", "license": "MIT", + "peer": true, "dependencies": { "dompurify": "3.4.15", "marked": "14.0.0" @@ -10341,6 +10331,7 @@ "version": "14.0.0", "resolved": "https://mirrors.tencent.com/npm/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -10721,7 +10712,6 @@ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=20.16.0 || >=22.3.0" }, @@ -10867,6 +10857,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -10882,6 +10873,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -10894,7 +10886,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/prismjs": { "version": "1.30.0", @@ -11563,7 +11556,6 @@ "resolved": "https://mirrors.tencent.com/npm/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11576,7 +11568,6 @@ "resolved": "https://mirrors.tencent.com/npm/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -11706,7 +11697,6 @@ "resolved": "https://mirrors.tencent.com/npm/react-redux/-/react-redux-9.3.0.tgz", "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -11864,8 +11854,7 @@ "version": "5.0.1", "resolved": "https://mirrors.tencent.com/npm/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -12166,7 +12155,6 @@ "resolved": "https://mirrors.tencent.com/npm/rollup/-/rollup-4.61.0.tgz", "integrity": "sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==", "dev": true, - "peer": true, "dependencies": { "@types/estree": "1.0.9" }, @@ -13245,7 +13233,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13636,12 +13623,11 @@ } }, "node_modules/vite": { - "version": "6.4.3", - "resolved": "https://mirrors.tencent.com/npm/vite/-/vite-6.4.3.tgz", - "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.5.tgz", + "integrity": "sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -13743,20 +13729,19 @@ } }, "node_modules/vitest": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", - "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.12.tgz", + "integrity": "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@vitest/expect": "4.1.11", - "@vitest/mocker": "4.1.11", - "@vitest/pretty-format": "4.1.11", - "@vitest/runner": "4.1.11", - "@vitest/snapshot": "4.1.11", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", + "@vitest/expect": "4.1.12", + "@vitest/mocker": "4.1.12", + "@vitest/pretty-format": "4.1.12", + "@vitest/runner": "4.1.12", + "@vitest/snapshot": "4.1.12", + "@vitest/spy": "4.1.12", + "@vitest/utils": "4.1.12", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -13784,12 +13769,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.11", - "@vitest/browser-preview": "4.1.11", - "@vitest/browser-webdriverio": "4.1.11", - "@vitest/coverage-istanbul": "4.1.11", - "@vitest/coverage-v8": "4.1.11", - "@vitest/ui": "4.1.11", + "@vitest/browser-playwright": "4.1.12", + "@vitest/browser-preview": "4.1.12", + "@vitest/browser-webdriverio": "^4.1.11", + "@vitest/coverage-istanbul": "4.1.12", + "@vitest/coverage-v8": "4.1.12", + "@vitest/ui": "4.1.12", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -14145,7 +14130,6 @@ "resolved": "https://mirrors.tencent.com/npm/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/dashboard/package.json b/dashboard/package.json index e0e6d198..9c5cf999 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -54,7 +54,8 @@ "serialize-javascript": "^7.0.3", "@rollup/plugin-terser": "^1.0.0", "katex": "^0.19.0", - "dompurify": "^3.4.16" + "dompurify": "^3.4.16", + "vite": "^6.4.5" }, "devDependencies": { "@eslint/js": "^9.25.0", @@ -68,7 +69,7 @@ "@types/react-dom": "^18", "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^4.4.1", - "@vitest/coverage-v8": "^4.1.11", + "@vitest/coverage-v8": "^4.1.12", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", @@ -80,8 +81,8 @@ "rollup-plugin-visualizer": "^5.14.0", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", - "vite": "^6.3.5", + "vite": "^6.4.5", "vite-plugin-pwa": "^0.21.2", - "vitest": "^4.1.11" + "vitest": "^4.1.12" } } diff --git a/docs/assets/qrcode.png b/docs/assets/qrcode.png deleted file mode 100644 index 6703b9d7870f9c2167a954ecbb786b1e59d99ad3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4766 zcmZ{oc{H2b*T>UVQA1U4D`}MuI>waT8j5PGDr#;eC~AIGCG&E(?VOr zKqtSUMHxH1;OUn3H1%2cG)WOl{H&HfvC1NWX33>aDW86FI;QZgOZr-{?_lOb-$&jm zoeTB8P$tT@;q++i4iH!#>bjBLP8drBty$$_33mSFd;-E23T9I-1M`bMb_Q+RkkzZXp_B-^ci$Nx9JK=^ zD(rmhuR)9*o7m+Y6p+sE+=FfI851NU_*Nu{-dq7Y#ZuK?rf_TiO!2=K|Ckr?XPH^B zweEVSpgAlfu=(%E)i^w%fIj1m`eK2HIjcMAp@GFG82;G~{@~p|bTM2qtg_oAJhgZNtZGxn=X z4kL@3dpU@-8ASdI#%t!q54`!$&DiWD*`*w7>&j3q*MC>=&v=T$-Gcb?YUv%C&c{I@ z2dr=tVR%XhW<~y`mXe+h-}Yu(>`BdL;3b{7mSr8YwO1o#41dt8X@0q}f4R7q7l7j- z4o(l3aJBeJs_QjbYXv|>wtJ0Djq(xb_kDz*-2XcXfUs(Q_7uxT+p1p^<}{ZTVMrl$ z)kkV#Dff?kyMwv&k_T?C3M&ci%I};CqXc3^(B@prwh4T3j1-$_GLg9)zN`CtqlCVe zFv@)&QDGS46;DvDRr$(dKe>s8brivoHYF<>^-YHfgV|T4)BGB*LJr5K0ML~YJ?Oy+ zJd=d5V@*oF#ff0M>s2+=cn@b}k4V5EF_ZJ?Lp2H@{ZX&JAR)K2N0gr@{_U_Re3t=& zvP1k`&d6p^7`%aQRG{h6)M+jRo1~U*$FWF zDWw04GXtcB9qo)z`2=*=qha8Wf;>K+u|XQ!%-ETQ__E!dTD%mDAZzi81`z9Tl@CC7 zTR)DG8M20Ydz=ibnh~?$ek7H><#hrn7`*E4{ht@`R^cRyR%Bvc);=%RMWXs#(NyS>TbW`9(|17lKO}IysLcwMM|IZ408)!^DJ9 z)1+Ia3>P`#0denyC64}x&(<&2jAh-CC@G=llvCKw*p`fl7e5`;_hoQ>04^5sKn@V>y#9<4Xi~J=OHZ->IE5qU zqmF~R<+*Fi)$)6&-G^tDO~bVLtqgmd{{k3ijK@?3$A>=h9xqbiK7V7l4lZ*D?PT`y zkF8g4N#}`|p6c?Na@R@t0H;^BpLV zQ7=)@PMJn@EMHQNAWnSIK|fnAb#`MU#zYe%A6{0|Be^S=`%tngnR~E;2GvdsV0`NAQ^_(P~;USU8G z+Jtr3$7ovh7KACc&Cjt8W+8kfsGDlDgxnFcGvXEuloQ3TBC^7-r zaR^&D))^3c@&#LVA*R5Y~saP`?jj|H~C$mp#$zMqL0 zgI+MlV>GWp>TV5ajPG2@#CS{>Isq3e5;-f63(E&)dUVrCrR#iy**>EY|GCLopH0li zMhT&Ow7%eQ+WPjny3APDU!Z^Ne!KSE9BrmkgcSLpp6@Hiy?cdSR#)B;k?1T@lb0_e z!Eqn)G94*JE2?_m1j^*l97?1_x~t6Ew6}Egq}CJR*C80qPZMLczwBn>EoZ|VHiqyb z@q}gsD8lU`C4$yiV~%dQYS&wmwrn-n6I~u|KW5cjqd3tw(kkw#vG?^6u5^;3a5p~( znjn2G#z5*)`CLj+WLv6*$n{K`xLq%vT-U?tdSU%c?tj`ULY;@Kvs@**drh$qo~%!Q zQdx*0&sAG&Q^yszuuGhwQ@X_>l_$N+TPqwhD{mqz>~QIeM>p-a=~*5@miGJ?1qsn- zJT%j*Yf-Nt?%|Equ>>Md*sNqxUUw~=VKC*y={zMzl~auIupfx`zPiAA z_79FIN^G4T;qXzO`O!!To^=FAlN%pA10FX)>JUSXxQiu;gxbZ;`+>pe&}_OsS(Q&L zf<}#wbnN@Uy=rEYfwoODPQJsCb|~&C#wTd!rmK}JV{r}IE5H9r2Uggta`dqUI6OS5e4yc_OP`0@b}!*MG2iT$i1rJ2Gz73ddZrM_KxDtId5#ck6qT zD_}sc64K261w~4c@>C+w?K$-z86jI~Oyy%7RA*x7vMi;DoV2FGIcjsXH=JCBRD0sy z#r4w;yi+j0r;M6j8T?UtLdt;$Kg>{OmiQ~*XkSQhURk0MtNelbqpo2t63Gb|&B4ZQ zN$btA+WxP&bv2-S`qX;hW@)LqAJs)}2_#<9s|hc1uAXfG(S*x72UXpNGGwHH>>M-B zp0cMERwAe-Jz z#vDK6VOm~#!KbuuvG?_r%XFv@CrSIoDPOuI9nzHBC<3~Wwo}P*ZV^u|TB@(pz8${) zsg@mAK4B%R3jScA{Ue39`%-S~PMprx8#U>7cPH~xRFbC`G?hC$(1RM5x^tI&T(=`i ztSXuj?f;+3tPGRM6DT1Wf#{5?pBi!x)sifDN~J6I`l09At+F? zz^s3tL_G1*2w%~LLPJ~}Z$|Hv+Ygd0%36@TrvEK-{a43=9P-!;z67@hFLoLa;hXA^6ZhND%`%MOKYB&&Te`Zj z+B!XRo*DW|TNswW2a?p^2V7MyKOD6~l_}lbQ7<)i1XB^nV$YJcZ_{6I*EqSr45r3p zzeAxb6S9u9#=L*+i=ZHS-y-7; z?og$>-;vgptWL8Jpg(y_&90j}d^HRv89m|r zzUanebtdHcAloj{>W)d71zyQ4A`)%BNunXTn&W@DGx}||Y8ylZ&s$|d11||A+$pY# z-}d9L)WCJ>4RxxX$NJl=Sk>NjJ247-=;- zuqvxC&9_;T?OXlJ(5v;12$|09Q>6^Hd+qWgBsQV1W@xCJ)Hn5^Mn<7FX~>vE{fPls9Mmti_4y(m*|&$6+)$4erDN<)^ewht1aF)^fe)3QfB= z8;6F|cK#dm6KWkY;ZEM{g_p&~>k&=Py=q5?K9TEMM~J&GehgN2_8umr@@A059aWtq zJTnqHpk?i}*_v(KQbZ+}#>FA>Go}{Q^fIJB*$3E7J?vG|>KgN9CiQFv2piHn6~QM~1&6+xKC}iCllxM1Mj%IlS9_GhoI> z-i;M-yLeatY5!(0*o%3&@jw-BI=o>Xw6K>3>j$HS(|EttDtOd3o+-AbtVX%2AtYdq z=_d(mzHK3XA#*%Mi%L1iq3MMj-s3_`!XJvyirFOIrCzN{?i;6YU&Bgc)L=&yw5%u2 zZC+al@a%8M|6gQBP3TxP%z}85J)78x<5l@H 二维码有效期至 **2026-08-03**,过期后请联系管理员更新。 +- 客户企业微信群二维码已于 **2026-08-03** 失效,指南不再附带该图。 +- 请改用 [GitHub Discussions](https://github.com/XYAIStudio/FreeOS/discussions),或加入 [Discord](https://discord.gg/jPas5J8Ua)。 --- @@ -497,4 +494,4 @@ Octop 支持两个方向的 ACP 集成: | 图 5.6 | 5.8 设置 | `use-06-settings.png` | ✅ 已就位 | 应用设置页面 | | 图 5.7 | 5.9 远程桌面 | `use-07-remote-desktop.png` | ✅ 已就位 | 远程桌面连接页 | | 图 5.8 | 5.9 浏览器 AI | `use-08-browser-ai.png` | ✅ 已就位 | 浏览器 AI+ 会话页 | -| 图 7.1 | 七、常见问题 | `qrcode.png` | ✅ 已就位 | 客户企业微信服务群二维码(有效期至 2026-08-03) | +| 图 7.1 | 七、常见问题 | `qrcode.png` | 已撤下 | 客户企业微信群码已于 2026-08-03 失效,不再随文档分发;请用 GitHub Discussions 或 Discord | diff --git a/fnos/README.md b/fnos/README.md index db66a974..19f9bec1 100644 --- a/fnos/README.md +++ b/fnos/README.md @@ -39,13 +39,13 @@ octop backup --help # 备份/恢复 | 版本 | 包名 | 体积 | 运行方式 | 依赖 | |------|------|------|----------|------| -| **Docker 版** | `octop-.fpk` | ~8 KB | 飞牛自动从 GHCR 拉取 `ghcr.io/xyaistudio/freeos:latest` 镜像运行 | 宿主需有 Docker 运行时 | -| **本地版(非 Docker)** | `octop-native-.fpk` | ~560 MB | 自带 Python 3.12 运行时 + 前端 + 核心依赖,原生运行在飞牛主机 | 无需 Docker | +| **Docker 版** | `FreeOS-fnos-docker-.fpk` | ~8 KB | 飞牛自动从 GHCR 拉取 `ghcr.io/xyaistudio/freeos:latest` 镜像运行 | 宿主需有 Docker 运行时 | +| **本地版(非 Docker)** | `FreeOS-fnos-native-.fpk` | ~560 MB | 自带 Python 3.12 运行时 + 前端 + 核心依赖,原生运行在飞牛主机 | 无需 Docker | - **Docker 版**实现为 FnOS `docker-project`:包体只含 `docker-compose.yaml` 与向导配置,运行时由飞牛从 GHCR 拉取镜像。镜像已内置 `desktop` 桌面控制与前端;Playwright Chromium 不预装,可在控制台按需安装。 - **本地版**实现为 FnOS 原生 `app`:包内自带独立 Python 3.12 运行时、Octop 核心依赖、前端构建产物,以及 `data-share` 共享数据目录,直接以进程方式运行,不依赖 Docker。 -> 两款包随正式版一起挂在 **`v*` GitHub Release** 上(例如 [Releases](https://github.com/XYAIStudio/FreeOS/releases/latest)):`Octop-fnos-docker-.fpk` / `Octop-fnos-native-.fpk`。 +> 两款包随正式版一起挂在 **`v*` GitHub Release** 上(例如 [Releases](https://github.com/XYAIStudio/FreeOS/releases/latest)):`FreeOS-fnos-docker-.fpk` / `FreeOS-fnos-native-.fpk`。更早的 Release 资产可能仍使用 `Octop-fnos-*` 文件名。 ## 目录结构 @@ -89,14 +89,14 @@ fnos/ 1. **发版链路**:`v*` tag → `release.yml`(PyPI + GitHub Release)与 `docker-publish.yml`(GHCR / Hub)并行;Release 成功后自动 `workflow_dispatch` 本工作流。 2. **镜像复用**:不再重新 build 镜像;`ensure-image` 轮询等待 `ghcr.io/xyaistudio/freeos:{version}`(由 docker-publish 推送)。Docker 版 `.fpk` 仅打包 compose,运行时拉取该镜像。 3. **Wheel 复用**:Native 版优先从同版本 GitHub Release(`v*`)下载 `octop-*.whl`;若缺失再回退源码构建前端 + wheel。 -4. **安装包构建**:`fpk` / `native` job 用 `scripts/build-fpk.sh` 打包,产物 `Octop-fnos-docker-.fpk` / `Octop-fnos-native-.fpk` 挂到同一个 `v*` GitHub Release(与 wheel、桌面包并列)。 +4. **安装包构建**:`fpk` / `native` job 用 `scripts/build-fpk.sh` 打包,产物 `FreeOS-fnos-docker-.fpk` / `FreeOS-fnos-native-.fpk` 挂到同一个 `v*` GitHub Release(与 wheel、桌面包并列)。 ## 本地构建 .fpk(无需 Docker) ```bash -bash scripts/build-fpk.sh # 仅 Docker 版 → dist/Octop-fnos-docker-.fpk +bash scripts/build-fpk.sh # 仅 Docker 版 → dist/FreeOS-fnos-docker-.fpk bash scripts/build-fpk.sh docker # 仅 Docker 版 -bash scripts/build-fpk.sh native # 仅本地版 → dist/Octop-fnos-native-.fpk +bash scripts/build-fpk.sh native # 仅本地版 → dist/FreeOS-fnos-native-.fpk ``` `.fpk` 为「双层 gzip tar」:外层含 `app.tgz / cmd / config / wizard / ICON.PNG / ICON_256.PNG / LICENSE / manifest`,内层 `app.tgz` 含 `app/` 内容。 @@ -104,8 +104,8 @@ bash scripts/build-fpk.sh native # 仅本地版 → dist/Octop-fnos-nat ## 在飞牛上安装 1. 飞牛「应用中心 → 设置 → 手动安装应用」选择对应 `.fpk`: - - 想用 Docker 跑、主机已装 Docker → 选 `Octop-fnos-docker-.fpk` - - 不想依赖 Docker、希望自带运行时原生运行 → 选 `Octop-fnos-native-.fpk` + - 想用 Docker 跑、主机已装 Docker → 选 `FreeOS-fnos-docker-.fpk` + - 不想依赖 Docker、希望自带运行时原生运行 → 选 `FreeOS-fnos-native-.fpk` 2. 安装向导中设置管理员账号/密码、日志级别、LLM 密钥(可选)。 3. 安装完成后桌面出现「Octop AI 助手」图标,浏览器打开 `http://<设备IP>:8088`。 4. Docker 版镜像首次会从 GHCR `ghcr.io/xyaistudio/freeos:latest` 拉取;请确保该包为 Public(首次推送后可在 GitHub Packages 设置)。本地版无需联网拉镜像。Playwright Chromium 不预装,需要远程浏览器时在控制台按需安装。 diff --git a/modules/openxyos/package-lock.json b/modules/openxyos/package-lock.json index 19d726a1..f2561f3b 100644 --- a/modules/openxyos/package-lock.json +++ b/modules/openxyos/package-lock.json @@ -70,7 +70,7 @@ "tsx": "^4.19.0", "typescript": "^5.6.0", "typescript-eslint": "^8.64.0", - "vite": "^6.0.0", + "vite": "^6.4.5", "vite-plugin-pwa": "^1.3.0", "zustand": "^5.0.0" }, @@ -8280,9 +8280,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", + "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", "funding": [ { "type": "individual", @@ -13323,9 +13323,9 @@ } }, "node_modules/vite": { - "version": "6.4.3", - "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.3.tgz", - "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.5.tgz", + "integrity": "sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/modules/openxyos/package.json b/modules/openxyos/package.json index 6f9d77ba..b515bd71 100644 --- a/modules/openxyos/package.json +++ b/modules/openxyos/package.json @@ -100,7 +100,7 @@ "tsx": "^4.19.0", "typescript": "^5.6.0", "typescript-eslint": "^8.64.0", - "vite": "^6.0.0", + "vite": "^6.4.5", "vite-plugin-pwa": "^1.3.0", "zustand": "^5.0.0" }, @@ -110,6 +110,8 @@ }, "overrides": { "qs": "6.16.0", + "follow-redirects": "1.16.1", + "vite": "^6.4.5", "katex": "^0.18.2", "exceljs": { "uuid": "^11.1.1" diff --git a/scripts/build-fpk.sh b/scripts/build-fpk.sh index c827e0b0..dc1846e5 100644 --- a/scripts/build-fpk.sh +++ b/scripts/build-fpk.sh @@ -1,18 +1,18 @@ #!/usr/bin/env bash # ============================================================================= -# 构建 Octop 飞牛 FnOS 安装包 (.fpk) +# 构建 FreeOS 飞牛 FnOS 安装包 (.fpk) # # 直接使用官方 fnpack CLI 打包(fnpack 在生成 .fpk 前会校验 manifest / cmd / # config / wizard / app 等结构,确保产物与飞牛 fnOS 安装校验完全一致)。 # # 用法(在仓库根目录执行): -# bash scripts/build-fpk.sh docker # 构建 Docker 版 -> dist/Octop-fnos-docker-.fpk -# bash scripts/build-fpk.sh native # 构建本地版(非Docker) -> dist/Octop-fnos-native-.fpk +# bash scripts/build-fpk.sh docker # 构建 Docker 版 -> dist/FreeOS-fnos-docker-.fpk +# bash scripts/build-fpk.sh native # 构建本地版(非Docker) -> dist/FreeOS-fnos-native-.fpk # bash scripts/build-fpk.sh # 两个都构建 # # 环境变量: -# FPK_NAME_PREFIX 输出文件名前缀,默认 "octop" -# 例如 FPK_NAME_PREFIX=Octop-fnos 会生成 Octop-fnos-docker-.fpk / Octop-fnos-native-.fpk +# FPK_NAME_PREFIX 输出文件名前缀,默认 "FreeOS-fnos" +# 生成 FreeOS-fnos-docker-.fpk / FreeOS-fnos-native-.fpk # FPK_ITER 迭代号,默认空 # 例如 FPK_ITER=01 会生成 ...--01.fpk(通常不需要,按版本号发布) # @@ -34,10 +34,10 @@ trap cleanup EXIT VER="$(grep -m1 '^version' "$ROOT/pyproject.toml" | sed -E 's/.*"([0-9][0-9.]*[0-9])".*/\1/')" [ -n "$VER" ] || { echo "无法从 pyproject.toml 解析版本"; exit 1; } -echo "[build-fpk] Octop 版本: $VER" +echo "[build-fpk] FreeOS 版本: $VER" -# 输出文件名前缀与迭代号(由 CI 传入,实现 Octop-fnos-docker-0.9.30.fpk 风格) -PREFIX="${FPK_NAME_PREFIX:-octop}" +# 输出文件名前缀与迭代号(默认 FreeOS-fnos-docker-.fpk) +PREFIX="${FPK_NAME_PREFIX:-FreeOS-fnos}" ITER_SUFFIX="" if [ -n "${FPK_ITER:-}" ]; then ITER_SUFFIX="-$FPK_ITER" diff --git a/scripts/release_download_links.py b/scripts/release_download_links.py index 00378222..98251e69 100644 --- a/scripts/release_download_links.py +++ b/scripts/release_download_links.py @@ -55,8 +55,8 @@ def render_download_section(raw_version: str) -> str: linux_arm = md_link("ARM64", tag, desktop_name("linux", "arm64", version, "tar.gz")) linux64_zip = md_link("64-bit", tag, portable_name("linux", "amd64", version)) linux_arm_zip = md_link("ARM64", tag, portable_name("linux", "arm64", version)) - fnos_docker = md_link("Docker", tag, f"Octop-fnos-docker-{version}.fpk") - fnos_native = md_link("Native", tag, f"Octop-fnos-native-{version}.fpk") + fnos_docker = md_link("Docker", tag, f"FreeOS-fnos-docker-{version}.fpk") + fnos_native = md_link("Native", tag, f"FreeOS-fnos-native-{version}.fpk") return "\n".join( [ "", diff --git a/tests/unit/test_release_download_links.py b/tests/unit/test_release_download_links.py index 53ccc266..8dca01d6 100644 --- a/tests/unit/test_release_download_links.py +++ b/tests/unit/test_release_download_links.py @@ -38,7 +38,7 @@ def test_render_download_section_matches_github_asset_names() -> None: assert f"{base}/FreeOS-desktop-linux-arm64-0.9.31.tar.gz" in body assert f"{base}/FreeOS-portable-linux-amd64-0.9.31.zip" in body assert f"{base}/FreeOS-portable-linux-arm64-0.9.31.zip" in body - assert f"{base}/Octop-fnos-docker-0.9.31.fpk" in body - assert f"{base}/Octop-fnos-native-0.9.31.fpk" in body + assert f"{base}/FreeOS-fnos-docker-0.9.31.fpk" in body + assert f"{base}/FreeOS-fnos-native-0.9.31.fpk" in body assert "pip install" not in body assert "## Downloads" in body From 65c5b5601c502096a503b65fcfddb59e9e05bd94 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 21 Oct 2026 22:06:15 +0000 Subject: [PATCH 08/14] fix(release): publish GitHub wheels as freeos-0.1.0 Set the distribution name to freeos so the wheel and sdist are freeos-0.1.0*. The import package and the octop CLI alias stay. FnOS, portable packaging, and release notes use the new filenames and say not to pip install octop. User-facing paths in the docs this change touches name ~/.octop as the data directory. FREEOS_HOME applies only when it is already set; this release does not migrate that directory to ~/.freeos. Co-authored-by: haoboy007-sudo --- .github/workflows/fnos-build-fpk.yml | 10 +++++----- .github/workflows/release.yml | 5 +++-- CHANGELOG.md | 6 +++--- README.md | 14 +++++-------- README_CN.md | 4 ++-- desktop/README.md | 24 +++++++++-------------- desktop/portable/package.sh | 10 +++++----- docs/architecture-integration.md | 11 +++++------ docs/user-guide.md | 2 +- fnos/README.md | 2 +- pyproject.toml | 4 ++-- scripts/release_download_links.py | 9 +++++++++ src/octop/cli/repl/render.py | 2 +- src/octop/infra/setup/self_update.py | 4 ++-- tests/unit/test_release_download_links.py | 5 ++++- uv.lock | 2 +- 16 files changed, 58 insertions(+), 56 deletions(-) diff --git a/.github/workflows/fnos-build-fpk.yml b/.github/workflows/fnos-build-fpk.yml index 7b14e4e1..21829870 100644 --- a/.github/workflows/fnos-build-fpk.yml +++ b/.github/workflows/fnos-build-fpk.yml @@ -131,8 +131,8 @@ jobs: set -euo pipefail mkdir -p dist REL_TAG="v${VER}" - if gh release download "$REL_TAG" --repo "${{ github.repository }}" -p 'octop-*.whl' -D dist 2>/tmp/wheel-dl.err; then - ls -lh dist/octop-*.whl + if gh release download "$REL_TAG" --repo "${{ github.repository }}" -p 'freeos-*.whl' -D dist 2>/tmp/wheel-dl.err; then + ls -lh dist/freeos-*.whl echo "source=release" >> "$GITHUB_OUTPUT" echo "Reusing wheel from GitHub Release ${REL_TAG}" else @@ -169,7 +169,7 @@ jobs: python3 -m pip install --quiet build python3 -m build --wheel echo "=== built wheel ===" - ls -lh dist/octop-*.whl + ls -lh dist/freeos-*.whl - name: Install Octop core into app/site-packages (no Chromium, no preinstalled skills) id: install_core @@ -185,7 +185,7 @@ jobs: { python3 -m pip install --quiet uv python3 -m uv pip install --python python3.12 \ - --target "$SP" --no-cache "$(ls dist/octop-*.whl | head -1)" + --target "$SP" --no-cache "$(ls dist/freeos-*.whl | head -1)" rm -rf "$SP/mcp" "$SP"/mcp-*.dist-info \ "$SP/langchain_mcp_adapters" "$SP"/langchain_mcp_adapters-*.dist-info mkdir -p /tmp/octop-pinned @@ -195,7 +195,7 @@ jobs: echo "Extracting pinned wheel: $whl" python3.12 -m zipfile -e "$whl" "$SP" done - cp dist/octop-*.whl "$SP/octop.whl" + cp dist/freeos-*.whl "$SP/freeos.whl" PYTHONPATH="$SP" python3.12 \ -c "import octop, mcp, langchain_mcp_adapters; from octop.launch import run_foreground_blocking; print('octop', octop.__version__, '| mcp', mcp.__version__, '| lcma', langchain_mcp_adapters.__version__)" } 2>&1 | tee -a "$LOG" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95ffa435..03c305ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,9 +115,10 @@ jobs: echo "" echo "## Install" echo "" - echo "Download the Python wheel from this GitHub Release and install it with:" + echo "**Do not** \`pip install octop\`. PyPI \`octop\` is upstream Octop, not this FreeOS release." + echo "Install the wheel attached to this GitHub Release:" echo '```bash' - echo "uv pip install ./octop-$version-py3-none-any.whl" + echo "uv pip install ./freeos-$version-py3-none-any.whl" echo '```' } > .release-notes.md fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 09aa70ac..83341437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,12 @@ ## [0.1.0] - TBD -FreeOS **0.1.0** is an independent semver baseline. It does not track or align with Octop 1.0.x. Python wheels stay on GitHub Releases; this release does not publish the colliding `octop` name to PyPI. +FreeOS **0.1.0** is an independent semver baseline. It does not track or align with Octop 1.0.x. GitHub Release Python artifacts are `freeos-0.1.0*` (sdist and wheel). The import package stays `octop`. This release does not publish to PyPI, and `pip install octop` installs upstream Octop, not FreeOS. Older Release files may still be named `octop-0.0.10*`. ### Upgrade from 0.0.10 -- In-app update and a manual reinstall both keep `~/.freeos` (legacy `~/.octop` is still recognized): sessions, local models, knowledge bases, organization data, and `config.json`. -- Desktop portable upgrades copy a SQLite backup under `$FREEOS_HOME/backups` before replacing the runtime, and checkpoint the WAL before the process restarts. +- In-app update and a manual reinstall keep the data directory the process already uses. Today that directory is `~/.octop` (`OCTOP_HOME`): sessions, local models, knowledge bases, organization data, and `config.json`. `FREEOS_HOME` is honored only when it is already set. This release does not migrate `~/.octop` to `~/.freeos`. +- Desktop portable upgrades copy a SQLite backup under the data directory's `backups/` before replacing the runtime, and checkpoint the WAL before the process restarts. - A `config.json` that cannot be parsed is left in place and reported. It is not rewritten as an empty file. - Roll back by installing the **v0.0.10** desktop package again, or restore a backup with `octop backup restore`. Do not point install scripts at PyPI `octop`; that package is upstream Octop, not this release. Use [GitHub Releases](https://github.com/XYAIStudio/FreeOS/releases) or `scripts/install.sh` (add `--from-pypi` only when you explicitly want upstream Octop). diff --git a/README.md b/README.md index 237b8afa..369537f8 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,9 @@ Operator detail: [docs/asset-loop.md](docs/asset-loop.md). 3. 打开 **FreeOS**。第一次启动会解压内置运行环境(可能要一两分钟),然后直接进入可用会话,无需先登录。 4. 保存、导出或发布到账号时再在本机完成注册或登录(更广的服务或授权不挡起步)。侧栏 **Organization** 当前默认走宿主内组织能力;桌面安装包附带的托管 Node + iframe(延续 0.0.3 过渡桥)、以及可选的完整 openXYOS Node 栈(`127.0.0.1:3780`,导出/同步/高级部署)都是 **过渡桥**,终态是把 openXYOS 迁入宿主。组织能力像工作室里另一间可独立布置的房间,与日常对话、助手协作同在一个 FreeOS,两种进入方式不捏成一种。 -数据目录默认是 `%USERPROFILE%\.freeos`(可用环境变量 `FREEOS_HOME` 改)。旧版 Octop 的 `~/.octop` 仍会被识别。卸载安装包会清空安装目录(默认为 `Program Files\FreeOS`)并删除快捷方式,但**不会**删除该用户数据目录;详见 [desktop/README.md](desktop/README.md#windows-uninstall)。 +**不要**执行 `pip install octop`。PyPI 上的 `octop` 是上游 Octop,不是 FreeOS。本仓库的 Python 制品在 GitHub Releases 上,文件名是 `freeos--py3-none-any.whl` 和 `freeos-.tar.gz`(导入包仍是 `octop`)。桌面安装包仍是 `FreeOS-desktop-*`。 + +数据目录是 `%USERPROFILE%\.octop`(也可用 `OCTOP_HOME` 指定)。已经设置的 `FREEOS_HOME` 会被沿用;这一版不会把 `~/.octop` 迁到 `~/.freeos`。卸载安装包会清空安装目录(默认为 `Program Files\FreeOS`)并删除快捷方式,但**不会**删除该用户数据目录;详见 [desktop/README.md](desktop/README.md#windows-uninstall)。 安装包由 CI 工作流 **FreeOS Desktop Package**(文件名仍是 `.github/workflows/octop-desktop.yml`,给现有发版脚本用)在 Windows runner 上打出来。本仓库的云环境打不出 `.exe`;合并后由该 job 产出。 @@ -168,13 +170,7 @@ FreeOS prefers runtimes and files on this computer. Cloud vendors are optional. | Knowledge | Sidebar **Knowledge Bases**: mount a local folder first. Local ONNX embeddings are the default vector path. ima / WeKnora remain optional connectors. | | Detail | [User guide](docs/user-guide.md#四配置模型llm-供应商) · [Configuration](docs/configuration.md#local-models-and-knowledge-bases) · [Product contract](docs/product-contract.md#local-models-and-knowledge-bases) | -Data directory precedence: - -1. `FREEOS_HOME` -2. `OCTOP_HOME` (legacy) -3. existing `~/.freeos` -4. existing `~/.octop` -5. new installs → `~/.freeos` +Data directory: `~/.octop` (`OCTOP_HOME`). If `FREEOS_HOME` is already set, that path is used. This release does not migrate `~/.octop` to `~/.freeos`. ### Rebuild the dashboard (optional) @@ -239,7 +235,7 @@ Inbound import compiles the blueprint **and** registers a FreeOS agent (`org-pol ## Host platform features (from Octop) -Unchanged in this fork: multi-user JWT, multi-agent chat, expert library, connectors, ACP, knowledge bases, bundled plugins, cron, terminal/browser/desktop surfaces. The **Windows desktop installer** is branded **FreeOS** and defaults to `~/.freeos` / `FREEOS_HOME`. The Python package, `octop` CLI alias, `OCTOP_*` env vars, and `octop.db` stay for compatibility. +Unchanged in this fork: multi-user JWT, multi-agent chat, expert library, connectors, ACP, knowledge bases, bundled plugins, cron, terminal/browser/desktop surfaces. The **Windows desktop installer** is branded **FreeOS**. User data stays in `~/.octop` (`OCTOP_HOME`); `FREEOS_HOME` is honored only when it is already set. The import package, `octop` CLI alias, `OCTOP_*` env vars, and `octop.db` stay for compatibility. GitHub Release wheels are named `freeos-*`. Longer host docs: [docs/user-guide.md](docs/user-guide.md), [docs/configuration.md](docs/configuration.md), [docs/architecture.md](docs/architecture.md), [README_CN.md](README_CN.md). diff --git a/README_CN.md b/README_CN.md index a708ad06..c02c6de7 100644 --- a/README_CN.md +++ b/README_CN.md @@ -247,7 +247,7 @@ curl -fsSL https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install 桌面客户端说明见 [desktop/README.md](desktop/README.md),飞牛打包指南见 [fnos/README.md](fnos/README.md)。独立商业组织站见 [docs/org-export.md](docs/org-export.md)(`freeos org export-standalone`,默认导出完整 openXYOS 源码树),不是默认安装器里的第二套 Node。 -**不要**从 PyPI 安装 `octop`。PyPI 上的 `octop` 是上游 Octop,不是 FreeOS 0.1.0。桌面安装包从 [GitHub Releases](https://github.com/XYAIStudio/FreeOS/releases) 下载;终端安装用上面的安装脚本(默认拉取 FreeOS GitHub 源码)。只有在你明确要上游 Octop 时才加 `--from-pypi`。可选组件仍通过安装脚本传递,例如 `--extras browser`。 +**不要** `pip install octop`。PyPI 上的 `octop` 是上游 Octop,不是 FreeOS 0.1.0。本仓库发到 GitHub Releases 的 Python 制品是 `freeos--py3-none-any.whl` 和 `freeos-.tar.gz`(导入包仍叫 `octop`)。更早的 Release 里可能还有 `octop-0.0.10*` 文件名,那是旧制品名,不是 PyPI 安装命令。桌面安装包从 [GitHub Releases](https://github.com/XYAIStudio/FreeOS/releases) 下载;终端安装用上面的安装脚本(默认拉取 FreeOS GitHub 源码)。只有在你明确要上游 Octop 时才加 `--from-pypi`。可选组件仍通过安装脚本传递,例如 `--extras browser`。 从源码用 uv 开发时: @@ -335,7 +335,7 @@ docker run -d \ | 远程一键安装 | Windows | `irm …/octop/install.ps1 \| iex` 或 `install.bat` | | 本地脚本 | macOS / Linux | `bash scripts/install.sh` | | 本地脚本 | Windows | `scripts\install.bat` 或 `install.ps1` | -| GitHub Releases | 全平台 | 桌面安装包与 wheel。不要安装 PyPI 上的上游 `octop` | +| GitHub Releases | 全平台 | 桌面安装包,以及 `freeos--py3-none-any.whl` / `freeos-.tar.gz`。**不要** `pip install octop`(那是上游 PyPI 包) | | Docker | 全平台 | `docker/docker-compose.yml` | 所有安装脚本均在 `~/.octop/venv` 创建隔离环境,并通过 `~/.octop/bin/octop` 包装 CLI,不会影响系统 Python。 diff --git a/desktop/README.md b/desktop/README.md index 9513b7c1..03f26972 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -14,13 +14,7 @@ Windows end-user product: **download the NSIS `.exe` → install → open FreeOS ## Data directory -Same precedence as the FreeOS CLI/server: - -1. `FREEOS_HOME` -2. `OCTOP_HOME` (legacy) -3. existing `~/.freeos` -4. existing `~/.octop` -5. new installs → `~/.freeos` +Today the data directory is `~/.octop` (`OCTOP_HOME`). If `FREEOS_HOME` is already set, that path is used. This release does not migrate `~/.octop` to `~/.freeos`. - Green runtime extract → `{home}/portable/` - Organization data (in-host) → `{home}/tenants/`, `{home}/openxyos-mirror/`, `{home}/org-skills/` @@ -48,7 +42,7 @@ The finish page offers **运行 FreeOS** / **Run FreeOS**, checked by default: leave it checked and click Finish to start FreeOS from `$INSTDIR`, or uncheck it and click Finish to close Setup without launching. Setup does **not** auto-launch or auto-close. The launch uses the unelevated -explorer token so the first run does not stamp `%USERPROFILE%\.freeos` +explorer token so the first run does not stamp `%USERPROFILE%\.octop` as High integrity. Silent (`/S`) installs stay headless and do not launch. Chinese installer strings are compiled with `makensis -INPUTCHARSET UTF8` from a UTF-8 BOM `project.nsi`. @@ -60,12 +54,12 @@ Upgrade / reinstall (same version included) refreshes the extracted runtime: handed off to the old process. 2. Setup writes `$INSTDIR\FREEOS_INSTALL_STAMP` (product version + tick count, unique per Setup run) and deletes - `%USERPROFILE%\.freeos\portable\FREEOS_STAMP` (also `FREEOS_HOME` / - `OCTOP_HOME` / leftover `~/.octop/portable` when those env vars are - set). User data (`octop.db`, settings, chats) is not deleted. + `%USERPROFILE%\.octop\portable\FREEOS_STAMP` (also `OCTOP_HOME`, or + `FREEOS_HOME` when that variable is already set). User data + (`octop.db`, settings, chats) is not deleted. 3. The next launch compares the install stamp and the zip `FREEOS_STAMP`. Either mismatch re-extracts `packages/` and the embedded dashboard from - the new installer into `~/.freeos/portable`, then copies the install + the new installer into `~/.octop/portable`, then copies the install stamp into that tree. Later opens skip the extract until the next Setup run. If renaming the live `portable` directory to `portable.previous` fails because Windows still has the folder open @@ -114,7 +108,7 @@ the sidecar-era `v0.0.1` Windows builds. **Keeps** -- `%USERPROFILE%\.freeos` (or `FREEOS_HOME` / `OCTOP_HOME` / legacy `~/.octop`): +- `%USERPROFILE%\.octop` (or `OCTOP_HOME`; `FREEOS_HOME` only when already set): workspaces, `octop.db`, memories, settings, logs, and the extracted portable runtime under `{home}/portable/` - Documented exceptions **inside** the install directory, if you created them: @@ -202,7 +196,7 @@ cd desktop/src OCTOP_DESKTOP_URL=http://127.0.0.1:8088 wails3 dev ``` -Without `OCTOP_DESKTOP_URL`, first launch uses `~/.freeos/portable/` if valid +Without `OCTOP_DESKTOP_URL`, first launch uses `~/.octop/portable/` if valid **and** it already matches this install (same `FREEOS_STAMP` / install stamp); otherwise it extracts the matching zip shipped with the desktop package (embedded in the Windows and Linux binaries, under `Contents/Resources` on @@ -226,7 +220,7 @@ GitHub Release names follow `FreeOS----.`: into `Program Files\FreeOS` and creates Start Menu + desktop shortcuts), `.tar.gz` (Linux) - Green runtime zip: `FreeOS-portable--.zip` -- PyPI wheels stay `octop--py3-none-any.whl` (PEP 427) +- Python wheels on GitHub Releases are `freeos--py3-none-any.whl` (import package remains `octop`). Do not `pip install octop`; PyPI `octop` is upstream Octop. This repo does not publish that name. Older `Octop-portable-*` / `Octop-.zip` files beside the binary are still accepted when extracting a local/dev build. They are **not** published anymore. diff --git a/desktop/portable/package.sh b/desktop/portable/package.sh index e8eeb411..067b4011 100755 --- a/desktop/portable/package.sh +++ b/desktop/portable/package.sh @@ -31,8 +31,8 @@ require_uv build_octop_wheel() { local wheel_dir="$1" mkdir -p "$wheel_dir" - # Drop previous local octop wheels so we pick the fresh build. - find "$wheel_dir" -maxdepth 1 -type f -name 'octop-*.whl' -delete 2>/dev/null || true + # Drop previous local wheels so we pick the fresh build. + find "$wheel_dir" -maxdepth 1 -type f \( -name 'octop-*.whl' -o -name 'freeos-*.whl' \) -delete 2>/dev/null || true if [[ ! -f "${REPO_ROOT}/src/octop/dashboard/index.html" ]]; then echo "[package] dashboard not built (src/octop/dashboard/index.html missing)." >&2 echo " Run: make build-frontend" >&2 @@ -42,12 +42,12 @@ build_octop_wheel() { echo "[package] dashboard boot splash is missing xyai-mark.png — refuse to ship Octop-branded UI." >&2 exit 1 fi - echo "[package] building octop wheel → ${wheel_dir}" >&2 + echo "[package] building freeos wheel → ${wheel_dir}" >&2 uv build --wheel --out-dir "$wheel_dir" "$REPO_ROOT" >&2 local whl - whl="$(ls -1 "${wheel_dir}"/octop-*.whl 2>/dev/null | sort | tail -1 || true)" + whl="$(ls -1 "${wheel_dir}"/freeos-*.whl 2>/dev/null | sort | tail -1 || true)" if [[ -z "$whl" ]]; then - echo "[package] failed to build octop-*.whl" >&2 + echo "[package] failed to build freeos-*.whl" >&2 exit 1 fi # stdout: wheel path only (captured by caller) diff --git a/docs/architecture-integration.md b/docs/architecture-integration.md index cf2dc5ba..9e8b098a 100644 --- a/docs/architecture-integration.md +++ b/docs/architecture-integration.md @@ -100,7 +100,7 @@ and applies asset packs onto openXYOS-shaped **in-host** surfaces │ in-host org storage + xyos-governance-mcp │ └───────┬───────────────────┬─────────────────┘ │ optional HTTP │ durable pause - │ if opted in │ ~/.freeos/governance/ + │ if opted in │ ~/.octop/governance/ ┌───────▼─────────┐ ┌─────▼─────────────────┐ Optional │ openXYOS :3780 │ │ Pause + audit JSONL │ Node sidecar │ export / sync │ │ Human must approve │ @@ -128,7 +128,7 @@ Agent → tool (outbound / delete / pay / prod) → optional POST {sidecar}/api/governance/validate → no matching allow rule → DEFAULT DENY → write audit.jsonl (and sidecar log when reachable) - → create durable pause ~/.freeos/governance/pauses/.json + → create durable pause ~/.octop/governance/pauses/.json → return execute=false (caller MUST stop) Human → FreeOS/Octop IM /approve or `freeos org governance approve ` → pause status=approved @@ -287,9 +287,8 @@ The Phase A work sits on the existing FreeOS bootstrap: - Octop history as the host (`src/octop/`, `dashboard/`) - openXYOS vendored at `modules/openxyos/` (subtree `@ 1bdba35f`) - Bundled plugin `org-os`, BFF `/api/org-module`, CLI `freeos org` -- PathLayout: `FREEOS_HOME` > `OCTOP_HOME` > existing `~/.freeos` > - existing `~/.octop` > new `~/.freeos` -- Package name stays `octop`; CLI alias `freeos` +- Data directory today: `~/.octop` (`OCTOP_HOME`). `FREEOS_HOME` is honored when already set. This release does not migrate `~/.octop` to `~/.freeos`. +- Distribution name is `freeos` (GitHub Release wheels). Imports stay `octop.*`. CLI aliases: `freeos` and `octop`. ### Capability map (host + sidecar) @@ -348,7 +347,7 @@ without `includes_org` leave those live directories untouched on restore. ## Remaining `octop` identifiers (intentional) -- Python package / imports: `octop.*` (`pyproject.toml` `name = "octop"`) +- Python imports stay `octop.*`. The distribution name in `pyproject.toml` is `freeos` (GitHub Release wheels `freeos-*`, not PyPI) - CLI: `octop` still works; `freeos` is the same entry point - Env: `OCTOP_*` still honored; prefer `FREEOS_*` - SQLite filename: `octop.db` diff --git a/docs/user-guide.md b/docs/user-guide.md index 0729a0cc..96b6cf98 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -1,6 +1,6 @@ # Octop 用户帮助文档 -> **Historical vs Current.** 本文沿自 Octop 宿主操作说明(安装、向导、模型、对话)。**Current:** 产品是 [FreeOS](../README.zh-CN.md);权威意图见 [产品契约](product-contract.zh-CN.md)。数据目录优先 `FREEOS_HOME` / `~/.freeos`(仍识别 `~/.octop`)。先在本机安顿好;组织能力像工作室里另一间可独立布置的房间。 +> **Historical vs Current.** 本文沿自 Octop 宿主操作说明(安装、向导、模型、对话)。**Current:** 产品是 [FreeOS](../README.zh-CN.md);权威意图见 [产品契约](product-contract.zh-CN.md)。数据目录今天是 `~/.octop`(`OCTOP_HOME`)。已经设置的 `FREEOS_HOME` 会沿用;这一版不会把 `~/.octop` 迁到 `~/.freeos`。先在本机安顿好;组织能力像工作室里另一间可独立布置的房间。 > 本帮助文档面向最终用户,介绍 **安装 → 设置向导 → 配置模型 → 基本使用** 的完整流程。 > 所有运行时数据默认存放在 `~/.octop/`(可通过 `OCTOP_HOME` 覆盖)。 diff --git a/fnos/README.md b/fnos/README.md index 19f9bec1..b8483e5a 100644 --- a/fnos/README.md +++ b/fnos/README.md @@ -88,7 +88,7 @@ fnos/ 1. **发版链路**:`v*` tag → `release.yml`(PyPI + GitHub Release)与 `docker-publish.yml`(GHCR / Hub)并行;Release 成功后自动 `workflow_dispatch` 本工作流。 2. **镜像复用**:不再重新 build 镜像;`ensure-image` 轮询等待 `ghcr.io/xyaistudio/freeos:{version}`(由 docker-publish 推送)。Docker 版 `.fpk` 仅打包 compose,运行时拉取该镜像。 -3. **Wheel 复用**:Native 版优先从同版本 GitHub Release(`v*`)下载 `octop-*.whl`;若缺失再回退源码构建前端 + wheel。 +3. **Wheel 复用**:Native 版优先从同版本 GitHub Release(`v*`)下载 `freeos-*.whl`;若缺失再回退源码构建前端 + wheel。更早的 Release 上可能仍是 `octop-*.whl`,那不是 PyPI 上的上游包。 4. **安装包构建**:`fpk` / `native` job 用 `scripts/build-fpk.sh` 打包,产物 `FreeOS-fnos-docker-.fpk` / `FreeOS-fnos-native-.fpk` 挂到同一个 `v*` GitHub Release(与 wheel、桌面包并列)。 ## 本地构建 .fpk(无需 Docker) diff --git a/pyproject.toml b/pyproject.toml index 0b9c75a9..37553b83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,9 @@ requires = ["hatchling>=1.18"] build-backend = "hatchling.build" [project] -name = "octop" +name = "freeos" version = "0.1.0" -description = "FreeOS — self-hosted multi-user multi-agent assistant with an optional organization OS module (Octop-compatible package name)" +description = "FreeOS — self-hosted multi-user multi-agent assistant. GitHub Release artifacts are named freeos-*; the import package stays octop. Not published to PyPI." readme = "README.md" license = { text = "MIT" } requires-python = ">=3.12" diff --git a/scripts/release_download_links.py b/scripts/release_download_links.py index 98251e69..38c7954a 100644 --- a/scripts/release_download_links.py +++ b/scripts/release_download_links.py @@ -89,6 +89,15 @@ def render_download_section(raw_version: str) -> str: "### fnOS NAS", f"- {fnos_docker} | {fnos_native}", "", + "### Python wheel and sdist", + "", + "**Do not** `pip install octop`. PyPI `octop` is upstream Octop, not FreeOS.", + "These GitHub Release files are the FreeOS distribution" + " (import package remains `octop`):", + "", + f"- {md_link('Wheel', tag, f'freeos-{version}-py3-none-any.whl')}", + f"- {md_link('sdist', tag, f'freeos-{version}.tar.gz')}", + "", ] ) diff --git a/src/octop/cli/repl/render.py b/src/octop/cli/repl/render.py index 34d7d246..55155f1f 100644 --- a/src/octop/cli/repl/render.py +++ b/src/octop/cli/repl/render.py @@ -288,7 +288,7 @@ def print_welcome(*, agent_id: str, model: str, session_key: str, thread_id: str console = Console() try: - ver = pkg_version("octop") + ver = pkg_version("freeos") except Exception: ver = "unknown" console.print(f" [bold green]Octop[/] [dim]v{ver}[/] — interactive chat") diff --git a/src/octop/infra/setup/self_update.py b/src/octop/infra/setup/self_update.py index 695b82ba..5aed9f1e 100644 --- a/src/octop/infra/setup/self_update.py +++ b/src/octop/infra/setup/self_update.py @@ -29,7 +29,7 @@ logger = logging.getLogger(__name__) -_PACKAGE_NAME = "octop" +_PACKAGE_NAME = "freeos" _GREEN_PACKAGES_ENV = "OCTOP_GREEN_PACKAGES" PENDING_PORTABLE_NAME = "pending-portable.zip" PENDING_META_NAME = "pending.json" @@ -429,7 +429,7 @@ def get_version_in_dir(python_exe: str, target: str) -> str | None: try: code = ( "import sys; sys.path.insert(0, sys.argv[1]); " - "from importlib.metadata import version; print(version('octop'))" + "from importlib.metadata import version; print(version('freeos'))" ) result = subprocess.run( [python_exe, "-c", code, target], diff --git a/tests/unit/test_release_download_links.py b/tests/unit/test_release_download_links.py index 8dca01d6..b52a9aa4 100644 --- a/tests/unit/test_release_download_links.py +++ b/tests/unit/test_release_download_links.py @@ -40,5 +40,8 @@ def test_render_download_section_matches_github_asset_names() -> None: assert f"{base}/FreeOS-portable-linux-arm64-0.9.31.zip" in body assert f"{base}/FreeOS-fnos-docker-0.9.31.fpk" in body assert f"{base}/FreeOS-fnos-native-0.9.31.fpk" in body - assert "pip install" not in body + assert f"{base}/freeos-0.9.31-py3-none-any.whl" in body + assert f"{base}/freeos-0.9.31.tar.gz" in body + assert "octop-0.9.31" not in body + assert "Do not** `pip install octop`" in body assert "## Downloads" in body diff --git a/uv.lock b/uv.lock index 6b273614..0662c6ff 100644 --- a/uv.lock +++ b/uv.lock @@ -2502,7 +2502,7 @@ wheels = [ ] [[package]] -name = "octop" +name = "freeos" version = "0.1.0" source = { editable = "." } dependencies = [ From b2f696451a6998d30a6718db5947f6b208278eff Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 21 Oct 2026 22:13:44 +0000 Subject: [PATCH 09/14] fix(install): stop pointing users at the upstream Octop script README and the user guide no longer recommend the Tencent COS octop/install URLs. Those scripts install PyPI octop. Desktop packages on GitHub Releases are the primary install path; CLI installs use this repo's develop scripts. New installs use ~/.freeos. install.sh, install.ps1, and install.bat follow PathLayout: FREEOS_HOME, OCTOP_HOME, an existing ~/.freeos, an existing ~/.octop when ~/.freeos is absent, otherwise ~/.freeos. The desktop app still does not adopt a leftover ~/.octop. Co-authored-by: haoboy007-sudo --- CHANGELOG.md | 2 +- README.md | 6 +-- README_CN.md | 57 +++++++++++++------------- desktop/README.md | 17 ++++---- docs/architecture-integration.md | 6 +-- docs/user-guide.html | 26 ++++++------ docs/user-guide.md | 36 +++++++++-------- scripts/README.md | 17 +++++--- scripts/install.bat | 18 +++++++-- scripts/install.ps1 | 50 ++++++++++++++++++++--- scripts/install.sh | 69 +++++++++++++++++++++++++++++--- 11 files changed, 208 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83341437..07eaa8eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ FreeOS **0.1.0** is an independent semver baseline. It does not track or align w ### Upgrade from 0.0.10 -- In-app update and a manual reinstall keep the data directory the process already uses. Today that directory is `~/.octop` (`OCTOP_HOME`): sessions, local models, knowledge bases, organization data, and `config.json`. `FREEOS_HOME` is honored only when it is already set. This release does not migrate `~/.octop` to `~/.freeos`. +- In-app update and a manual reinstall keep the data directory the process already uses. New installs use `~/.freeos` (`FREEOS_HOME`, then `OCTOP_HOME`, then an existing `~/.freeos`; the desktop app does not adopt a leftover `~/.octop`). CLI installs keep an existing `~/.octop` when `~/.freeos` is not there yet. Sessions, local models, knowledge bases, organization data, and `config.json` stay in that directory. This release does not copy one tree into the other. - Desktop portable upgrades copy a SQLite backup under the data directory's `backups/` before replacing the runtime, and checkpoint the WAL before the process restarts. - A `config.json` that cannot be parsed is left in place and reported. It is not rewritten as an empty file. - Roll back by installing the **v0.0.10** desktop package again, or restore a backup with `octop backup restore`. Do not point install scripts at PyPI `octop`; that package is upstream Octop, not this release. Use [GitHub Releases](https://github.com/XYAIStudio/FreeOS/releases) or `scripts/install.sh` (add `--from-pypi` only when you explicitly want upstream Octop). diff --git a/README.md b/README.md index 369537f8..09bbf633 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ Operator detail: [docs/asset-loop.md](docs/asset-loop.md). **不要**执行 `pip install octop`。PyPI 上的 `octop` 是上游 Octop,不是 FreeOS。本仓库的 Python 制品在 GitHub Releases 上,文件名是 `freeos--py3-none-any.whl` 和 `freeos-.tar.gz`(导入包仍是 `octop`)。桌面安装包仍是 `FreeOS-desktop-*`。 -数据目录是 `%USERPROFILE%\.octop`(也可用 `OCTOP_HOME` 指定)。已经设置的 `FREEOS_HOME` 会被沿用;这一版不会把 `~/.octop` 迁到 `~/.freeos`。卸载安装包会清空安装目录(默认为 `Program Files\FreeOS`)并删除快捷方式,但**不会**删除该用户数据目录;详见 [desktop/README.md](desktop/README.md#windows-uninstall)。 +新安装的数据目录是 `%USERPROFILE%\.freeos`(`FREEOS_HOME` 优先于 `OCTOP_HOME`)。桌面端不会把残留的 `~/.octop` 当成家目录。命令行安装在还没有 `~/.freeos`、但已经有 `~/.octop` 时继续用旧目录。卸载安装包会清空安装目录(默认为 `Program Files\FreeOS`)并删除快捷方式,但**不会**删除该用户数据目录;详见 [desktop/README.md](desktop/README.md#windows-uninstall)。 安装包由 CI 工作流 **FreeOS Desktop Package**(文件名仍是 `.github/workflows/octop-desktop.yml`,给现有发版脚本用)在 Windows runner 上打出来。本仓库的云环境打不出 `.exe`;合并后由该 job 产出。 @@ -170,7 +170,7 @@ FreeOS prefers runtimes and files on this computer. Cloud vendors are optional. | Knowledge | Sidebar **Knowledge Bases**: mount a local folder first. Local ONNX embeddings are the default vector path. ima / WeKnora remain optional connectors. | | Detail | [User guide](docs/user-guide.md#四配置模型llm-供应商) · [Configuration](docs/configuration.md#local-models-and-knowledge-bases) · [Product contract](docs/product-contract.md#local-models-and-knowledge-bases) | -Data directory: `~/.octop` (`OCTOP_HOME`). If `FREEOS_HOME` is already set, that path is used. This release does not migrate `~/.octop` to `~/.freeos`. +Data directory for a new install: `~/.freeos`. Order: `FREEOS_HOME`, `OCTOP_HOME`, an existing `~/.freeos`, an existing `~/.octop` (CLI only; the desktop app does not adopt a leftover `~/.octop`), otherwise `~/.freeos`. This release does not copy an old tree into the new one. ### Rebuild the dashboard (optional) @@ -235,7 +235,7 @@ Inbound import compiles the blueprint **and** registers a FreeOS agent (`org-pol ## Host platform features (from Octop) -Unchanged in this fork: multi-user JWT, multi-agent chat, expert library, connectors, ACP, knowledge bases, bundled plugins, cron, terminal/browser/desktop surfaces. The **Windows desktop installer** is branded **FreeOS**. User data stays in `~/.octop` (`OCTOP_HOME`); `FREEOS_HOME` is honored only when it is already set. The import package, `octop` CLI alias, `OCTOP_*` env vars, and `octop.db` stay for compatibility. GitHub Release wheels are named `freeos-*`. +Unchanged in this fork: multi-user JWT, multi-agent chat, expert library, connectors, ACP, knowledge bases, bundled plugins, cron, terminal/browser/desktop surfaces. The **Windows desktop installer** is branded **FreeOS** and stores new data in `~/.freeos`. The import package, `octop` CLI alias, `OCTOP_*` env vars, and `octop.db` stay for compatibility. GitHub Release wheels are named `freeos-*`. Do not install from the Tencent COS `octop/install.*` URLs; those scripts install upstream PyPI `octop`. Longer host docs: [docs/user-guide.md](docs/user-guide.md), [docs/configuration.md](docs/configuration.md), [docs/architecture.md](docs/architecture.md), [README_CN.md](README_CN.md). diff --git a/README_CN.md b/README_CN.md index c02c6de7..d0706fe3 100644 --- a/README_CN.md +++ b/README_CN.md @@ -91,7 +91,7 @@ FreeOS 诞生于一个简单而清晰的目标:在 Octop 的自托管多智能 | 💻 | **终端 AI+** | 浏览器内交互式 Shell,AI 辅助命令执行与排障 | | 🌐 | **浏览器 AI+** | 基于 Chromium 的无头浏览器会话,支持网页自动化、截图与远程操控 | | 🖥️ | **远程桌面** | 控制台内实时看屏与键鼠操控,跨 Linux / Windows / macOS;适合远程办公、GUI 软件操作,无图形 Linux 可一键搭建隔离桌面 | -| 🏠 | **可自托管** | 一条 `octop run` 即可运行控制台、CLI、IM 通道与定时任务,数据存于 `~/.octop/` | +| 🏠 | **可自托管** | 一条 `octop run` 即可运行控制台、CLI、IM 通道与定时任务,新安装的数据在 `~/.freeos/` |
🐾 你能用 Octop 做什么 @@ -128,7 +128,7 @@ Octop 基于一系列 Harness 工程实践构建——它将这些专注的运 - **harness-memory** — 分层记忆与全文检索,让 Agent 的记忆随工作区一同迁移。 - **harness-browser** — 基于 CDP 的浏览器自动化,支持持久化配置,用于网页类任务。 -Octop 不依赖外部消息队列或中间件,而是通过进程内的 `HarnessProcessor` 统一路由所有入口——Web UI、IM 与定时任务。最终呈现为一个可重启恢复的单进程:启动时整个状态都从控制面数据库重建(默认 `~/.octop/octop.db`,亦可配置 PostgreSQL)。 +Octop 不依赖外部消息队列或中间件,而是通过进程内的 `HarnessProcessor` 统一路由所有入口——Web UI、IM 与定时任务。最终呈现为一个可重启恢复的单进程:启动时整个状态都从控制面数据库重建(新安装默认 `~/.freeos/octop.db`;已有 `~/.octop` 且没有 `~/.freeos` 时仍用旧目录,亦可配置 PostgreSQL)。 ## 🤔 功能特性 @@ -192,30 +192,32 @@ Octop 支持两个方向的 ACP 集成: ### 环境要求 - **macOS / Linux / Windows** -- 无需预先安装 Python — 安装脚本通过 [uv](https://docs.astral.sh/uv/) 在 `~/.octop/` 下创建隔离的 Python 3.12 虚拟环境 +- 无需预先安装 Python — 安装脚本通过 [uv](https://docs.astral.sh/uv/) 在 `~/.freeos/` 下创建隔离的 Python 3.12 虚拟环境(若还没有 `~/.freeos`、但已有 `~/.octop`,则继续用旧目录) - 现代多核 CPU,并预留数 GB 内存供进程与模型/Embedding 缓存使用;磁盘需容纳数据库、Agent 工作区与文档语料 ### 1. 安装 -**macOS / Linux** — 一键安装(推荐): +**推荐:桌面安装包。** 从 [GitHub Releases](https://github.com/XYAIStudio/FreeOS/releases/latest) 下载 `FreeOS-desktop-*`,安装后打开 FreeOS。 + +**命令行 / 源码**(安装本仓库脚本,不是上游 Octop)。打出版本标签之后,Release 说明会改成固定标签;当前用 `develop`: ```bash -curl -fsSL https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.sh | bash ``` -**Windows(PowerShell)**: - ```powershell -irm https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install.ps1 | iex +irm https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.ps1 | iex ``` -**Windows(cmd)** — 下载后运行,或从已克隆的仓库执行: - ```bat -curl -fsSL https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install.bat -o install.bat +curl -fsSL https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.bat -o install.bat install.bat ``` +已克隆本仓库时:`bash scripts/install.sh`,或 Windows 的 `scripts\install.ps1` / `scripts\install.bat`。 + +**不要**使用 `finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install.sh`(以及同目录的 `.ps1` / `.bat`)。那是上游 Octop 脚本,默认安装 PyPI 上的 `octop`,不是 FreeOS。 + 安装完成后,请打开**新终端**,或重新加载 shell 配置: ```bash @@ -224,14 +226,11 @@ source ~/.zshrc # Zsh source ~/.bashrc # Bash ``` -安装脚本会将 `octop` 加入 PATH(`~/.octop/bin`)。可选附加组件: +新安装会把 `octop` 加入 PATH(`~/.freeos/bin`)。可选附加组件: ```bash -# 浏览器自动化(Playwright Chromium) -curl -fsSL https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install.sh | bash -s -- --extras browser - -# 飞书通道支持 -curl -fsSL https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install.sh | bash -s -- --extras channels-feishu +curl -fsSL https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.sh | bash -s -- --extras browser +curl -fsSL https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.sh | bash -s -- --extras channels-feishu ``` 完整安装选项见 [scripts/README.md](scripts/README.md)(`--version`、`--from-source`、`--mirror` 及 Windows 参数)。 @@ -331,18 +330,16 @@ docker run -d \ | 方式 | 平台 | 说明 | |------|------|------| -| 远程一键安装 | macOS / Linux | `curl …/octop/install.sh \| bash` | -| 远程一键安装 | Windows | `irm …/octop/install.ps1 \| iex` 或 `install.bat` | -| 本地脚本 | macOS / Linux | `bash scripts/install.sh` | -| 本地脚本 | Windows | `scripts\install.bat` 或 `install.ps1` | -| GitHub Releases | 全平台 | 桌面安装包,以及 `freeos--py3-none-any.whl` / `freeos-.tar.gz`。**不要** `pip install octop`(那是上游 PyPI 包) | +| GitHub Releases(推荐) | 全平台 | `FreeOS-desktop-*` 桌面安装包,以及 `freeos--py3-none-any.whl` / `freeos-.tar.gz`。**不要** `pip install octop`(那是上游 PyPI 包) | +| 本仓库脚本 | macOS / Linux | `bash scripts/install.sh`,或 `raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.sh` | +| 本仓库脚本 | Windows | `scripts\install.ps1` / `install.bat`,或同路径的 develop raw 脚本 | | Docker | 全平台 | `docker/docker-compose.yml` | -所有安装脚本均在 `~/.octop/venv` 创建隔离环境,并通过 `~/.octop/bin/octop` 包装 CLI,不会影响系统 Python。 +新安装的数据目录是 `~/.freeos`:脚本在 `~/.freeos/venv` 创建隔离环境,并用 `~/.freeos/bin/octop` 包装 CLI,不会影响系统 Python。若还没有 `~/.freeos`、但已经有 `~/.octop`,脚本和 CLI 继续用旧目录。`FREEOS_HOME` 优先于 `OCTOP_HOME`。桌面端不会把残留的 `~/.octop` 当成家目录。 ### 升级 -`octop update` 只替换 wheel / 二进制,你的 `~/.octop/` 数据库、工作区、密钥与 `config.json` 均会保留: +`octop update` 只替换 wheel / 二进制。数据留在当前家目录(新安装是 `~/.freeos/`;已在使用的 `~/.octop/` 会保留)里的数据库、工作区、密钥与 `config.json`: ```bash octop update # 获取并安装最新版 Octop,若已注册系统服务则自动重启 @@ -352,7 +349,7 @@ octop update # 获取并安装最新版 Octop,若已注册系统服 ### ⚙️ 配置 -所有运行时数据存放在 `~/.octop/`。可通过 CLI 管理,也可直接编辑文件。 +新安装的运行时数据在 `~/.freeos/`。已有 `~/.octop/` 且还没有 `~/.freeos/` 时,CLI 继续用旧目录。可通过 CLI 管理,也可直接编辑文件。 ```bash # LLM 供应商与模型 @@ -393,7 +390,7 @@ octop user list | 命令 | 说明 | |------|------| -| `octop init` | 初始化 `~/.octop/`(数据库、管理员、JWT 密钥) | +| `octop init` | 初始化数据目录(新安装为 `~/.freeos/`;数据库、管理员、JWT 密钥) | | `octop run` | 前台启动 Octop | | `octop service start` | 安装并启动系统服务 | | `octop service stop` | 停止系统服务 | @@ -406,7 +403,7 @@ octop user list | `octop skills` | 按 Agent 启用/禁用 Skill | | `octop plugin` | 安装并管理第三方插件 | | `octop backup` | 导出 / 恢复备份 | -| `octop clean` | 清理 CLI 状态或清空 `~/.octop/` | +| `octop clean` | 清理 CLI 状态或清空当前数据目录 | | `octop update` | 检查并安装更新 | 完整参考:**[docs/cli.md](docs/cli.md)**。 @@ -434,7 +431,7 @@ octop user list ### 📁 数据目录 ``` -~/.octop/ ← 安装与数据根目录 +~/.freeos/ ← 新安装的数据根目录(已有 ~/.octop 且没有 ~/.freeos 时仍用旧目录) ├── config.json # 进程配置(含可选 database 段) ├── octop.db # 默认 SQLite 控制面(用户、Agent、通道、定时任务 …) ├── secrets/ # JWT 密钥、通道 Token @@ -509,10 +506,10 @@ cd dashboard && npx tsc --noEmit ### 🔒 安全与隐私 -- **本地优先**:配置、对话、工作区与凭证均存储在 `~/.octop/`。 +- **本地优先**:配置、对话、工作区与凭证存在本机数据目录。新安装是 `~/.freeos/`;已有的 `~/.octop/` 在没有 `~/.freeos/` 时继续使用。 - **多用户隔离**:JWT 认证,按用户隔离 Agent 与工作区。 - **敏感信息脱敏与工具审批**:离开工作区前自动脱敏敏感数据;高风险工具或 Shell 命令需依据护栏规则显式审批。 -- **工具护栏**:可在 `~/.octop/security/tool_guard/` 编辑 Shell 命令规则。 +- **工具护栏**:可在数据目录的 `security/tool_guard/` 编辑 Shell 命令规则(新安装为 `~/.freeos/security/tool_guard/`)。 - **无厂商锁定**:可自由切换 LLM 供应商、存储后端与 IM 通道。 ### 🤝 参与贡献 diff --git a/desktop/README.md b/desktop/README.md index 03f26972..48ede4e9 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -14,7 +14,7 @@ Windows end-user product: **download the NSIS `.exe` → install → open FreeOS ## Data directory -Today the data directory is `~/.octop` (`OCTOP_HOME`). If `FREEOS_HOME` is already set, that path is used. This release does not migrate `~/.octop` to `~/.freeos`. +New desktop installs use `~/.freeos`. Order: `FREEOS_HOME`, then `OCTOP_HOME`, then `~/.freeos`. A leftover `~/.octop` is not the desktop home. CLI installs still keep that legacy tree when `~/.freeos` does not exist yet. This release does not copy one tree into the other. - Green runtime extract → `{home}/portable/` - Organization data (in-host) → `{home}/tenants/`, `{home}/openxyos-mirror/`, `{home}/org-skills/` @@ -42,7 +42,7 @@ The finish page offers **运行 FreeOS** / **Run FreeOS**, checked by default: leave it checked and click Finish to start FreeOS from `$INSTDIR`, or uncheck it and click Finish to close Setup without launching. Setup does **not** auto-launch or auto-close. The launch uses the unelevated -explorer token so the first run does not stamp `%USERPROFILE%\.octop` +explorer token so the first run does not stamp `%USERPROFILE%\.freeos` as High integrity. Silent (`/S`) installs stay headless and do not launch. Chinese installer strings are compiled with `makensis -INPUTCHARSET UTF8` from a UTF-8 BOM `project.nsi`. @@ -54,12 +54,13 @@ Upgrade / reinstall (same version included) refreshes the extracted runtime: handed off to the old process. 2. Setup writes `$INSTDIR\FREEOS_INSTALL_STAMP` (product version + tick count, unique per Setup run) and deletes - `%USERPROFILE%\.octop\portable\FREEOS_STAMP` (also `OCTOP_HOME`, or - `FREEOS_HOME` when that variable is already set). User data - (`octop.db`, settings, chats) is not deleted. + `%USERPROFILE%\.freeos\portable\FREEOS_STAMP` (also `FREEOS_HOME` / + `OCTOP_HOME` when those variables are set). A leftover `~/.octop` + is not the desktop home. User data (`octop.db`, settings, chats) is + not deleted. 3. The next launch compares the install stamp and the zip `FREEOS_STAMP`. Either mismatch re-extracts `packages/` and the embedded dashboard from - the new installer into `~/.octop/portable`, then copies the install + the new installer into `~/.freeos/portable`, then copies the install stamp into that tree. Later opens skip the extract until the next Setup run. If renaming the live `portable` directory to `portable.previous` fails because Windows still has the folder open @@ -108,7 +109,7 @@ the sidecar-era `v0.0.1` Windows builds. **Keeps** -- `%USERPROFILE%\.octop` (or `OCTOP_HOME`; `FREEOS_HOME` only when already set): +- `%USERPROFILE%\.freeos` (or `FREEOS_HOME` / `OCTOP_HOME`; not a leftover `~/.octop`): workspaces, `octop.db`, memories, settings, logs, and the extracted portable runtime under `{home}/portable/` - Documented exceptions **inside** the install directory, if you created them: @@ -196,7 +197,7 @@ cd desktop/src OCTOP_DESKTOP_URL=http://127.0.0.1:8088 wails3 dev ``` -Without `OCTOP_DESKTOP_URL`, first launch uses `~/.octop/portable/` if valid +Without `OCTOP_DESKTOP_URL`, first launch uses `~/.freeos/portable/` if valid **and** it already matches this install (same `FREEOS_STAMP` / install stamp); otherwise it extracts the matching zip shipped with the desktop package (embedded in the Windows and Linux binaries, under `Contents/Resources` on diff --git a/docs/architecture-integration.md b/docs/architecture-integration.md index 9e8b098a..cbc35039 100644 --- a/docs/architecture-integration.md +++ b/docs/architecture-integration.md @@ -100,7 +100,7 @@ and applies asset packs onto openXYOS-shaped **in-host** surfaces │ in-host org storage + xyos-governance-mcp │ └───────┬───────────────────┬─────────────────┘ │ optional HTTP │ durable pause - │ if opted in │ ~/.octop/governance/ + │ if opted in │ ~/.freeos/governance/ ┌───────▼─────────┐ ┌─────▼─────────────────┐ Optional │ openXYOS :3780 │ │ Pause + audit JSONL │ Node sidecar │ export / sync │ │ Human must approve │ @@ -128,7 +128,7 @@ Agent → tool (outbound / delete / pay / prod) → optional POST {sidecar}/api/governance/validate → no matching allow rule → DEFAULT DENY → write audit.jsonl (and sidecar log when reachable) - → create durable pause ~/.octop/governance/pauses/.json + → create durable pause ~/.freeos/governance/pauses/.json → return execute=false (caller MUST stop) Human → FreeOS/Octop IM /approve or `freeos org governance approve ` → pause status=approved @@ -287,7 +287,7 @@ The Phase A work sits on the existing FreeOS bootstrap: - Octop history as the host (`src/octop/`, `dashboard/`) - openXYOS vendored at `modules/openxyos/` (subtree `@ 1bdba35f`) - Bundled plugin `org-os`, BFF `/api/org-module`, CLI `freeos org` -- Data directory today: `~/.octop` (`OCTOP_HOME`). `FREEOS_HOME` is honored when already set. This release does not migrate `~/.octop` to `~/.freeos`. +- Data directory for a new install: `~/.freeos`. Order matches `PathLayout.from_env`: `FREEOS_HOME`, `OCTOP_HOME`, existing `~/.freeos`, existing `~/.octop` (not for the desktop app), else `~/.freeos`. This release does not copy one tree into the other. - Distribution name is `freeos` (GitHub Release wheels). Imports stay `octop.*`. CLI aliases: `freeos` and `octop`. ### Capability map (host + sidecar) diff --git a/docs/user-guide.html b/docs/user-guide.html index d47006d8..8f864402 100644 --- a/docs/user-guide.html +++ b/docs/user-guide.html @@ -173,7 +173,7 @@

本帮助文档面向最终用户,介绍 安装 → 设置向导 → 配置模型 → 基本使用 的完整流程。 -所有运行时数据默认存放在 ~/.octop/(可通过 OCTOP_HOME 覆盖)。

+新安装的运行时数据在 ~/.freeos/。已有 ~/.octop/ 且还没有 ~/.freeos/ 时,CLI 继续用旧目录。


一、简介

@@ -193,26 +193,26 @@

二、安装 Octop

2.1 环境要求

  • 操作系统:macOS / Linux / Windows。
  • -
  • 无需 预先安装 Python —— 安装脚本会通过 uv 在 ~/.octop/ 下自动创建隔离的 Python 3.12 虚拟环境。
  • +
  • 无需 预先安装 Python —— 本仓库安装脚本会通过 uv 在 ~/.freeos/ 下自动创建隔离的 Python 3.12 虚拟环境(已有 ~/.octop/ 且还没有 ~/.freeos/ 时继续用旧目录)。
  • 需要可访问外网,用于下载安装脚本与依赖。
-

2.2 一键脚本安装(推荐)

-

macOS / Linux

-
curl -fsSL https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install.sh | bash
+

2.2 安装

+

推荐:从 GitHub Releases 下载 FreeOS-desktop-*。

+

命令行使用本仓库脚本(当前 develop;打 tag 后由 Release 说明固定标签):

+
curl -fsSL https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.sh | bash
 
-

Windows(PowerShell)

-
irm https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install.ps1 | iex
+
irm https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.ps1 | iex
 
-

Windows(cmd) —— 先下载再运行:

-
curl -fsSL https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install.bat -o install.bat
+
curl -fsSL https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.bat -o install.bat
 install.bat
 
+

不要使用腾讯云 COS 上的 …/octop/install.sh(以及 .ps1 / .bat)。那是上游 Octop,默认安装 PyPI 上的 octop,不是 FreeOS。

安装完成后,打开一个新终端 或重新加载 shell 配置,使 PATH 生效:

source ~/.zshrc   # Zsh
 # 或
 source ~/.bashrc  # Bash
 
-

安装脚本会把 octop 命令放入 ~/.octop/bin 并加入 PATH,并在 ~/.octop/venv 创建隔离环境;不会改动系统 Python。

+

新安装会把 octop 放入 ~/.freeos/bin 并加入 PATH,并在 ~/.freeos/venv 创建隔离环境;不会改动系统 Python。

可选附加组件:安装脚本支持通过 --extras 追加能力,例如浏览器自动化 --extras browser、飞书通道 --extras channels-feishu;也可用 --version 指定版本、--mirror <url> 使用国内 PyPI 镜像。更多选项见 scripts/README.md。

@@ -220,7 +220,7 @@

2.3 验证安装

octop --version
 octop run --help
 
-

若提示 command not found: octop,请确认已重新加载 shell 或检查 ~/.octop/bin 是否在 PATH 中。

+

若提示 command not found: octop,请确认已重新加载 shell,或检查 ~/.freeos/bin(旧安装则是 ~/.octop/bin)是否在 PATH 中。

2.4 Docker 安装(生产推荐)

# 构建并后台启动
 docker compose -f docker/docker-compose.yml up -d
@@ -579,8 +579,8 @@ 

七、常见问题

Q:如何修改监听地址让局域网访问? - 启动时:octop run --host 0.0.0.0 --port 8088;或设置环境变量 OCTOP_BIND_HOST=0.0.0.0、OCTOP_PORT=8088。

Q:数据存放在哪里? -- 全部在 ~/.octop/:

-
~/.octop/
+- 新安装在 ~/.freeos/。还没有 ~/.freeos/ 时,已有的 ~/.octop/ 仍给 CLI 使用。桌面端不用残留的 ~/.octop 当家目录。

+
~/.freeos/
 ├── config.json              # 进程级配置(地址、端口、CORS、TLS …)
 ├── octop.db                 # SQLite — 用户、Agent、通道、定时任务 …
 ├── secrets/                 # JWT 密钥、通道 Token
diff --git a/docs/user-guide.md b/docs/user-guide.md
index 96b6cf98..1220316e 100644
--- a/docs/user-guide.md
+++ b/docs/user-guide.md
@@ -1,9 +1,9 @@
 # Octop 用户帮助文档
 
-> **Historical vs Current.** 本文沿自 Octop 宿主操作说明(安装、向导、模型、对话)。**Current:** 产品是 [FreeOS](../README.zh-CN.md);权威意图见 [产品契约](product-contract.zh-CN.md)。数据目录今天是 `~/.octop`(`OCTOP_HOME`)。已经设置的 `FREEOS_HOME` 会沿用;这一版不会把 `~/.octop` 迁到 `~/.freeos`。先在本机安顿好;组织能力像工作室里另一间可独立布置的房间。
+> **Historical vs Current.** 本文沿自 Octop 宿主操作说明(安装、向导、模型、对话)。**Current:** 产品是 [FreeOS](../README.zh-CN.md);权威意图见 [产品契约](product-contract.zh-CN.md)。新安装的数据目录是 `~/.freeos`。`FREEOS_HOME` 优先于 `OCTOP_HOME`;还没有 `~/.freeos` 时,已有的 `~/.octop` 仍给 CLI 使用。桌面端不会把残留的 `~/.octop` 当成家目录。先在本机安顿好;组织能力像工作室里另一间可独立布置的房间。
 
 > 本帮助文档面向最终用户,介绍 **安装 → 设置向导 → 配置模型 → 基本使用** 的完整流程。
-> 所有运行时数据默认存放在 `~/.octop/`(可通过 `OCTOP_HOME` 覆盖)。
+> 新安装的运行时数据在 `~/.freeos/`。已有 `~/.octop/` 且还没有 `~/.freeos/` 时,CLI 继续用旧目录。
 
 ---
 
@@ -66,30 +66,32 @@
 ### 2.1 环境要求
 
 - 操作系统:**macOS / Linux / Windows**。
-- **无需** 预先安装 Python —— 安装脚本会通过 [uv](https://docs.astral.sh/uv/) 在 `~/.octop/` 下自动创建隔离的 Python 3.12 虚拟环境。
+- **无需** 预先安装 Python —— 本仓库安装脚本会通过 [uv](https://docs.astral.sh/uv/) 在 `~/.freeos/` 下自动创建隔离的 Python 3.12 虚拟环境(已有 `~/.octop/` 且还没有 `~/.freeos/` 时继续用旧目录)。
 - 需要可访问外网,用于下载安装脚本与依赖。
 
-### 2.2 一键脚本安装(推荐)
+### 2.2 安装
 
-**macOS / Linux**
+**推荐:桌面安装包。** 从 [GitHub Releases](https://github.com/XYAIStudio/FreeOS/releases/latest) 下载 `FreeOS-desktop-*`。
+
+**命令行**使用本仓库脚本。打出版本标签之后,Release 说明会改成固定标签;当前用 `develop`:
 
 ```bash
-curl -fsSL https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install.sh | bash
+curl -fsSL https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.sh | bash
 ```
 
-**Windows(PowerShell)**
-
 ```powershell
-irm https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install.ps1 | iex
+irm https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.ps1 | iex
 ```
 
-**Windows(cmd)** —— 先下载再运行:
-
 ```bat
-curl -fsSL https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install.bat -o install.bat
+curl -fsSL https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.bat -o install.bat
 install.bat
 ```
 
+已克隆仓库时:`bash scripts/install.sh`。
+
+**不要**使用腾讯云 COS 上的 `…/octop/install.sh`(以及 `.ps1` / `.bat`)。那是上游 Octop,默认安装 PyPI 上的 `octop`,不是 FreeOS。
+
 安装完成后,**打开一个新终端** 或重新加载 shell 配置,使 PATH 生效:
 
 ```bash
@@ -98,9 +100,9 @@ source ~/.zshrc   # Zsh
 source ~/.bashrc  # Bash
 ```
 
-安装脚本会把 `octop` 命令放入 `~/.octop/bin` 并加入 PATH,并在 `~/.octop/venv` 创建隔离环境;**不会改动系统 Python**。
+新安装会把 `octop` 放入 `~/.freeos/bin` 并加入 PATH,并在 `~/.freeos/venv` 创建隔离环境;**不会改动系统 Python**。
 
-> **可选附加组件**:安装脚本默认从 FreeOS GitHub 仓库安装,支持通过 `--extras` 追加能力,例如浏览器自动化 `--extras browser`、飞书通道 `--extras channels-feishu`;也可用 `--version` 指定 GitHub 版本标签、`--mirror ` 使用国内 PyPI 镜像下载 Python 依赖。更多选项见 [scripts/README.md](../scripts/README.md)。
+> **可选附加组件**:本仓库脚本支持 `--extras browser`、`--extras channels-feishu`,也可用 `--version` 指定 GitHub 版本标签。更多选项见 [scripts/README.md](../scripts/README.md)。
 
 ### 2.3 验证安装
 
@@ -109,7 +111,7 @@ octop --version
 octop run --help
 ```
 
-若提示 `command not found: octop`,请确认已重新加载 shell 或检查 `~/.octop/bin` 是否在 PATH 中。
+若提示 `command not found: octop`,请确认已重新加载 shell,或检查 `~/.freeos/bin`(旧安装则是 `~/.octop/bin`)是否在 PATH 中。
 
 ### 2.4 Docker 安装(生产推荐)
 
@@ -452,10 +454,10 @@ Octop 支持两个方向的 ACP 集成:
 - 启动时:`octop run --host 0.0.0.0 --port 8088`;或设置环境变量 `OCTOP_BIND_HOST=0.0.0.0`、`OCTOP_PORT=8088`。
 
 **Q:数据存放在哪里?**
-- 全部在 `~/.octop/`:
+- 新安装在 `~/.freeos/`。还没有 `~/.freeos/` 时,已有的 `~/.octop/` 仍给 CLI 使用。桌面端不用残留的 `~/.octop` 当家目录。
 
 ```
-~/.octop/
+~/.freeos/
 ├── config.json              # 进程级配置(地址、端口、CORS、TLS、database …)
 ├── octop.db                 # 默认 SQLite 控制面 — 用户、Agent、通道、定时任务 …
 ├── secrets/                 # JWT 密钥、通道 Token
diff --git a/scripts/README.md b/scripts/README.md
index 556c04cb..61228121 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -4,15 +4,19 @@
 
 ## 安装 Octop(终端用户)
 
-将 Octop 安装到隔离虚拟环境 `~/.octop/venv`,并创建 `~/.octop/bin/octop` 包装脚本加入 PATH。
+新安装使用 `~/.freeos/venv` 与 `~/.freeos/bin/octop`。还没有 `~/.freeos`、但已经有 `~/.octop` 时,继续用旧目录。
 
-### 远程一键安装(推荐)
+**不要**使用腾讯云 COS 上的 `…/octop/install.*`。那是上游 Octop,默认安装 PyPI `octop`,不是 FreeOS。
+
+桌面用户优先下载 GitHub Releases 的 `FreeOS-desktop-*`。
+
+### 远程脚本(develop;打 tag 后由 Release 说明固定标签)
 
 | 平台 | 命令 |
 |------|------|
-| macOS / Linux | `curl -fsSL https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install.sh \| bash` |
-| Windows (PowerShell) | `irm https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install.ps1 \| iex` |
-| Windows (cmd) | 下载 `…/octop/install.bat` 后运行,或使用下方本地脚本 |
+| macOS / Linux | `curl -fsSL https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.sh \| bash` |
+| Windows (PowerShell) | `irm https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.ps1 \| iex` |
+| Windows (cmd) | 下载 `https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/install.bat` 后运行 |
 
 ### 本地脚本(仓库内)
 
@@ -52,7 +56,8 @@ Windows PowerShell 等价参数:`-Version`、`-FromSource`、`-FromPyPI`、`-S
 
 | 变量 | 说明 |
 |------|------|
-| `OCTOP_HOME` | 安装根目录,默认 `~/.octop` |
+| `FREEOS_HOME` | 安装根目录(优先) |
+| `OCTOP_HOME` | 旧安装根目录覆盖。未设置时:已有 `~/.freeos`,否则已有 `~/.octop`,否则新建 `~/.freeos` |
 | `OCTOP_REPO` | `--from-source` 无本地目录时的 git 克隆地址 |
 | `OCTOP_PYPI_MIRROR` | PyPI 镜像(与 `--mirror` 等效) |
 | `PLAYWRIGHT_DOWNLOAD_HOST` | Playwright 浏览器下载镜像(仅 `--extras browser`) |
diff --git a/scripts/install.bat b/scripts/install.bat
index 3980721e..cbdc1003 100644
--- a/scripts/install.bat
+++ b/scripts/install.bat
@@ -4,7 +4,17 @@ setlocal EnableDelayedExpansion
 REM Octop Installer for Windows (cmd.exe)
 REM Usage: install.bat [-Version X.Y.Z] [-FromSource | -FromPyPI] [-SourceDir DIR] [-Extras browser] [-Help]
 
-if defined OCTOP_HOME (set "OCTOP_HOME=%OCTOP_HOME%") else (set "OCTOP_HOME=%USERPROFILE%\.octop")
+if defined FREEOS_HOME (
+    set "OCTOP_HOME=%FREEOS_HOME%"
+) else if not defined OCTOP_HOME (
+    if exist "%USERPROFILE%\.freeos" (
+        set "OCTOP_HOME=%USERPROFILE%\.freeos"
+    ) else if exist "%USERPROFILE%\.octop" (
+        set "OCTOP_HOME=%USERPROFILE%\.octop"
+    ) else (
+        set "OCTOP_HOME=%USERPROFILE%\.freeos"
+    )
+)
 set "OCTOP_VENV=%OCTOP_HOME%\venv"
 set "OCTOP_BIN=%OCTOP_HOME%\bin"
 set "PYTHON_VERSION=3.12"
@@ -147,8 +157,10 @@ if errorlevel 1 (
 if not exist "%OCTOP_BIN%" mkdir "%OCTOP_BIN%"
 
 echo @echo off > "%OCTOP_BIN%\octop.cmd"
-echo set "OCTOP_HOME=%%OCTOP_HOME%%" >> "%OCTOP_BIN%\octop.cmd"
-echo if "%%OCTOP_HOME%%"=="" set "OCTOP_HOME=%%USERPROFILE%%\.octop" >> "%OCTOP_BIN%\octop.cmd"
+echo if defined FREEOS_HOME set "OCTOP_HOME=%%FREEOS_HOME%%" >> "%OCTOP_BIN%\octop.cmd"
+echo if not defined OCTOP_HOME if exist "%%USERPROFILE%%\.freeos" set "OCTOP_HOME=%%USERPROFILE%%\.freeos" >> "%OCTOP_BIN%\octop.cmd"
+echo if not defined OCTOP_HOME if exist "%%USERPROFILE%%\.octop" set "OCTOP_HOME=%%USERPROFILE%%\.octop" >> "%OCTOP_BIN%\octop.cmd"
+echo if not defined OCTOP_HOME set "OCTOP_HOME=%%USERPROFILE%%\.freeos" >> "%OCTOP_BIN%\octop.cmd"
 echo "%%OCTOP_HOME%%\venv\Scripts\octop.exe" %%* >> "%OCTOP_BIN%\octop.cmd"
 
 set "OCTOP_BIN_FOR_PS=%OCTOP_BIN%"
diff --git a/scripts/install.ps1 b/scripts/install.ps1
index 4b891c1d..506f4875 100644
--- a/scripts/install.ps1
+++ b/scripts/install.ps1
@@ -2,7 +2,8 @@
 # Usage: irm /install.ps1 | iex
 #    or: .\install.ps1 [-Version X.Y.Z] [-FromSource] [-SourceDir DIR] [-Extras "browser"]
 #
-# Installs Octop into ~/.octop with a uv-managed Python environment.
+# New installs use ~/.freeos. An existing ~/.octop is kept when ~/.freeos
+# does not exist yet (same order as PathLayout.from_env).
 
 & {
 param(
@@ -17,7 +18,20 @@ param(
 
 $ErrorActionPreference = "Stop"
 
-$OctopHome     = if ($env:OCTOP_HOME) { $env:OCTOP_HOME } else { Join-Path $HOME ".octop" }
+function Resolve-FreeOSHome {
+    if ($env:FREEOS_HOME) { return $env:FREEOS_HOME }
+    if ($env:OCTOP_HOME) { return $env:OCTOP_HOME }
+    $freeos = Join-Path $HOME ".freeos"
+    $legacy = Join-Path $HOME ".octop"
+    if (Test-Path $freeos) { return $freeos }
+    foreach ($flag in @($env:OCTOP_DESKTOP, $env:FREEOS_DESKTOP)) {
+        if ($flag -match '^(1|true|yes|on)$') { return $freeos }
+    }
+    if (Test-Path $legacy) { return $legacy }
+    return $freeos
+}
+
+$OctopHome     = Resolve-FreeOSHome
 $OctopVenv     = Join-Path $OctopHome "venv"
 $OctopBin      = Join-Path $OctopHome "bin"
 $PythonVersion = "3.12"
@@ -50,7 +64,10 @@ to install it, or install later from the dashboard. If a system Chrome already
 exists, that download is skipped even with -Extras browser.
 
 Environment:
-  OCTOP_HOME            Installation directory (default: ~/.octop)
+  FREEOS_HOME           Installation directory (preferred)
+  OCTOP_HOME            Legacy installation directory override
+                        New installs use ~/.freeos. An existing ~/.octop is
+                        kept when ~/.freeos does not exist yet.
   OCTOP_REPO            Git clone URL for -FromSource without -SourceDir
 "@
     exit 0
@@ -318,7 +335,19 @@ New-Item -ItemType Directory -Path $OctopBin -Force | Out-Null
 $wrapperPs1 = Join-Path $OctopBin "octop.ps1"
 @'
 $ErrorActionPreference = "Stop"
-$OctopHome = if ($env:OCTOP_HOME) { $env:OCTOP_HOME } else { Join-Path $HOME ".octop" }
+function Resolve-FreeOSHome {
+    if ($env:FREEOS_HOME) { return $env:FREEOS_HOME }
+    if ($env:OCTOP_HOME) { return $env:OCTOP_HOME }
+    $freeos = Join-Path $HOME ".freeos"
+    $legacy = Join-Path $HOME ".octop"
+    if (Test-Path $freeos) { return $freeos }
+    foreach ($flag in @($env:OCTOP_DESKTOP, $env:FREEOS_DESKTOP)) {
+        if ($flag -match '^(1|true|yes|on)$') { return $freeos }
+    }
+    if (Test-Path $legacy) { return $legacy }
+    return $freeos
+}
+$OctopHome = Resolve-FreeOSHome
 $RealBin = Join-Path $OctopHome "venv\Scripts\octop.exe"
 if (-not (Test-Path $RealBin)) {
     Write-Error "Octop environment not found at $OctopHome\venv"
@@ -330,8 +359,17 @@ if (-not (Test-Path $RealBin)) {
 $cmdWrapper = Join-Path $OctopBin "octop.cmd"
 @'
 @echo off
-set "OCTOP_HOME=%OCTOP_HOME%"
-if "%OCTOP_HOME%"=="" set "OCTOP_HOME=%USERPROFILE%\.octop"
+if defined FREEOS_HOME (
+  set "OCTOP_HOME=%FREEOS_HOME%"
+) else if defined OCTOP_HOME (
+  rem keep the legacy override
+) else if exist "%USERPROFILE%\.freeos" (
+  set "OCTOP_HOME=%USERPROFILE%\.freeos"
+) else if exist "%USERPROFILE%\.octop" (
+  set "OCTOP_HOME=%USERPROFILE%\.octop"
+) else (
+  set "OCTOP_HOME=%USERPROFILE%\.freeos"
+)
 set "REAL_BIN=%OCTOP_HOME%\venv\Scripts\octop.exe"
 if not exist "%REAL_BIN%" (
     echo Error: Octop environment not found at %OCTOP_HOME%\venv >&2
diff --git a/scripts/install.sh b/scripts/install.sh
index c90570ca..246a398b 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -4,14 +4,42 @@
 #   或: bash scripts/install.sh --from-pypi   # 从 PyPI 安装 Octop(不含 FreeOS 下游改动)
 #   或: curl -fsSL /install.sh | bash   # 远程安装
 #
-# 将 Octop 安装到 ~/.octop,使用 uv 管理 Python 环境。
+# 新安装目录是 ~/.freeos(已有 ~/.octop 且还没有 ~/.freeos 时继续用旧目录)。
 # 用户无需预先安装 Python — uv 会处理一切。
 # 安装后会尽量把 octop 链接到已在 PATH 中的目录(如 /usr/local/bin),
 # 当前终端无需 source / 重开即可直接使用。
 set -euo pipefail
 
 # ── 默认配置 ──────────────────────────────────────────────────────────────────
-OCTOP_HOME="${OCTOP_HOME:-$HOME/.octop}"
+# Same order as src/octop/infra/utils/paths.py PathLayout.from_env:
+# FREEOS_HOME, OCTOP_HOME, existing ~/.freeos, existing ~/.octop
+# (skipped when the desktop flag is set), else ~/.freeos.
+resolve_install_home() {
+    if [ -n "${FREEOS_HOME:-}" ]; then
+        printf '%s\n' "$FREEOS_HOME"
+        return
+    fi
+    if [ -n "${OCTOP_HOME:-}" ]; then
+        printf '%s\n' "$OCTOP_HOME"
+        return
+    fi
+    if [ -e "$HOME/.freeos" ]; then
+        printf '%s\n' "$HOME/.freeos"
+        return
+    fi
+    _skip_legacy=0
+    for _flag in "${OCTOP_DESKTOP:-}" "${FREEOS_DESKTOP:-}"; do
+        case "$(printf '%s' "$_flag" | tr '[:upper:]' '[:lower:]')" in
+            1|true|yes|on) _skip_legacy=1 ;;
+        esac
+    done
+    if [ "$_skip_legacy" -eq 0 ] && [ -e "$HOME/.octop" ]; then
+        printf '%s\n' "$HOME/.octop"
+        return
+    fi
+    printf '%s\n' "$HOME/.freeos"
+}
+OCTOP_HOME="$(resolve_install_home)"
 OCTOP_VENV="$OCTOP_HOME/venv"
 OCTOP_BIN="$OCTOP_HOME/bin"
 PYTHON_VERSION="3.12"
@@ -94,7 +122,10 @@ Note: Playwright Chromium is not downloaded by default. Pass
   even with --extras browser.
 
 Environment variables:
-  OCTOP_HOME              Install directory (default: ~/.octop)
+  FREEOS_HOME             Install directory (preferred)
+  OCTOP_HOME              Legacy install directory override
+                          New installs use ~/.freeos. An existing ~/.octop is
+                          kept when ~/.freeos does not exist yet.
   OCTOP_PYPI_MIRROR       PyPI mirror URL (same as --mirror)
   OCTOP_REPO              Git URL to clone (used by --from-source with no local dir)
   HARNESS_AGENT_REPO      harness-agent repo (derived from OCTOP_REPO by default)
@@ -104,7 +135,8 @@ Environment variables:
   PLAYWRIGHT_INSTALL_TIMEOUT  Per-mirror download timeout in seconds (default 600)
 
 Details:
-  Everything is installed into an isolated virtualenv (~/.octop/venv), so the
+  Everything is installed into an isolated virtualenv (~/.freeos/venv, or the
+  existing ~/.octop/venv when that legacy tree is still the home), so the
   system Python is untouched. Playwright Chromium is opt-in (--extras browser):
   1. Playwright Chromium browser (skipped if a system browser exists)
   2. System libraries (Linux: apt/dnf/yum/pacman/zypper; only needed for Chromium)
@@ -950,10 +982,35 @@ mkdir -p "$OCTOP_BIN"
 
 cat > "$OCTOP_BIN/octop" << 'WRAPPER'
 #!/usr/bin/env bash
-# Octop CLI 包装脚本 — 委托给 uv 管理的环境。
+# FreeOS CLI wrapper — delegates to the uv-managed environment.
 set -euo pipefail
 
-OCTOP_HOME="${OCTOP_HOME:-$HOME/.octop}"
+resolve_install_home() {
+    if [ -n "${FREEOS_HOME:-}" ]; then
+        printf '%s\n' "$FREEOS_HOME"
+        return
+    fi
+    if [ -n "${OCTOP_HOME:-}" ]; then
+        printf '%s\n' "$OCTOP_HOME"
+        return
+    fi
+    if [ -e "$HOME/.freeos" ]; then
+        printf '%s\n' "$HOME/.freeos"
+        return
+    fi
+    _skip_legacy=0
+    for _flag in "${OCTOP_DESKTOP:-}" "${FREEOS_DESKTOP:-}"; do
+        case "$(printf '%s' "$_flag" | tr '[:upper:]' '[:lower:]')" in
+            1|true|yes|on) _skip_legacy=1 ;;
+        esac
+    done
+    if [ "$_skip_legacy" -eq 0 ] && [ -e "$HOME/.octop" ]; then
+        printf '%s\n' "$HOME/.octop"
+        return
+    fi
+    printf '%s\n' "$HOME/.freeos"
+}
+OCTOP_HOME="$(resolve_install_home)"
 REAL_BIN="$OCTOP_HOME/venv/bin/octop"
 
 if [ ! -x "$REAL_BIN" ]; then

From 26a153a57b25b70a5c9c3c35f286b7b692a96bf3 Mon Sep 17 00:00:00 2001
From: Cursor Agent 
Date: Mon, 26 Oct 2026 15:34:28 +0000
Subject: [PATCH 10/14] chore(release): cut 0.1.0rc1 and publish SHA256SUMS

Point the package version at 0.1.0rc1 so tag v0.1.0rc1 matches
pyproject and stays a GitHub prerelease. Attach SHA256SUMS for desktop
installers, portable zips, wheels, and sdists. Align the employee
password hint and backup home help with the code.

Co-authored-by: haoboy007-sudo 
---
 .github/workflows/octop-desktop.yml           |   8 ++
 .github/workflows/release.yml                 |  10 ++
 CHANGELOG.md                                  |   6 +-
 README.md                                     |   2 +-
 README_CN.md                                  |   4 +-
 dashboard/package-lock.json                   |   4 +-
 dashboard/package.json                        |   2 +-
 docs/adr/001-single-process-model.md          |   2 +-
 docs/cli.md                                   |  18 ++-
 .../frontend/src/pages/EmployeesPage.tsx      |   2 +-
 pyproject.toml                                |   2 +-
 scripts/README.md                             |   4 +-
 scripts/org-export/template/package.json      |   2 +-
 scripts/publish_sha256sums.sh                 |  96 +++++++++++++++
 scripts/release_download_links.py             |   4 +
 scripts/write_sha256sums.py                   | 109 ++++++++++++++++++
 src/octop/__init__.py                         |   2 +-
 src/octop/api/app.py                          |   3 +-
 src/octop/api/routers/backup.py               |   5 +-
 src/octop/cli/commands/backup.py              |  37 +++---
 .../infra/connectors/gateway/protocol.py      |   3 +-
 tests/unit/cli/test_backup_help.py            |  30 +++++
 tests/unit/test_release_download_links.py     |   1 +
 tests/unit/test_write_sha256sums.py           | 102 ++++++++++++++++
 uv.lock                                       |   2 +-
 25 files changed, 420 insertions(+), 40 deletions(-)
 create mode 100644 scripts/publish_sha256sums.sh
 create mode 100644 scripts/write_sha256sums.py
 create mode 100644 tests/unit/cli/test_backup_help.py
 create mode 100644 tests/unit/test_write_sha256sums.py

diff --git a/.github/workflows/octop-desktop.yml b/.github/workflows/octop-desktop.yml
index 1c508e60..f2a7420f 100644
--- a/.github/workflows/octop-desktop.yml
+++ b/.github/workflows/octop-desktop.yml
@@ -414,3 +414,11 @@ jobs:
           generate_release_notes: false
           prerelease: ${{ steps.channel.outputs.prerelease }}
           make_latest: ${{ steps.channel.outputs.prerelease == 'true' && 'false' || 'true' }}
+
+      # Refresh SHA256SUMS after desktop installers and portable zips land.
+      # Release publishes the same file for wheel/sdist; this pass adds the
+      # packages just uploaded and any freeos-* assets already on the release.
+      - name: Publish SHA256SUMS
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: bash scripts/publish_sha256sums.sh "${{ steps.rel.outputs.tag }}" release-assets
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 03c305ff..4a7a57d4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -139,6 +139,16 @@ jobs:
           prerelease: ${{ steps.channel.outputs.prerelease }}
           make_latest: ${{ steps.channel.outputs.prerelease == 'true' && 'false' || 'true' }}
 
+      # Hash wheel/sdist plus any desktop installers and portable zips already
+      # attached by Desktop Package. That workflow runs the same script again
+      # after its own upload, so the last publisher leaves one SHA256SUMS that
+      # covers every class. v0.1.0rc1 is a prerelease via release_prerelease.py
+      # (PEP 440 rcN). v0.1.0-rc.1 is not.
+      - name: Publish SHA256SUMS
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: bash scripts/publish_sha256sums.sh "${GITHUB_REF_NAME}" dist
+
   # workflow_run / release:published are unreliable when Release itself was
   # started via GITHUB_TOKEN workflow_dispatch (Auto Tag). Explicitly cascade.
   #
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07eaa8eb..36d8dd5e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,9 +4,9 @@
 
 格式遵循 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),版本号遵循 [语义化版本规范](https://semver.org/spec/v2.0.0.html)。
 
-## [0.1.0] - TBD
+## [0.1.0rc1] - TBD
 
-FreeOS **0.1.0** is an independent semver baseline. It does not track or align with Octop 1.0.x. GitHub Release Python artifacts are `freeos-0.1.0*` (sdist and wheel). The import package stays `octop`. This release does not publish to PyPI, and `pip install octop` installs upstream Octop, not FreeOS. Older Release files may still be named `octop-0.0.10*`.
+FreeOS **0.1.0rc1** is a prerelease of an independent semver baseline. It does not track or align with Octop 1.0.x. The release tag is `v0.1.0rc1`. GitHub Release Python artifacts are `freeos-0.1.0rc1*` (sdist and wheel). The import package stays `octop`. This release does not publish to PyPI, and `pip install octop` installs upstream Octop, not FreeOS. Older Release files may still be named `octop-0.0.10*`.
 
 ### Upgrade from 0.0.10
 
@@ -27,7 +27,7 @@ FreeOS **0.1.0** is an independent semver baseline. It does not track or align w
 - Preserve `config.json` when it is malformed: startup and configuration writers now report the file and parser position, avoid leaking stored credentials, and use atomic replacement instead of silently rebuilding an empty configuration. Absorbed from upstream Octop #730 / #738.
 - Checkpoint the SQLite WAL before the desktop process restarts so an in-app update does not drop the latest writes. Absorbed from upstream Octop #918.
 
-### Not in 0.1.0
+### Not in 0.1.0rc1
 
 Harness migration, cloud bridge default-on, default LDAP/SSO, Teams, and a whole-repo catch-up to Octop are out of scope.
 
diff --git a/README.md b/README.md
index 09bbf633..3b58bd6f 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
 
 

Python 3.12+ - Version + Version License: MIT + Apache-2.0 Upstream: Octop Organization module: openXYOS diff --git a/README_CN.md b/README_CN.md index d0706fe3..be49352b 100644 --- a/README_CN.md +++ b/README_CN.md @@ -11,7 +11,7 @@

Python 3.12+ License: MIT - Version + Version 上游:Octop 组织模块:openXYOS Code Style: Ruff @@ -246,7 +246,7 @@ curl -fsSL https://raw.githubusercontent.com/XYAIStudio/FreeOS/develop/scripts/i 桌面客户端说明见 [desktop/README.md](desktop/README.md),飞牛打包指南见 [fnos/README.md](fnos/README.md)。独立商业组织站见 [docs/org-export.md](docs/org-export.md)(`freeos org export-standalone`,默认导出完整 openXYOS 源码树),不是默认安装器里的第二套 Node。 -**不要** `pip install octop`。PyPI 上的 `octop` 是上游 Octop,不是 FreeOS 0.1.0。本仓库发到 GitHub Releases 的 Python 制品是 `freeos--py3-none-any.whl` 和 `freeos-.tar.gz`(导入包仍叫 `octop`)。更早的 Release 里可能还有 `octop-0.0.10*` 文件名,那是旧制品名,不是 PyPI 安装命令。桌面安装包从 [GitHub Releases](https://github.com/XYAIStudio/FreeOS/releases) 下载;终端安装用上面的安装脚本(默认拉取 FreeOS GitHub 源码)。只有在你明确要上游 Octop 时才加 `--from-pypi`。可选组件仍通过安装脚本传递,例如 `--extras browser`。 +**不要** `pip install octop`。PyPI 上的 `octop` 是上游 Octop,不是 FreeOS 0.1.0rc1。本仓库发到 GitHub Releases 的 Python 制品是 `freeos--py3-none-any.whl` 和 `freeos-.tar.gz`(导入包仍叫 `octop`)。更早的 Release 里可能还有 `octop-0.0.10*` 文件名,那是旧制品名,不是 PyPI 安装命令。桌面安装包从 [GitHub Releases](https://github.com/XYAIStudio/FreeOS/releases) 下载;终端安装用上面的安装脚本(默认拉取 FreeOS GitHub 源码)。只有在你明确要上游 Octop 时才加 `--from-pypi`。可选组件仍通过安装脚本传递,例如 `--extras browser`。 从源码用 uv 开发时: diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index b53fed4f..c97d3d08 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -1,12 +1,12 @@ { "name": "octop-dashboard", - "version": "0.1.0", + "version": "0.1.0rc1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "octop-dashboard", - "version": "0.1.0", + "version": "0.1.0rc1", "dependencies": { "@aiden0z/pptx-renderer": "^1.2.4", "@mermaid-js/parser": "^1.1.0", diff --git a/dashboard/package.json b/dashboard/package.json index 9c5cf999..c5ea36b2 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -1,7 +1,7 @@ { "name": "octop-dashboard", "private": true, - "version": "0.1.0", + "version": "0.1.0rc1", "type": "module", "scripts": { "dev": "vite --host", diff --git a/docs/adr/001-single-process-model.md b/docs/adr/001-single-process-model.md index 09fbfcbd..f2447a21 100644 --- a/docs/adr/001-single-process-model.md +++ b/docs/adr/001-single-process-model.md @@ -15,7 +15,7 @@ Everything runs in a single Python process served by uvicorn. There is no extern ## Rationale -- **Self-hosted target:** Operators install with `pip install octop && octop init`. Adding Redis or a process supervisor doubles the ops burden for the primary audience. Historical note (2026-10): that command describes the upstream-era install path. FreeOS 0.1.0 is installed from GitHub Releases or `scripts/install.sh`, not from the PyPI `octop` package. +- **Self-hosted target:** Operators install with `pip install octop && octop init`. Adding Redis or a process supervisor doubles the ops burden for the primary audience. Historical note (2026-10): that command describes the upstream-era install path. FreeOS 0.1.0rc1 is installed from GitHub Releases or `scripts/install.sh`, not from the PyPI `octop` package. - **Workload profile:** Agent calls are LLM-bound (seconds to minutes of I/O wait). Python's asyncio handles this fan-out efficiently without threads or processes. - **SQLite is sufficient:** Concurrent writes are rare (one writer per agent at a time); WAL mode handles the load. Migrating to Postgres requires only a new `SqlitePool` implementation. - **Restart semantics are simple:** `OctopServer.start()` rebuilds the entire runtime tree from the SQLite file. No external state to reconcile. diff --git a/docs/cli.md b/docs/cli.md index ae5249eb..18713f10 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -60,7 +60,7 @@ Octop commands pick one of three transports: | Layer | When | Login? | Examples | |-------|------|--------|----------| -| **Offline** (local DB only) | Need to read/write `~/.octop` without a running server | No | `init`, `backup`, `plugin`, `agent list`, `chats list/get/create/update/delete`, `cron list`, `user *`, `admin overview/audit`, `models presets/list/active` | +| **Offline** (local DB only) | Need to read/write `~/.freeos` without a running server. An existing `~/.octop` is used when `~/.freeos` is absent, unless `OCTOP_DESKTOP` or `FREEOS_DESKTOP` is set. `FREEOS_HOME` overrides `OCTOP_HOME`. | No | `init`, `backup`, `plugin`, `agent list`, `chats list/get/create/update/delete`, `cron list`, `user *`, `admin overview/audit`, `models presets/list/active` | | **Attach** (HTTP / WS) | Need a live `octop run` process (IM, streams, model pulls) | Yes (`octop user login`) | `chats send/repl`, `channel test/probe`, `models ollama-*`, `skills enable/disable`, `provider test` | | **Embedded** (in-process) | CLI boots `OctopServer` for a single command | No | `octop acp`, `octop chats repl`, `octop chats send` (defaults to embedded), `octop agent create/from-expert/start/stop/reload` | @@ -327,17 +327,23 @@ Commands: ## `octop backup` -Export and restore Octop backups. Works fully offline. +Export and restore FreeOS backups. Works fully offline. + +New installs use `~/.freeos`. When `~/.freeos` does not exist yet, an existing `~/.octop` is used, unless `OCTOP_DESKTOP` or `FREEOS_DESKTOP` is set. `FREEOS_HOME` overrides `OCTOP_HOME`. That is the same order as `PathLayout.from_env`. ``` Usage: octop backup [OPTIONS] COMMAND [ARGS]... - Backup and restore database + local agent workspaces. + Backup and restore the database and local agent workspaces. + + New installs use ~/.freeos. An existing ~/.octop is used when ~/.freeos is + absent, unless OCTOP_DESKTOP or FREEOS_DESKTOP is set. FREEOS_HOME overrides + OCTOP_HOME. Commands: - create Create a backup archive (DB + workspaces + config). - restore Restore a backup archive into ~/.octop. - auto Automatic backup status and one-shot run. + auto Automatic backup status and one-shot run (schedule requires... + create Create a selected-content backup archive. + restore Restore a backup into ~/.freeos, or ~/.octop when that legacy... ``` ### `octop backup auto` diff --git a/modules/openxyos/frontend/src/pages/EmployeesPage.tsx b/modules/openxyos/frontend/src/pages/EmployeesPage.tsx index 957bcc7c..ffa3ea49 100644 --- a/modules/openxyos/frontend/src/pages/EmployeesPage.tsx +++ b/modules/openxyos/frontend/src/pages/EmployeesPage.tsx @@ -618,7 +618,7 @@ function CreateEmployeeModal({ form, departments, skillsList, selectedSkills, on onChange({ ...form, email: e.target.value })} placeholder={t("员工登录邮箱", "Employee sign-in email")} className="w-full px-3 py-2 bg-bg border border-border rounded-lg text-sm focus:outline-none focus:border-primary" /> -

{t("自动创建系统账号,默认密码 emp123456", "A system account is created automatically. Default password: emp123456")}

+

{t("自动创建系统账号,初始密码由系统随机生成。", "A system account is created automatically. The initial password is generated at random.")}

)} diff --git a/pyproject.toml b/pyproject.toml index 37553b83..4dba70a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "freeos" -version = "0.1.0" +version = "0.1.0rc1" description = "FreeOS — self-hosted multi-user multi-agent assistant. GitHub Release artifacts are named freeos-*; the import package stays octop. Not published to PyPI." readme = "README.md" license = { text = "MIT" } diff --git a/scripts/README.md b/scripts/README.md index 61228121..7312c870 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -32,8 +32,8 @@ # 从 FreeOS GitHub 源码安装最新版(默认) bash scripts/install.sh -# 从 FreeOS GitHub v0.1.0 标签安装 -bash scripts/install.sh --version 0.1.0 +# 从 FreeOS GitHub v0.1.0rc1 标签安装 +bash scripts/install.sh --version 0.1.0rc1 # 显式安装 PyPI 上游 Octop(不包含 FreeOS 下游修改) bash scripts/install.sh --from-pypi diff --git a/scripts/org-export/template/package.json b/scripts/org-export/template/package.json index 01e0c0f1..6aadfa9a 100644 --- a/scripts/org-export/template/package.json +++ b/scripts/org-export/template/package.json @@ -1,7 +1,7 @@ { "name": "openxyos-web", "private": true, - "version": "0.1.0", + "version": "0.1.0rc1", "description": "Standalone Organization web generated from dashboard/src/org-ui. Interim API talks to a FreeOS host /api/org-module via proxy.", "type": "module", "scripts": { diff --git a/scripts/publish_sha256sums.sh b/scripts/publish_sha256sums.sh new file mode 100644 index 00000000..79fa6423 --- /dev/null +++ b/scripts/publish_sha256sums.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Upload SHA256SUMS for the desktop installers, portable zips, wheels, and +# sdists on a GitHub Release. Local directories are overlaid on assets already +# attached, so Release and Desktop Package can each refresh the same file. +# +# bash scripts/publish_sha256sums.sh v0.1.0rc1 dist +# bash scripts/publish_sha256sums.sh v0.1.0rc1 release-assets +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "usage: publish_sha256sums.sh [extra_dir ...]" >&2 + exit 2 +fi + +tag="$1" +shift +if [[ "$tag" != v* ]]; then + echo "tag must start with v: $tag" >&2 + exit 2 +fi + +repo="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +stage="$(mktemp -d)" +trap 'rm -rf "$stage"' EXIT + +EXTRAS=("$@") + +download_pattern_if_present() { + local pattern="$1" + local name saw=0 + for name in "${ASSET_NAMES[@]}"; do + # shellcheck disable=SC2254 + if [[ "$name" == $pattern ]]; then + saw=1 + break + fi + done + if [[ "$saw" -eq 1 ]]; then + gh release download "$tag" --repo "$repo" --pattern "$pattern" --dir "$stage" --skip-existing + fi +} + +stage_assets() { + local extra f base + rm -rf "$stage" + mkdir -p "$stage" + shopt -s nullglob + for extra in "${EXTRAS[@]}"; do + if [[ -d "$extra" ]]; then + for f in "$extra"/*; do + [[ -f "$f" ]] || continue + base="$(basename "$f")" + [[ "$base" == "SHA256SUMS" ]] && continue + if [[ ! -e "$stage/$base" ]]; then + cp "$f" "$stage/$base" + fi + done + fi + done + ASSET_NAMES=() + if gh release view "$tag" --repo "$repo" >/dev/null 2>&1; then + local listed + listed="$(gh release view "$tag" --repo "$repo" --json assets --jq '.assets[].name')" + if [[ -n "$listed" ]]; then + mapfile -t ASSET_NAMES <<<"$listed" + fi + download_pattern_if_present 'freeos-*' + download_pattern_if_present 'FreeOS-*' + fi + rm -f "$stage/SHA256SUMS" +} + +refresh_if_release_grew() { + local listed status + listed="$(gh release view "$tag" --repo "$repo" --json assets --jq '.assets[].name')" + set +e + printf '%s\n' "$listed" | python3 "$script_dir/write_sha256sums.py" --missing-from "$stage/SHA256SUMS" + status=$? + set -e + if [[ "$status" -eq 0 ]]; then + return 0 + fi + if [[ "$status" -ne 3 ]]; then + exit "$status" + fi + echo "Release gained artifacts after the first hash; rewriting SHA256SUMS" + stage_assets + python3 "$script_dir/write_sha256sums.py" "$stage" + gh release upload "$tag" "$stage/SHA256SUMS" --clobber --repo "$repo" +} + +stage_assets +python3 "$script_dir/write_sha256sums.py" "$stage" +gh release upload "$tag" "$stage/SHA256SUMS" --clobber --repo "$repo" +refresh_if_release_grew diff --git a/scripts/release_download_links.py b/scripts/release_download_links.py index 38c7954a..deb68c8d 100644 --- a/scripts/release_download_links.py +++ b/scripts/release_download_links.py @@ -98,6 +98,10 @@ def render_download_section(raw_version: str) -> str: f"- {md_link('Wheel', tag, f'freeos-{version}-py3-none-any.whl')}", f"- {md_link('sdist', tag, f'freeos-{version}.tar.gz')}", "", + "### Checksums", + "", + f"- {md_link('SHA256SUMS', tag, 'SHA256SUMS')}", + "", ] ) diff --git a/scripts/write_sha256sums.py b/scripts/write_sha256sums.py new file mode 100644 index 00000000..1d8717a6 --- /dev/null +++ b/scripts/write_sha256sums.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Write GNU SHA256SUMS for FreeOS GitHub Release artifacts. + +Covers desktop installers (``FreeOS-desktop-*``), portable zips +(``FreeOS-portable-*.zip``), wheels (``*.whl``), and sdists +(``freeos-*.tar.gz``). ``SHA256SUMS`` itself is not hashed. + + python3 scripts/write_sha256sums.py dist/ +""" + +from __future__ import annotations + +import argparse +import hashlib +import sys +from pathlib import Path + + +def is_checksum_target(name: str) -> bool: + """Return whether ``name`` is a desktop installer, zip, wheel, or sdist.""" + if not name or name == "SHA256SUMS" or name.startswith("."): + return False + if name.startswith("FreeOS-desktop-"): + return True + if name.startswith("FreeOS-portable-") and name.endswith(".zip"): + return True + if name.endswith(".whl"): + return True + # Linux desktop packages are FreeOS-desktop-*.tar.gz, already matched. + return name.startswith("freeos-") and name.endswith(".tar.gz") + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def write_sha256sums(directory: Path) -> Path: + """Write ``directory/SHA256SUMS`` and return that path. + + Lines use the GNU coreutils form `` `` so + ``sha256sum -c SHA256SUMS`` can verify them. Names are sorted. + """ + if not directory.is_dir(): + raise SystemExit(f"not a directory: {directory}") + files = sorted( + path for path in directory.iterdir() if path.is_file() and is_checksum_target(path.name) + ) + if not files: + raise SystemExit(f"no desktop, zip, wheel, or sdist artifacts in {directory}") + lines = [f"{_sha256(path)} {path.name}" for path in files] + destination = directory / "SHA256SUMS" + destination.write_text("\n".join(lines) + "\n", encoding="utf-8") + return destination + + +def covered_names(checksum_file: Path) -> set[str]: + names: set[str] = set() + text = checksum_file.read_text(encoding="utf-8") + for line in text.splitlines(): + if not line.strip(): + continue + _digest, _sep, name = line.partition(" ") + if name: + names.add(name) + return names + + +def missing_targets(checksum_file: Path, remote_names: list[str]) -> list[str]: + covered = covered_names(checksum_file) + return [name for name in remote_names if is_checksum_target(name) and name not in covered] + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Write SHA256SUMS for FreeOS release artifacts.") + parser.add_argument("directory", nargs="?", type=Path) + parser.add_argument( + "--is-target", + metavar="NAME", + help="Exit 0 when NAME should be covered, else 1.", + ) + parser.add_argument( + "--missing-from", + type=Path, + metavar="SHA256SUMS", + help="Read release asset names on stdin. Print targets absent from the file " + "and exit 3 when any are missing.", + ) + args = parser.parse_args(argv) + if args.is_target is not None: + return 0 if is_checksum_target(args.is_target) else 1 + if args.missing_from is not None: + remote = [line.strip() for line in sys.stdin if line.strip()] + missing = missing_targets(args.missing_from, remote) + if missing: + sys.stdout.write("\n".join(missing) + "\n") + return 3 + return 0 + if args.directory is None: + parser.error("directory is required") + write_sha256sums(args.directory) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/octop/__init__.py b/src/octop/__init__.py index d7857b72..ca29e732 100644 --- a/src/octop/__init__.py +++ b/src/octop/__init__.py @@ -2,4 +2,4 @@ from __future__ import annotations -__version__ = "0.1.0" +__version__ = "0.1.0rc1" diff --git a/src/octop/api/app.py b/src/octop/api/app.py index e92136dc..f7c9fd15 100644 --- a/src/octop/api/app.py +++ b/src/octop/api/app.py @@ -13,6 +13,7 @@ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, PlainTextResponse from scalar_fastapi import get_scalar_api_reference +from octop import __version__ from octop.api.middleware.jwt_auth import install as install_jwt_auth from octop.api.middleware.setup_lockdown import install as install_setup_lockdown from octop.api.openapi_meta import API_DESCRIPTION, OPENAPI_TAGS, configure_openapi @@ -108,7 +109,7 @@ def build_app(server: OctopServer) -> FastAPI: app = FastAPI( title="FreeOS API", - version="0.1.0", + version=__version__, description=API_DESCRIPTION, openapi_url="/api/openapi.json" if enable_api_docs else None, openapi_tags=OPENAPI_TAGS, diff --git a/src/octop/api/routers/backup.py b/src/octop/api/routers/backup.py index b9ce428d..0d86dabe 100644 --- a/src/octop/api/routers/backup.py +++ b/src/octop/api/routers/backup.py @@ -216,8 +216,11 @@ async def list_backups( _: Any = Depends(require_permission("backup")), server: Any = Depends(get_server), ) -> dict[str, Any]: - """List ``.tar.gz`` files in ``~/.octop/backups/``. + """List ``.tar.gz`` files in the data directory's ``backups/``. + New installs use ``~/.freeos/backups/``. An existing ``~/.octop/backups/`` + is used when ``~/.freeos`` is absent, unless ``OCTOP_DESKTOP`` or + ``FREEOS_DESKTOP`` is set. ``FREEOS_HOME`` overrides ``OCTOP_HOME``. Disk/manifest peek runs in a worker thread so the event loop stays free. """ files = await asyncio.to_thread(list_backup_files, server.paths) diff --git a/src/octop/cli/commands/backup.py b/src/octop/cli/commands/backup.py index b90ddb40..64698ec7 100644 --- a/src/octop/cli/commands/backup.py +++ b/src/octop/cli/commands/backup.py @@ -16,6 +16,13 @@ from octop.infra.db.services import build_shared_services from octop.infra.utils.paths import PathLayout +# Same order as PathLayout.from_env. Do not describe ~/.octop as the default. +_HOME_HELP = ( + "Data directory. Precedence: FREEOS_HOME, OCTOP_HOME, an existing ~/.freeos, " + "an existing ~/.octop (skipped when OCTOP_DESKTOP or FREEOS_DESKTOP is set), " + "otherwise ~/.freeos." +) + def _paths(home: Path | None = None) -> PathLayout: return PathLayout(home) if home is not None else PathLayout.from_env() @@ -23,16 +30,19 @@ def _paths(home: Path | None = None) -> PathLayout: @click.group() def backup() -> None: - """Backup and restore database + local agent workspaces.""" + """Backup and restore the database and local agent workspaces. + + New installs use ~/.freeos. An existing ~/.octop is used when ~/.freeos + is absent, unless OCTOP_DESKTOP or FREEOS_DESKTOP is set. FREEOS_HOME + overrides OCTOP_HOME. + """ @backup.command("create") @click.option( "-o", "--output", type=click.Path(path_type=Path), default=None, help="Output .tar.gz path." ) -@click.option( - "--home", type=click.Path(path_type=Path), default=None, help="Octop home (default ~/.octop)." -) +@click.option("--home", type=click.Path(path_type=Path), default=None, help=_HOME_HELP) @click.option("--no-config", is_flag=True, help="Do not include config.json / env.") @click.option("--no-workspaces", is_flag=True, help="Do not include agent workspaces.") @click.option("--no-skill-packages", is_flag=True, help="Do not include global skill packages.") @@ -93,9 +103,7 @@ def auto() -> None: @auto.command("status") -@click.option( - "--home", type=click.Path(path_type=Path), default=None, help="Octop home (default ~/.octop)." -) +@click.option("--home", type=click.Path(path_type=Path), default=None, help=_HOME_HELP) def auto_status(home: Path | None) -> None: """Print automatic backup settings from config.json.""" paths = _paths(home) @@ -114,9 +122,7 @@ def auto_status(home: Path | None) -> None: @auto.command("run") -@click.option( - "--home", type=click.Path(path_type=Path), default=None, help="Octop home (default ~/.octop)." -) +@click.option("--home", type=click.Path(path_type=Path), default=None, help=_HOME_HELP) def auto_run(home: Path | None) -> None: """Create one automatic backup now and apply retention pruning.""" paths = _paths(home) @@ -148,9 +154,7 @@ def auto_run(home: Path | None) -> None: @backup.command("restore") @click.argument("archive", type=click.Path(exists=True, path_type=Path)) -@click.option( - "--home", type=click.Path(path_type=Path), default=None, help="Octop home (default ~/.octop)." -) +@click.option("--home", type=click.Path(path_type=Path), default=None, help=_HOME_HELP) @click.option("--no-config", is_flag=True, default=False, help="Do not restore config.json / env.") @click.option( "--owner-user-id", @@ -167,7 +171,12 @@ def restore( owner_user_id: int | None, yes: bool, ) -> None: - """Restore from a backup archive. Stop ``octop run`` first for a clean restore.""" + """Restore a backup into ~/.freeos, or ~/.octop when that legacy home is still in use. + + Stop ``octop run`` first for a clean restore. An existing ~/.octop is + selected only when ~/.freeos is absent and OCTOP_DESKTOP / FREEOS_DESKTOP + are unset. FREEOS_HOME overrides OCTOP_HOME. + """ if not yes: click.confirm( "This overwrites the database and local workspaces. Continue?", diff --git a/src/octop/infra/connectors/gateway/protocol.py b/src/octop/infra/connectors/gateway/protocol.py index 1f738f68..5b82599e 100644 --- a/src/octop/infra/connectors/gateway/protocol.py +++ b/src/octop/infra/connectors/gateway/protocol.py @@ -5,6 +5,7 @@ import logging from typing import Any +from octop import __version__ from octop.infra.connectors.gateway.registry import call_gateway_tool, mcp_tools_for_kind logger = logging.getLogger(__name__) @@ -28,7 +29,7 @@ def handle_mcp_request( { "protocolVersion": MCP_PROTOCOL_VERSION, "capabilities": {"tools": {}}, - "serverInfo": {"name": f"octop-{kind}", "version": "0.1.0"}, + "serverInfo": {"name": f"octop-{kind}", "version": __version__}, }, ) diff --git a/tests/unit/cli/test_backup_help.py b/tests/unit/cli/test_backup_help.py new file mode 100644 index 00000000..df8da660 --- /dev/null +++ b/tests/unit/cli/test_backup_help.py @@ -0,0 +1,30 @@ +"""``octop backup --help`` names the FreeOS home and the legacy Octop home.""" + +from __future__ import annotations + +from click.testing import CliRunner + +from octop.cli.main import cli + + +def test_backup_help_uses_freeos_home_and_legacy_octop() -> None: + runner = CliRunner() + result = runner.invoke(cli, ["backup", "--help"]) + assert result.exit_code == 0, result.output + assert "~/.freeos" in result.output + assert "~/.octop" in result.output + assert "default ~/.octop" not in result.output + assert "FREEOS_HOME" in result.output + assert "OCTOP_DESKTOP" in result.output + + create = runner.invoke(cli, ["backup", "create", "--help"]) + assert create.exit_code == 0, create.output + assert "~/.freeos" in create.output + assert "existing ~/.octop" in create.output + assert "default ~/.octop" not in create.output + + restore = runner.invoke(cli, ["backup", "restore", "--help"]) + assert restore.exit_code == 0, restore.output + assert "~/.freeos" in restore.output + assert "~/.octop" in restore.output + assert "default ~/.octop" not in restore.output diff --git a/tests/unit/test_release_download_links.py b/tests/unit/test_release_download_links.py index b52a9aa4..1fd3b3ae 100644 --- a/tests/unit/test_release_download_links.py +++ b/tests/unit/test_release_download_links.py @@ -42,6 +42,7 @@ def test_render_download_section_matches_github_asset_names() -> None: assert f"{base}/FreeOS-fnos-native-0.9.31.fpk" in body assert f"{base}/freeos-0.9.31-py3-none-any.whl" in body assert f"{base}/freeos-0.9.31.tar.gz" in body + assert f"{base}/SHA256SUMS" in body assert "octop-0.9.31" not in body assert "Do not** `pip install octop`" in body assert "## Downloads" in body diff --git a/tests/unit/test_write_sha256sums.py b/tests/unit/test_write_sha256sums.py new file mode 100644 index 00000000..e1c1182b --- /dev/null +++ b/tests/unit/test_write_sha256sums.py @@ -0,0 +1,102 @@ +"""SHA256SUMS generation for FreeOS release artifacts.""" + +from __future__ import annotations + +import hashlib +import importlib.util +import subprocess +from pathlib import Path + +_SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "write_sha256sums.py" + + +def _load(): + spec = importlib.util.spec_from_file_location("write_sha256sums", _SCRIPT) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_checksum_covers_desktop_zip_wheel_and_sdist(tmp_path: Path) -> None: + mod = _load() + names = { + "FreeOS-desktop-windows-amd64-0.1.0rc1.exe": b"exe", + "FreeOS-desktop-darwin-arm64-0.1.0rc1.dmg": b"dmg", + "FreeOS-desktop-linux-amd64-0.1.0rc1.tar.gz": b"linux-desktop", + "FreeOS-portable-linux-amd64-0.1.0rc1.zip": b"zip", + "freeos-0.1.0rc1-py3-none-any.whl": b"wheel", + "freeos-0.1.0rc1.tar.gz": b"sdist", + "FreeOS-fnos-docker-0.1.0rc1.fpk": b"not-a-target", + "notes.txt": b"nope", + } + for name, payload in names.items(): + (tmp_path / name).write_bytes(payload) + + destination = mod.write_sha256sums(tmp_path) + text = destination.read_text(encoding="utf-8") + lines = text.splitlines() + covered = [line.split(" ", 1)[1] for line in lines] + assert covered == [ + "FreeOS-desktop-darwin-arm64-0.1.0rc1.dmg", + "FreeOS-desktop-linux-amd64-0.1.0rc1.tar.gz", + "FreeOS-desktop-windows-amd64-0.1.0rc1.exe", + "FreeOS-portable-linux-amd64-0.1.0rc1.zip", + "freeos-0.1.0rc1-py3-none-any.whl", + "freeos-0.1.0rc1.tar.gz", + ] + assert "FreeOS-fnos" not in text + assert "notes.txt" not in text + assert text.endswith("\n") + wheel_line = next(line for line in lines if line.endswith("freeos-0.1.0rc1-py3-none-any.whl")) + assert wheel_line.startswith(hashlib.sha256(b"wheel").hexdigest() + " ") + + +def test_empty_directory_is_rejected(tmp_path: Path) -> None: + mod = _load() + try: + mod.write_sha256sums(tmp_path) + except SystemExit as exc: + assert "no desktop" in str(exc) + else: + raise AssertionError("expected SystemExit") + + +def test_missing_from_reports_assets_added_after_the_hash(tmp_path: Path) -> None: + mod = _load() + (tmp_path / "freeos-0.1.0rc1-py3-none-any.whl").write_bytes(b"wheel") + checksum = mod.write_sha256sums(tmp_path) + missing = mod.missing_targets( + checksum, + [ + "freeos-0.1.0rc1-py3-none-any.whl", + "SHA256SUMS", + "FreeOS-desktop-windows-amd64-0.1.0rc1.exe", + "FreeOS-fnos-native-0.1.0rc1.fpk", + ], + ) + assert missing == ["FreeOS-desktop-windows-amd64-0.1.0rc1.exe"] + + +def test_cli_missing_from_exits_3(tmp_path: Path) -> None: + (tmp_path / "freeos-0.1.0rc1.tar.gz").write_bytes(b"sdist") + spec_mod = _load() + checksum = spec_mod.write_sha256sums(tmp_path) + completed = subprocess.run( + ["python3", str(_SCRIPT), "--missing-from", str(checksum)], + input="FreeOS-portable-darwin-arm64-0.1.0rc1.zip\n", + text=True, + capture_output=True, + check=False, + ) + assert completed.returncode == 3 + assert completed.stdout.strip() == "FreeOS-portable-darwin-arm64-0.1.0rc1.zip" + + +def test_release_workflows_publish_sha256sums() -> None: + root = Path(__file__).resolve().parents[2] + release = (root / ".github/workflows/release.yml").read_text(encoding="utf-8") + desktop = (root / ".github/workflows/octop-desktop.yml").read_text(encoding="utf-8") + assert "bash scripts/publish_sha256sums.sh" in release + assert "bash scripts/publish_sha256sums.sh" in desktop + assert 'scripts/release_prerelease.py "${GITHUB_REF_NAME}"' in release diff --git a/uv.lock b/uv.lock index 0662c6ff..d66146e5 100644 --- a/uv.lock +++ b/uv.lock @@ -2503,7 +2503,7 @@ wheels = [ [[package]] name = "freeos" -version = "0.1.0" +version = "0.1.0rc1" source = { editable = "." } dependencies = [ { name = "acme" }, From 7d4f7e847f75b8261f030052bec712bde3697fbc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 15:48:50 +0000 Subject: [PATCH 11/14] fix(deps): patch npm audit findings without major bumps npm audit fix updates DOMPurify, fast-uri, and serialize-javascript in the dashboard lock, and those plus body-parser, ip-address, multer, and undici in the organization module. All stay on the current major. Co-authored-by: haoboy007-sudo --- dashboard/package-lock.json | 18 ++++++------- modules/openxyos/package-lock.json | 42 +++++++++++++++--------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index c97d3d08..25e4dd81 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -6558,9 +6558,9 @@ "peer": true }, "node_modules/dompurify": { - "version": "3.4.16", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", - "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -7118,9 +7118,9 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "dev": true, "funding": [ { @@ -12394,9 +12394,9 @@ } }, "node_modules/serialize-javascript": { - "version": "7.0.5", - "resolved": "https://mirrors.tencent.com/npm/serialize-javascript/-/serialize-javascript-7.0.5.tgz", - "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.4.tgz", + "integrity": "sha512-FqlEuoTq/zRmA4Lg6aOqyH3lCHbUtCB0bmHCeFniX2GBdDqMJM3FwdvEhlE5Pim1dlt2FfBdVPmUNNiGhhKqpw==", "dev": true, "license": "BSD-3-Clause", "engines": { diff --git a/modules/openxyos/package-lock.json b/modules/openxyos/package-lock.json index f2561f3b..8d6c8323 100644 --- a/modules/openxyos/package-lock.json +++ b/modules/openxyos/package-lock.json @@ -5779,9 +5779,9 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.8", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz", - "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", + "version": "1.20.9", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.9.tgz", + "integrity": "sha512-FISIQxV5u91Y9gvuFq3q/tA/qTqLJn3AhRcEvyozyhJTh5OBFFtv827AdHU7Rbyct4r+Ba1/99oFa1Y3yuFsdw==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -7305,9 +7305,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.16", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", - "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -8112,9 +8112,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "dev": true, "funding": [ { @@ -8989,9 +8989,9 @@ "license": "MIT" }, "node_modules/ip-address": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", - "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", + "version": "10.7.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz", + "integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==", "license": "MIT", "engines": { "node": ">= 12" @@ -10662,9 +10662,9 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz", - "integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.5.0.tgz", + "integrity": "sha512-BsqOzYGVq49JcW2IzoSAxeKYRevb+dGGPPWleOR8Lrlk4igwxu1/9pwSXlByhCb7MVZDrNJFC0IBP5/O5nLYRw==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -12018,9 +12018,9 @@ "license": "MIT" }, "node_modules/serialize-javascript": { - "version": "7.0.7", - "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-7.0.7.tgz", - "integrity": "sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.4.tgz", + "integrity": "sha512-FqlEuoTq/zRmA4Lg6aOqyH3lCHbUtCB0bmHCeFniX2GBdDqMJM3FwdvEhlE5Pim1dlt2FfBdVPmUNNiGhhKqpw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -13088,9 +13088,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", - "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "version": "7.30.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz", + "integrity": "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==", "dev": true, "license": "MIT", "engines": { From b8bcd2ad73f813a350ca452c7811c1bf420e1a83 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 15:52:19 +0000 Subject: [PATCH 12/14] fix(release): include FnOS packages in SHA256SUMS Hash FreeOS-fnos-*.fpk with the other release artifacts, and refresh SHA256SUMS after the FnOS workflow attaches those packages. Co-authored-by: haoboy007-sudo --- .github/workflows/fnos-build-fpk.yml | 8 ++++++++ scripts/publish_sha256sums.sh | 9 ++++++--- scripts/write_sha256sums.py | 9 ++++++--- tests/unit/test_write_sha256sums.py | 18 +++++++++++++++--- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/.github/workflows/fnos-build-fpk.yml b/.github/workflows/fnos-build-fpk.yml index 21829870..8c8cf082 100644 --- a/.github/workflows/fnos-build-fpk.yml +++ b/.github/workflows/fnos-build-fpk.yml @@ -288,3 +288,11 @@ jobs: make_latest: ${{ steps.channel.outputs.prerelease == 'true' && 'false' || 'true' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Refresh SHA256SUMS after FreeOS-fnos-*.fpk lands. Release and Desktop + # Package publish the same file earlier; this pass adds the FnOS packages + # and keeps installers, zips, wheels, and sdists already on the release. + - name: Publish SHA256SUMS + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bash scripts/publish_sha256sums.sh "${{ needs.version.outputs.tag }}" dist diff --git a/scripts/publish_sha256sums.sh b/scripts/publish_sha256sums.sh index 79fa6423..a9b2613d 100644 --- a/scripts/publish_sha256sums.sh +++ b/scripts/publish_sha256sums.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash -# Upload SHA256SUMS for the desktop installers, portable zips, wheels, and -# sdists on a GitHub Release. Local directories are overlaid on assets already -# attached, so Release and Desktop Package can each refresh the same file. +# Upload SHA256SUMS for the desktop installers, portable zips, FnOS packages +# (FreeOS-fnos-*.fpk), wheels, and sdists on a GitHub Release. Local +# directories are overlaid on assets already attached, so Release, Desktop +# Package, and FnOS can each refresh the same file. # # bash scripts/publish_sha256sums.sh v0.1.0rc1 dist # bash scripts/publish_sha256sums.sh v0.1.0rc1 release-assets @@ -65,6 +66,8 @@ stage_assets() { if [[ -n "$listed" ]]; then mapfile -t ASSET_NAMES <<<"$listed" fi + # freeos-* is the wheel and sdist. FreeOS-* is desktop installers, + # portable zips, and FnOS packages named FreeOS-fnos-*.fpk. download_pattern_if_present 'freeos-*' download_pattern_if_present 'FreeOS-*' fi diff --git a/scripts/write_sha256sums.py b/scripts/write_sha256sums.py index 1d8717a6..8675f9b2 100644 --- a/scripts/write_sha256sums.py +++ b/scripts/write_sha256sums.py @@ -2,8 +2,9 @@ """Write GNU SHA256SUMS for FreeOS GitHub Release artifacts. Covers desktop installers (``FreeOS-desktop-*``), portable zips -(``FreeOS-portable-*.zip``), wheels (``*.whl``), and sdists -(``freeos-*.tar.gz``). ``SHA256SUMS`` itself is not hashed. +(``FreeOS-portable-*.zip``), FnOS packages (``FreeOS-fnos-*.fpk``), +wheels (``*.whl``), and sdists (``freeos-*.tar.gz``). ``SHA256SUMS`` +itself is not hashed. python3 scripts/write_sha256sums.py dist/ """ @@ -17,13 +18,15 @@ def is_checksum_target(name: str) -> bool: - """Return whether ``name`` is a desktop installer, zip, wheel, or sdist.""" + """Return whether ``name`` is a desktop installer, zip, FnOS package, wheel, or sdist.""" if not name or name == "SHA256SUMS" or name.startswith("."): return False if name.startswith("FreeOS-desktop-"): return True if name.startswith("FreeOS-portable-") and name.endswith(".zip"): return True + if name.startswith("FreeOS-fnos-") and name.endswith(".fpk"): + return True if name.endswith(".whl"): return True # Linux desktop packages are FreeOS-desktop-*.tar.gz, already matched. diff --git a/tests/unit/test_write_sha256sums.py b/tests/unit/test_write_sha256sums.py index e1c1182b..eef08c07 100644 --- a/tests/unit/test_write_sha256sums.py +++ b/tests/unit/test_write_sha256sums.py @@ -27,7 +27,9 @@ def test_checksum_covers_desktop_zip_wheel_and_sdist(tmp_path: Path) -> None: "FreeOS-portable-linux-amd64-0.1.0rc1.zip": b"zip", "freeos-0.1.0rc1-py3-none-any.whl": b"wheel", "freeos-0.1.0rc1.tar.gz": b"sdist", - "FreeOS-fnos-docker-0.1.0rc1.fpk": b"not-a-target", + "FreeOS-fnos-docker-0.1.0rc1.fpk": b"fnos-docker", + "FreeOS-fnos-native-0.1.0rc1.fpk": b"fnos-native", + "Octop-fnos-docker-0.1.0rc1.fpk": b"legacy-name", "notes.txt": b"nope", } for name, payload in names.items(): @@ -41,15 +43,19 @@ def test_checksum_covers_desktop_zip_wheel_and_sdist(tmp_path: Path) -> None: "FreeOS-desktop-darwin-arm64-0.1.0rc1.dmg", "FreeOS-desktop-linux-amd64-0.1.0rc1.tar.gz", "FreeOS-desktop-windows-amd64-0.1.0rc1.exe", + "FreeOS-fnos-docker-0.1.0rc1.fpk", + "FreeOS-fnos-native-0.1.0rc1.fpk", "FreeOS-portable-linux-amd64-0.1.0rc1.zip", "freeos-0.1.0rc1-py3-none-any.whl", "freeos-0.1.0rc1.tar.gz", ] - assert "FreeOS-fnos" not in text + assert "Octop-fnos" not in text assert "notes.txt" not in text assert text.endswith("\n") wheel_line = next(line for line in lines if line.endswith("freeos-0.1.0rc1-py3-none-any.whl")) assert wheel_line.startswith(hashlib.sha256(b"wheel").hexdigest() + " ") + fpk_line = next(line for line in lines if line.endswith("FreeOS-fnos-docker-0.1.0rc1.fpk")) + assert fpk_line.startswith(hashlib.sha256(b"fnos-docker").hexdigest() + " ") def test_empty_directory_is_rejected(tmp_path: Path) -> None: @@ -73,9 +79,13 @@ def test_missing_from_reports_assets_added_after_the_hash(tmp_path: Path) -> Non "SHA256SUMS", "FreeOS-desktop-windows-amd64-0.1.0rc1.exe", "FreeOS-fnos-native-0.1.0rc1.fpk", + "Octop-fnos-docker-0.1.0rc1.fpk", ], ) - assert missing == ["FreeOS-desktop-windows-amd64-0.1.0rc1.exe"] + assert missing == [ + "FreeOS-desktop-windows-amd64-0.1.0rc1.exe", + "FreeOS-fnos-native-0.1.0rc1.fpk", + ] def test_cli_missing_from_exits_3(tmp_path: Path) -> None: @@ -97,6 +107,8 @@ def test_release_workflows_publish_sha256sums() -> None: root = Path(__file__).resolve().parents[2] release = (root / ".github/workflows/release.yml").read_text(encoding="utf-8") desktop = (root / ".github/workflows/octop-desktop.yml").read_text(encoding="utf-8") + fnos = (root / ".github/workflows/fnos-build-fpk.yml").read_text(encoding="utf-8") assert "bash scripts/publish_sha256sums.sh" in release assert "bash scripts/publish_sha256sums.sh" in desktop + assert "bash scripts/publish_sha256sums.sh" in fnos assert 'scripts/release_prerelease.py "${GITHUB_REF_NAME}"' in release From 43997488e4d73b8e0e31f08dccbe814bc812373a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 16:07:06 +0000 Subject: [PATCH 13/14] fix(desktop): map prerelease versions onto numeric Windows resources NSIS VIProductVersion and go-winres file_version reject 0.1.0rc1. Keep the display string, and write 0.1.0.0 for the numeric fields. Co-authored-by: haoboy007-sudo --- .gitignore | 1 + desktop/src/build/stamp_version.py | 59 +++++++++++++++++++++- desktop/src/build/windows/Taskfile.yml | 4 ++ desktop/src/build/windows/nsis/project.nsi | 8 ++- tests/unit/desktop/test_stamp_version.py | 53 +++++++++++++++++++ 5 files changed, 122 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 2f413793..b091066d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ build/ !desktop/src/build/ !desktop/src/build/** +desktop/src/build/windows/nsis/product_version.nsh dist/ # OS diff --git a/desktop/src/build/stamp_version.py b/desktop/src/build/stamp_version.py index 02ece8fe..91b3391e 100644 --- a/desktop/src/build/stamp_version.py +++ b/desktop/src/build/stamp_version.py @@ -15,6 +15,10 @@ r'(]*\bname="com\.xyaistudio\.freeos"[^>]*\bversion=")[^"]+(")', re.IGNORECASE, ) +# 0.1.0rc1 / 0.1.0a1 / 0.1.0b1 / 0.1.0.dev1 → release core 0.1.0. +_PEP440_CORE = re.compile(r"^(?P\d+(?:\.\d+)*)(?:(?:a|b|rc)\d+|\.dev\d+)?$") +# wails_tools.nsh fallback INFO_PRODUCTVERSION, plus the extra ".0" NSIS appends. +_NSIS_DEV_FALLBACK = "0.0.2.0" def should_stamp(version: str) -> bool: @@ -32,6 +36,44 @@ def four_part_version(version: str) -> str | None: return ".".join(parts[:4]) +def release_core(version: str) -> str | None: + """Numeric core of a PEP 440 version, dropping a/b/rc/.dev suffixes.""" + if not should_stamp(version): + return None + match = _PEP440_CORE.fullmatch(version) + if match is None: + return None + return match.group("core") + + +def nsis_vi_version(version: str) -> str: + """Four integers for NSIS VIProductVersion / VIFileVersion. + + ``0.1.0`` stays ``0.1.0.0``. ``0.1.0rc1`` uses the same numeric fields; + the installer display string keeps the prerelease text separately. + ``dev`` matches the local makensis fallback in ``wails_tools.nsh``. + """ + if not should_stamp(version): + return _NSIS_DEV_FALLBACK + core = release_core(version) + if core is None: + raise SystemExit(f"cannot map {version!r} to an NSIS version") + dotted = four_part_version(core) + if dotted is None: + raise SystemExit(f"cannot map {version!r} to an NSIS version") + return dotted + + +def write_nsis_numeric(version: str, dest: Path) -> None: + """Write the ``!define`` included by ``project.nsi``.""" + numeric = nsis_vi_version(version) + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text( + f'!define INFO_PRODUCTVERSION_NUMERIC "{numeric}"\n', + encoding="utf-8", + ) + + def stamp_plist(path: Path, version: str) -> None: if not should_stamp(version): return @@ -48,7 +90,10 @@ def stamp_info_json(src: Path, dest: Path, version: str) -> None: if not should_stamp(version): return data = json.loads(dest.read_text(encoding="utf-8")) - data["fixed"]["file_version"] = version + # go-winres parses file_version as integers. A prerelease string such as + # 0.1.0rc1 stays on the ProductVersion display field only. + file_version = version if four_part_version(version) is not None else nsis_vi_version(version) + data["fixed"]["file_version"] = file_version data["info"]["0000"]["ProductVersion"] = version dest.write_text(json.dumps(data, indent=4) + "\n", encoding="utf-8") @@ -60,6 +105,9 @@ def stamp_manifest(src: Path, dest: Path, version: str) -> None: if not should_stamp(version): return dotted = four_part_version(version) + if dotted is None: + core = release_core(version) + dotted = four_part_version(core) if core is not None else None if dotted is None: return text = dest.read_text(encoding="utf-8") @@ -87,11 +135,20 @@ def main(argv: list[str] | None = None) -> int: man_p.add_argument("dest", type=Path) man_p.add_argument("version") + nsis_p = sub.add_parser( + "nsis-numeric", + help="Write product_version.nsh with a four-integer VIProductVersion", + ) + nsis_p.add_argument("version") + nsis_p.add_argument("dest", type=Path) + args = parser.parse_args(argv) if args.cmd == "plist": stamp_plist(args.path, args.version) elif args.cmd == "json": stamp_info_json(args.src, args.dest, args.version) + elif args.cmd == "nsis-numeric": + write_nsis_numeric(args.version, args.dest) else: stamp_manifest(args.src, args.dest, args.version) return 0 diff --git a/desktop/src/build/windows/Taskfile.yml b/desktop/src/build/windows/Taskfile.yml index 9f7a33f6..6288510b 100644 --- a/desktop/src/build/windows/Taskfile.yml +++ b/desktop/src/build/windows/Taskfile.yml @@ -115,6 +115,10 @@ tasks: powershell -NoProfile -Command "Remove-Item -Force '{{.ROOT_DIR}}/{{.ARCHIVE}}' -ErrorAction SilentlyContinue; New-Item -ItemType Directory -Force '{{.ROOT_DIR}}/{{.BIN_DIR}}' | Out-Null" + - cmd: python ../../stamp_version.py nsis-numeric "{{.VERSION}}" product_version.nsh + platforms: [windows] + - cmd: python3 ../../stamp_version.py nsis-numeric "{{.VERSION}}" product_version.nsh + platforms: [linux, darwin] - | {{if eq OS "windows"}} makensis -INPUTCHARSET UTF8 {{.VERSION_NSIS_DEFINE}}-DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" -DINSTALLER_OUTFILE="{{.ROOT_DIR}}\{{.ARCHIVE}}" project.nsi diff --git a/desktop/src/build/windows/nsis/project.nsi b/desktop/src/build/windows/nsis/project.nsi index c9385c06..4125fc5c 100644 --- a/desktop/src/build/windows/nsis/project.nsi +++ b/desktop/src/build/windows/nsis/project.nsi @@ -11,12 +11,16 @@ # "è¿è¡Œ FreeOS". Keep this file UTF-8 with BOM. !include "wails_tools.nsh" +# Written by `stamp_version.py nsis-numeric` before makensis. +# VIProductVersion / VIFileVersion must be four integers (0.1.0rc1 -> 0.1.0.0). +# String keys below keep the full product version, including a prerelease suffix. +!include "product_version.nsh" SetCompressor /SOLID lzma # The version information for this two must consist of 4 parts -VIProductVersion "${INFO_PRODUCTVERSION}.0" -VIFileVersion "${INFO_PRODUCTVERSION}.0" +VIProductVersion "${INFO_PRODUCTVERSION_NUMERIC}" +VIFileVersion "${INFO_PRODUCTVERSION_NUMERIC}" VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" diff --git a/tests/unit/desktop/test_stamp_version.py b/tests/unit/desktop/test_stamp_version.py index 00562856..e59fd8fb 100644 --- a/tests/unit/desktop/test_stamp_version.py +++ b/tests/unit/desktop/test_stamp_version.py @@ -65,6 +65,59 @@ def test_four_part_version_pads_semver() -> None: assert stamp_version.four_part_version("1.2.3") == "1.2.3.0" assert stamp_version.four_part_version("1.2.3.4") == "1.2.3.4" assert stamp_version.four_part_version("dev") is None + assert stamp_version.four_part_version("0.1.0rc1") is None + + +def test_nsis_vi_version_strips_prerelease() -> None: + assert stamp_version.nsis_vi_version("0.1.0") == "0.1.0.0" + assert stamp_version.nsis_vi_version("0.1.0rc1") == "0.1.0.0" + assert stamp_version.nsis_vi_version("1.2.3a1") == "1.2.3.0" + assert stamp_version.nsis_vi_version("1.2.3.dev2") == "1.2.3.0" + assert stamp_version.nsis_vi_version("dev") == "0.0.2.0" + + +def test_stamp_info_json_keeps_prerelease_on_display_string(tmp_path: Path) -> None: + src = ( + Path(__file__).resolve().parents[3] / "desktop" / "src" / "build" / "windows" / "info.json" + ) + dest = tmp_path / "info.generated.json" + stamp_version.stamp_info_json(src, dest, "0.1.0rc1") + data = json.loads(dest.read_text(encoding="utf-8")) + assert data["fixed"]["file_version"] == "0.1.0.0" + assert data["info"]["0000"]["ProductVersion"] == "0.1.0rc1" + + +def test_stamp_manifest_uses_numeric_core_for_prerelease(tmp_path: Path) -> None: + src = ( + Path(__file__).resolve().parents[3] + / "desktop" + / "src" + / "build" + / "windows" + / "wails.exe.manifest" + ) + dest = tmp_path / "wails.exe.generated.manifest" + stamp_version.stamp_manifest(src, dest, "0.1.0rc1") + text = dest.read_text(encoding="utf-8") + assert 'name="com.xyaistudio.freeos" version="0.1.0.0"' in text + + +def test_write_nsis_numeric_and_installer_wiring(tmp_path: Path) -> None: + dest = tmp_path / "product_version.nsh" + stamp_version.write_nsis_numeric("0.1.0rc1", dest) + assert dest.read_text(encoding="utf-8") == '!define INFO_PRODUCTVERSION_NUMERIC "0.1.0.0"\n' + root = Path(__file__).resolve().parents[3] + nsi = (root / "desktop" / "src" / "build" / "windows" / "nsis" / "project.nsi").read_text( + encoding="utf-8" + ) + task = (root / "desktop" / "src" / "build" / "windows" / "Taskfile.yml").read_text( + encoding="utf-8" + ) + assert 'VIProductVersion "${INFO_PRODUCTVERSION_NUMERIC}"' in nsi + assert 'VIFileVersion "${INFO_PRODUCTVERSION_NUMERIC}"' in nsi + assert 'VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"' in nsi + assert "stamp_version.py nsis-numeric" in task + assert '!include "product_version.nsh"' in nsi def test_stamp_manifest_sets_octop_identity_only(tmp_path: Path) -> None: From 5ca8e23bec1098d9cf7a19d9bbcc90a1490e5b95 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 16:22:24 +0000 Subject: [PATCH 14/14] fix(release): sort SHA256SUMS by filename on every platform Windows pathlib orders paths case-insensitively, so freeos-* sorted ahead of FreeOS-*. Compare the filename string instead. Co-authored-by: haoboy007-sudo --- scripts/write_sha256sums.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/write_sha256sums.py b/scripts/write_sha256sums.py index 8675f9b2..8c5e9a5b 100644 --- a/scripts/write_sha256sums.py +++ b/scripts/write_sha256sums.py @@ -49,8 +49,11 @@ def write_sha256sums(directory: Path) -> Path: """ if not directory.is_dir(): raise SystemExit(f"not a directory: {directory}") + # Sort by the filename string. pathlib.Path ordering on Windows is + # case-insensitive, which would place freeos-* ahead of FreeOS-*. files = sorted( - path for path in directory.iterdir() if path.is_file() and is_checksum_target(path.name) + (path for path in directory.iterdir() if path.is_file() and is_checksum_target(path.name)), + key=lambda path: path.name, ) if not files: raise SystemExit(f"no desktop, zip, wheel, or sdist artifacts in {directory}")