diff --git a/.github/workflows/fnos-build-fpk.yml b/.github/workflows/fnos-build-fpk.yml index 6786038e..21829870 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: @@ -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" @@ -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) @@ -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 65d5c623..1c508e60 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 @@ -325,6 +360,8 @@ jobs: contents: write actions: read steps: + - uses: actions/checkout@v4 + - name: Resolve release tag id: rel run: | @@ -339,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. @@ -369,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 cf626dad..03c305ff 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: @@ -54,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 @@ -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,10 @@ jobs: echo "" echo "## Install" echo "" + 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 "pip install octop==$version" + echo "uv pip install ./freeos-$version-py3-none-any.whl" echo '```' } > .release-notes.md fi @@ -137,12 +126,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. @@ -165,7 +160,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 +170,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 f87fb586..07eaa8eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +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. 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 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). + +### Changed + +- 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 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 + +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/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..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 @@ -29,7 +30,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 ──────────────────────────────────────────────────────────────────── @@ -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/README.md b/README.md index 55df7923..09bbf633 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,9 +136,11 @@ 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)。 +**不要**执行 `pip install octop`。PyPI 上的 `octop` 是上游 Octop,不是 FreeOS。本仓库的 Python 制品在 GitHub Releases 上,文件名是 `freeos--py3-none-any.whl` 和 `freeos-.tar.gz`(导入包仍是 `octop`)。桌面安装包仍是 `FreeOS-desktop-*`。 + +新安装的数据目录是 `%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 产出。 @@ -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 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) @@ -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** 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 fd7b3dcc..d0706fe3 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 @@ -92,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 做什么 @@ -129,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)。 ## 🤔 功能特性 @@ -193,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 @@ -225,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 参数)。 @@ -244,18 +242,11 @@ 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。 -**备选 — PyPI**(若你已自行管理 Python 环境): - -```bash -pip install octop -# 可选:pip install "octop[browser]" -# 可选本地 ONNX Embedding 模型缓存(设置 → 模型 → 本地):pip install "octop[local-embedding]" -# 仅下载目录模型到 ~/.octop/embedding_models;不用于对话,也不接入 Memory。 -``` +**不要** `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 开发时: @@ -339,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` | -| PyPI | 全平台 | `pip install octop` 或 `pip install "octop[browser]"` | +| 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,若已注册系统服务则自动重启 @@ -360,7 +349,7 @@ octop update # 获取并安装最新版 Octop,若已注册系统服 ### ⚙️ 配置 -所有运行时数据存放在 `~/.octop/`。可通过 CLI 管理,也可直接编辑文件。 +新安装的运行时数据在 `~/.freeos/`。已有 `~/.octop/` 且还没有 `~/.freeos/` 时,CLI 继续用旧目录。可通过 CLI 管理,也可直接编辑文件。 ```bash # LLM 供应商与模型 @@ -401,7 +390,7 @@ octop user list | 命令 | 说明 | |------|------| -| `octop init` | 初始化 `~/.octop/`(数据库、管理员、JWT 密钥) | +| `octop init` | 初始化数据目录(新安装为 `~/.freeos/`;数据库、管理员、JWT 密钥) | | `octop run` | 前台启动 Octop | | `octop service start` | 安装并启动系统服务 | | `octop service stop` | 停止系统服务 | @@ -414,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)**。 @@ -442,7 +431,7 @@ octop user list ### 📁 数据目录 ``` -~/.octop/ ← 安装与数据根目录 +~/.freeos/ ← 新安装的数据根目录(已有 ~/.octop 且没有 ~/.freeos 时仍用旧目录) ├── config.json # 进程配置(含可选 database 段) ├── octop.db # 默认 SQLite 控制面(用户、Agent、通道、定时任务 …) ├── secrets/ # JWT 密钥、通道 Token @@ -517,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 通道。 ### 🤝 参与贡献 @@ -553,13 +542,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 ff1fc723..b53fed4f 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.12", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", @@ -63,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": "^3.2.6" + "vitest": "^4.1.12" } }, "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", @@ -268,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", @@ -1807,20 +1791,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", @@ -1910,7 +1928,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1934,22 +1951,10 @@ } ], "license": "MIT", - "peer": true, "engines": { "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 +2803,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 +3141,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 +3789,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", @@ -3938,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", @@ -4376,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" @@ -4387,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" } @@ -4409,12 +4382,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", @@ -4477,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", @@ -4631,15 +4597,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 +4728,29 @@ } }, "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.12", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.12.tgz", + "integrity": "sha512-6S7/iVknKglLAxMFxQGNppBI0iqJS2NgM6FBxavitj7KaDq/vywOWGf6dGu7F2L1Qy0G6S3uTp7aGshHrzK8JA==", "dev": true, "license": "MIT", "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.12", + "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.12", + "vitest": "4.1.12" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -4795,39 +4759,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.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": "3.2.6", - "@vitest/utils": "3.2.6", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/spy": "4.1.12", + "@vitest/utils": "4.1.12", + "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.12", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.12.tgz", + "integrity": "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.6", + "@vitest/spy": "4.1.12", "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 +4814,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.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": { - "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.12", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.12.tgz", + "integrity": "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.6", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.1.12", + "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.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": "3.2.6", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.1.12", + "@vitest/utils": "4.1.12", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { @@ -4892,28 +4857,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.12", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.12.tgz", + "integrity": "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==", "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.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": "3.2.6", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.1.12", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -4941,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", @@ -4950,7 +4911,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4967,15 +4927,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 +5158,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 +5189,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 +5307,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 +5320,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 +5349,13 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, + "license": "MIT", "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 +5370,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 +5428,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 +5458,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 +5521,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 +5572,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 +5590,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 +5652,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 +5733,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 +5764,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,11 +5792,10 @@ "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": { "node": ">=0.10" } @@ -6358,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" } @@ -6604,16 +6438,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", @@ -6730,12 +6554,13 @@ "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.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 +6581,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 +6613,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 +6631,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 +6754,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" }, @@ -7061,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", @@ -7300,9 +7118,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 +7131,8 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/fault": { "version": "1.0.4", @@ -7346,12 +7165,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 +7188,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 +7250,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 +7308,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 +7516,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": { @@ -8173,7 +7973,6 @@ "url": "https://www.locize.com" } ], - "peer": true, "dependencies": { "@babel/runtime": "^7.29.2" }, @@ -8730,6 +8529,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 +8724,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 +8784,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 +8895,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 +8974,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 +9013,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 +9138,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 +9159,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", @@ -9476,6 +9197,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -9491,15 +9213,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 +9267,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,31 +9616,55 @@ } }, "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/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": { @@ -10579,28 +10317,21 @@ } }, "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", + "peer": true, "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", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -10608,23 +10339,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 +10388,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 +10445,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,22 +10707,11 @@ "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", "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=20.16.0 || >=22.3.0" }, @@ -11067,9 +10785,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 +10803,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" @@ -11138,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", @@ -11153,6 +10873,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -11165,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", @@ -11181,11 +10903,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", @@ -11839,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" }, @@ -11852,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" @@ -11982,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" @@ -12011,9 +11725,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 +11747,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" @@ -12140,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", @@ -12442,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" }, @@ -12620,15 +12332,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 +12405,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 +12599,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 +12639,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 +12653,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 +12703,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 +12829,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 +12864,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 +12974,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 +12983,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 +13022,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 +13052,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 +13090,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", @@ -13899,7 +13233,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13932,14 +13265,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 +13482,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": [ { @@ -14298,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", @@ -14373,29 +13697,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 +13729,79 @@ } }, "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.12", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.12.tgz", + "integrity": "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==", "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.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", + "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.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": "*" + "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 +13812,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 +13840,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 +14029,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", @@ -14907,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", @@ -15107,34 +14329,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 +14352,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..9c5cf999 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,21 @@ "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", + "vite": "^6.4.5" }, "devDependencies": { "@eslint/js": "^9.25.0", @@ -67,7 +69,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.12", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", @@ -79,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": "^3.2.6" + "vitest": "^4.1.12" } } 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/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/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/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/desktop/README.md b/desktop/README.md index 9513b7c1..48ede4e9 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` +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/` @@ -61,8 +55,9 @@ Upgrade / reinstall (same version included) refreshes the extracted runtime: 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. + `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 `~/.freeos/portable`, then copies the install @@ -114,7 +109,7 @@ the sidecar-era `v0.0.1` Windows builds. **Keeps** -- `%USERPROFILE%\.freeos` (or `FREEOS_HOME` / `OCTOP_HOME` / legacy `~/.octop`): +- `%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: @@ -226,7 +221,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/bundle-org-sidecar.sh b/desktop/portable/bundle-org-sidecar.sh index d275c458..f8172c22 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() { @@ -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/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/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/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/docs/architecture-integration.md b/docs/architecture-integration.md index cf2dc5ba..cbc35039 100644 --- a/docs/architecture-integration.md +++ b/docs/architecture-integration.md @@ -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 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) @@ -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/assets/qrcode.png b/docs/assets/qrcode.png deleted file mode 100644 index 6703b9d7..00000000 Binary files a/docs/assets/qrcode.png and /dev/null differ 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.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 960404d8..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)。数据目录优先 `FREEOS_HOME` / `~/.freeos`(仍识别 `~/.octop`)。先在本机安顿好;组织能力像工作室里另一间可独立布置的房间。
+> **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**。
 
-> **可选附加组件**:安装脚本支持通过 `--extras` 追加能力,例如浏览器自动化 `--extras browser`、飞书通道 `--extras channels-feishu`;也可用 `--version` 指定版本、`--mirror ` 使用国内 PyPI 镜像。更多选项见 [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
@@ -466,14 +468,11 @@ Octop 支持两个方向的 ACP 集成:
 ```
 
 **Q:如何升级?**
-- `octop update`(若通过一键安装);或从 PyPI / 源码重新安装后重启服务。
+- `octop update`(若通过一键安装);或下载 GitHub Release 中的 Python 制品 / 从 FreeOS 源码重新安装后重启服务。
 
 **Q:如何加入客户企业微信服务群?**
-- 请扫描下方二维码加入:
-
-![客户企业微信服务群二维码](assets/qrcode.png)
-
-> 二维码有效期至 **2026-08-03**,过期后请联系管理员更新。
+- 客户企业微信群二维码已于 **2026-08-03** 失效,指南不再附带该图。
+- 请改用 [GitHub Discussions](https://github.com/XYAIStudio/FreeOS/discussions),或加入 [Discord](https://discord.gg/jPas5J8Ua)。
 
 ---
 
@@ -497,4 +496,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..b8483e5a 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-*` 文件名。
 
 ## 目录结构
 
@@ -88,15 +88,15 @@ 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、桌面包并列)。
+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)
 
 ```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/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/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 (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 caa4fcbf..f2561f3b 100644 --- a/modules/openxyos/package-lock.json +++ b/modules/openxyos/package-lock.json @@ -36,7 +36,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": { @@ -70,12 +70,12 @@ "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" }, "engines": { - "node": ">=20.19" + "node": ">=22.12" } }, "node_modules/@antfu/install-pkg": { @@ -1953,18 +1953,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", @@ -1976,17 +1964,6 @@ "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", - "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", @@ -2752,9 +2729,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 +2748,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 +2771,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 +2785,7 @@ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -2818,9 +2795,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 +2812,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 +2829,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 +2846,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 +2863,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 +2880,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 +2897,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 +2914,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 +2931,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 +2948,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 +2965,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 +2984,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 +3007,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 +3030,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 +3053,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 +3076,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 +3099,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 +3122,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 +3145,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 +3166,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 +3176,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 +3186,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 +3206,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 +3226,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" ], @@ -4266,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, @@ -5202,9 +5200,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": { @@ -5506,13 +5504,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 +5612,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 +5830,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", @@ -5945,6 +5940,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 +6224,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" @@ -7324,9 +7305,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" @@ -8007,15 +7988,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 +8112,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 +8186,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": { @@ -8308,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", @@ -9017,9 +8989,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 +9554,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 +9846,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 +9856,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 +10451,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 +10662,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": { @@ -11386,9 +11348,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 +11358,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/proxy-from-env": { @@ -11588,9 +11554,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 +12111,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 +12128,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 +12197,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 +12319,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 +12358,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", @@ -13064,12 +13024,6 @@ "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", @@ -13347,16 +13301,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": { @@ -13369,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": { @@ -14203,9 +14157,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..b515bd71 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": { @@ -99,15 +100,24 @@ "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" }, "description": "Open-source operating system for governed human-agent organizations", "engines": { - "node": ">=20.19" + "node": ">=22.12" }, "overrides": { - "qs": "6.16.0" + "qs": "6.16.0", + "follow-redirects": "1.16.1", + "vite": "^6.4.5", + "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/pyproject.toml b/pyproject.toml index 63fae65b..37553b83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,9 @@ requires = ["hatchling>=1.18"] build-backend = "hatchling.build" [project] -name = "octop" -version = "0.0.10" -description = "FreeOS — self-hosted multi-user multi-agent assistant with an optional organization OS module (Octop-compatible package name)" +name = "freeos" +version = "0.1.0" +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/README.md b/scripts/README.md index 72997813..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` 后运行 | ### 本地脚本(仓库内) @@ -25,12 +29,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,17 +46,18 @@ 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`。 ### 环境变量 | 变量 | 说明 | |------|------| -| `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`) | @@ -65,7 +73,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 +89,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/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/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..cbdc1003 100644 --- a/scripts/install.bat +++ b/scripts/install.bat @@ -2,16 +2,26 @@ 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") +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" -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 +31,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 +45,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 +98,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%" @@ -141,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 48c4cb1c..506f4875 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -2,12 +2,14 @@ # 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( [string]$Version = "", [switch]$FromSource, + [switch]$FromPyPI, [string]$SourceDir = "", [string]$Extras = "", [string]$UvPath = "", @@ -16,16 +18,32 @@ 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" -$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 +53,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 @@ -45,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 @@ -226,7 +248,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 { @@ -309,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" @@ -321,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 15138c8c..246a398b 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,17 +1,45 @@ #!/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 环境。 +# 新安装目录是 ~/.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" @@ -34,7 +62,7 @@ else fi VERSION="" -FROM_SOURCE=false +FROM_SOURCE=true EXTRAS="" PYPI_MIRROR="" @@ -81,9 +109,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 @@ -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) @@ -648,7 +680,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 @@ -946,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 diff --git a/scripts/release_download_links.py b/scripts/release_download_links.py index 00378222..38c7954a 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( [ "", @@ -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/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/__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/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/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/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/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/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/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/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/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/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/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/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/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/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/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() 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 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/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/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/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/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 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 diff --git a/tests/unit/test_release_download_links.py b/tests/unit/test_release_download_links.py index 53ccc266..b52a9aa4 100644 --- a/tests/unit/test_release_download_links.py +++ b/tests/unit/test_release_download_links.py @@ -38,7 +38,10 @@ 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 "pip install" not 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 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/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( diff --git a/uv.lock b/uv.lock index 75ee7d32..0662c6ff 100644 --- a/uv.lock +++ b/uv.lock @@ -2502,8 +2502,8 @@ wheels = [ ] [[package]] -name = "octop" -version = "0.0.10" +name = "freeos" +version = "0.1.0" source = { editable = "." } dependencies = [ { name = "acme" },