diff --git a/.github/workflows/octop-desktop.yml b/.github/workflows/octop-desktop.yml index 65d5c623..25633a9d 100644 --- a/.github/workflows/octop-desktop.yml +++ b/.github/workflows/octop-desktop.yml @@ -77,6 +77,16 @@ jobs: - name: Build frontend → src/octop/dashboard run: make build-frontend + - name: Check dashboard dependencies + working-directory: dashboard + run: npm audit --registry=https://registry.npmjs.org + + - name: Test dashboard + working-directory: dashboard + env: + NODE_OPTIONS: "--no-experimental-webstorage" + run: npm run lint && npm test + - uses: actions/upload-artifact@v7 with: name: dashboard-dist @@ -84,6 +94,27 @@ jobs: if-no-files-found: error retention-days: 7 + organization-quality: + name: Verify organization module + if: github.event_name != 'workflow_dispatch' || github.event.inputs.attach_from_run == '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: "24" + cache: npm + cache-dependency-path: modules/openxyos/package-lock.json + - name: Install organization dependencies + working-directory: modules/openxyos + run: npm ci + - name: Check organization module + working-directory: modules/openxyos + run: npm run ci + - name: Check organization dependencies + working-directory: modules/openxyos + run: npm audit --registry=https://registry.npmjs.org + # Job-level `if` cannot read `matrix.*` (GitHub 422). Filter here so only # selected runners start — PRs → darwin-* + windows-*; dispatch honors `platforms`. select-platforms: @@ -134,7 +165,7 @@ jobs: package: name: "${{ matrix.plat }}" - needs: [frontend, select-platforms] + needs: [frontend, select-platforms, organization-quality] runs-on: ${{ matrix.os }} # Matrix runners are native for each plat; pin host detection so x64 Git Bash # on windows-11-arm does not mis-classify the job as windows-amd64 cross-build. @@ -177,7 +208,7 @@ jobs: - uses: actions/setup-node@v5 with: - node-version: "20" + node-version: "24" cache: npm cache-dependency-path: modules/openxyos/package-lock.json @@ -260,6 +291,10 @@ jobs: - name: Install Wails v3 CLI run: go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-beta.13 + - name: Test native desktop lifecycle and updater + working-directory: desktop/src + run: go test ./... + - name: Install NSIS if: runner.os == 'Windows' shell: pwsh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf626dad..b23978e7 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,9 @@ jobs: echo "" echo "## Install" echo "" + echo "Download the Python wheel from this GitHub Release and install it with:" echo '```bash' - echo "pip install octop==$version" + echo "uv pip install ./octop-$version-py3-none-any.whl" echo '```' } > .release-notes.md fi @@ -165,7 +153,7 @@ jobs: -f version="${{ github.ref_name }}" # FnOS FPK:复用本 Release 的 wheel + docker-publish 的 GHCR 镜像。 - # 与 sync-develop 并行。Dispatch 失败必须让本 workflow 变红(PyPI 已发布仍如此)。 + # 与 sync-develop 并行。Dispatch 失败必须让本 workflow 变红,以便发现下游构建漏触发。 trigger-fnos: name: Trigger FnOS FPK build needs: github-release @@ -175,7 +163,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # --ref 用版本 tag,保证 FPK 打在与 PyPI / 镜像相同的 commit 上 + # --ref 用版本 tag,保证 FPK 打在与 GitHub Release / 镜像相同的 commit 上 gh workflow run fnos-build-fpk.yml \ --repo "${{ github.repository }}" \ --ref "${{ github.ref_name }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index f87fb586..15367e0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,27 @@ ## [Unreleased] +### Changed + +- Publish Python distributions as GitHub Release assets only; the FreeOS release workflow no longer publishes the colliding `octop` package name to PyPI, and install scripts default to the FreeOS GitHub source. + +### Fixed + +- Remove checkpoint-resident system messages before model calls while preserving the active compiled prompt, preventing strict Qwen chat templates from failing when an older conversation contains a stale bootstrap message. +- Detect models from an already-running Ollama daemon even when FreeOS-managed Ollama startup is disabled; model discovery never starts Ollama unless the user enabled it. +- Preserve `config.json` when it is malformed: startup and all configuration writers now report the file and parser position, avoid leaking stored credentials, and use atomic replacement instead of silently rebuilding an empty configuration. + +## [0.1.0] - 2026-10-07 + +### Changed + +- Publish the successor to 0.0.10 as 0.1.0, with Python distributions attached to GitHub Releases only. + +### Fixed + +- Remove stale checkpoint system messages before model calls, detect models from an already-running Ollama daemon without starting it, and preserve malformed `config.json` files instead of silently replacing them. +- Improve desktop update safety by checkpointing SQLite WAL before restart. + ## [0.0.10] - 2026-10-03 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d1cd7ad..cba35a82 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,7 +73,7 @@ See [AGENTS.md](AGENTS.md) for module boundaries and coding conventions. 1. Cut `release/x.y.z` from latest `develop` (version bump + CHANGELOG on that branch) 2. Open PR: `release/x.y.z` → `main` and merge when green -3. Tag `v` on **main tip** and push — GitHub Actions builds, publishes to PyPI, and creates the GitHub Release +3. Tag `v` on **main tip** and push — GitHub Actions builds Python distributions and creates a GitHub Release with the artifacts; FreeOS does not publish the `octop` distribution to PyPI 4. Delete `release/x.y.z`; Actions syncs `main` → `develop` (or opens `chore/sync-develop-after-*` if merge conflicts / branch protection) Agent-assisted publish: `.cursor/skills/publish` (`/publish `). diff --git a/Makefile b/Makefile index cdf862e8..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/dashboard/package-lock.json b/dashboard/package-lock.json index ff1fc723..9ef4a83a 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -17,12 +17,11 @@ "ahooks": "^3.9.6", "antd": "^5.29.1", "antd-style": "^3.7.1", - "build": "^0.1.4", "docx-preview": "^0.3.2", "i18next": "^25.8.4", "jszip": "^3.10.1", "lucide-react": "^0.562.0", - "mermaid": "^11.13.0", + "mermaid": "^12.1.0", "qrcode.react": "^4.2.0", "react": "^18", "react-dom": "^18", @@ -30,14 +29,14 @@ "react-json-view-lite": "^2.5.0", "react-markdown": "^10.1.0", "react-pdf": "^10.5.0", - "react-router-dom": "^7.13.0", + "react-router-dom": "^7.18.4", "react-syntax-highlighter": "^16.1.1", "react-virtuoso": "^4.18.10", "recharts": "^3.10.1", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", - "xlsx": "^0.18.5" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" }, "devDependencies": { "@eslint/js": "^9.25.0", @@ -51,7 +50,7 @@ "@types/react-dom": "^18", "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^4.4.1", - "@vitest/coverage-v8": "^3.2.6", + "@vitest/coverage-v8": "^4.1.11", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", @@ -65,7 +64,7 @@ "typescript-eslint": "^8.30.1", "vite": "^6.3.5", "vite-plugin-pwa": "^0.21.2", - "vitest": "^3.2.6" + "vitest": "^4.1.11" } }, "node_modules/@adobe/css-tools": { @@ -93,20 +92,6 @@ } } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@ant-design/colors": { "version": "7.2.1", "resolved": "https://mirrors.tencent.com/npm/@ant-design/colors/-/colors-7.2.1.tgz", @@ -1807,20 +1792,54 @@ "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", "license": "MIT" }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-13.2.0.tgz", + "integrity": "sha512-bA9dvWlhAUcHkNwU+8w9yiUxJPlj/Lm3VqS7MD+NbIR/7LmpNLuK/AWNFdCo/T3SK5ND0/UHf3IJvl/Jn/i6yg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "13.2.0", + "@chevrotain/types": "13.2.0" + } + }, + "node_modules/@chevrotain/cst-dts-gen/node_modules/@chevrotain/types": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-13.2.0.tgz", + "integrity": "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/gast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-13.2.0.tgz", + "integrity": "sha512-bCARdAsG+eEbOveSXcOAC7pVxA04I1ionzeMOwIYf5NX7SaS7zgn4Dgug2BSqoL+iyKeMNSorQjsFSHJHsCNwg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "13.2.0" + } + }, + "node_modules/@chevrotain/gast/node_modules/@chevrotain/types": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-13.2.0.tgz", + "integrity": "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-13.2.0.tgz", + "integrity": "sha512-kxpUkLtiaK09ph+dib+Yvm+3sWSnuwlo25gqfKRgTPNmF6H7uW4gVkMjaRdtnN783S9sM6vUNWh9CsxYX4QwKw==", + "license": "Apache-2.0" + }, "node_modules/@chevrotain/types": { "version": "11.1.2", "resolved": "https://mirrors.tencent.com/npm/@chevrotain/types/-/types-11.1.2.tgz", "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", "license": "Apache-2.0" }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://mirrors.tencent.com/npm/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } + "node_modules/@chevrotain/utils": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-13.2.0.tgz", + "integrity": "sha512-SzKYY68rseOrb9KILVzPsl6SjkQtdWw01SRGB5aCVI5kFcHltQsOYV3JB0Ffst72DgkvHCyDQ0NLwxtmCCf97Q==", + "license": "Apache-2.0" }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", @@ -1939,17 +1958,6 @@ "node": ">=18" } }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.8", - "resolved": "https://mirrors.tencent.com/npm/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", - "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", - "license": "MIT", - "dependencies": { - "@so-ric/colorspace": "^1.1.6", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://mirrors.tencent.com/npm/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -2798,16 +2806,6 @@ "node": ">=18" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://mirrors.tencent.com/npm/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3146,17 +3144,6 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@rc-component/async-validator": { "version": "5.1.0", "resolved": "https://mirrors.tencent.com/npm/@rc-component/async-validator/-/async-validator-5.1.0.tgz", @@ -3805,16 +3792,6 @@ "win32" ] }, - "node_modules/@so-ric/colorspace": { - "version": "1.1.6", - "resolved": "https://mirrors.tencent.com/npm/@so-ric/colorspace/-/colorspace-1.1.6.tgz", - "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", - "license": "MIT", - "dependencies": { - "color": "^5.0.2", - "text-hex": "1.0.x" - } - }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://mirrors.tencent.com/npm/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -4409,12 +4386,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://mirrors.tencent.com/npm/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://mirrors.tencent.com/npm/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -4631,15 +4602,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://mirrors.tencent.com/npm/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -4761,32 +4733,30 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.6.tgz", - "integrity": "sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", - "ast-v8-to-istanbul": "^0.3.3", - "debug": "^4.4.1", + "@vitest/utils": "4.1.11", + "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.2.6", - "vitest": "3.2.6" + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -4795,39 +4765,40 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", - "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.6", - "@vitest/utils": "3.2.6", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", - "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.6", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -4849,42 +4820,42 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", - "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", - "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.6", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", - "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.6", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { @@ -4892,28 +4863,25 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", - "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", - "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.6", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -4967,15 +4935,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/adler-32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", - "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -5207,9 +5166,9 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", - "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.7.tgz", + "integrity": "sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==", "dev": true, "license": "MIT", "dependencies": { @@ -5238,7 +5197,8 @@ "node_modules/async": { "version": "3.2.6", "resolved": "https://mirrors.tencent.com/npm/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true }, "node_modules/async-function": { "version": "1.0.0", @@ -5355,9 +5315,9 @@ "dev": true }, "node_modules/baseline-browser-mapping": { - "version": "2.10.33", - "resolved": "https://mirrors.tencent.com/npm/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", - "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5368,19 +5328,20 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://mirrors.tencent.com/npm/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://mirrors.tencent.com/npm/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", "dev": true, "funding": [ { @@ -5396,13 +5357,14 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -5417,36 +5379,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "node_modules/build": { - "version": "0.1.4", - "resolved": "https://mirrors.tencent.com/npm/build/-/build-0.1.4.tgz", - "integrity": "sha512-KwbDJ/zrsU8KZRRMfoURG14cKIAStUlS8D5jBDvtrZbwO5FEkYqc3oB8HIhRiyD64A48w1lc+sOmQ+mmBw5U/Q==", - "dependencies": { - "cssmin": "0.3.x", - "jsmin": "1.x", - "jxLoader": "*", - "moo-server": "*", - "promised-io": "*", - "timespan": "2.x", - "uglify-js": "1.x", - "walker": "1.x", - "winston": "*", - "wrench": "1.3.x" - }, - "engines": { - "node": ">v0.4.12" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://mirrors.tencent.com/npm/call-bind/-/call-bind-1.0.9.tgz", @@ -5505,9 +5437,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://mirrors.tencent.com/npm/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", "dev": true, "funding": [ { @@ -5535,32 +5467,12 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/cfb": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", - "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", - "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "crc-32": "~1.2.0" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -5618,16 +5530,28 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", + "node_modules/chevrotain": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-13.2.0.tgz", + "integrity": "sha512-GTouk5YzgBxlTPCaDn5q0q2EJKkrYmnPF+HmF8v0Qo4r3iyPG7HJivkN1wthCXZcm3sIcZFpGTRovLPNdJNRCA==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "13.2.0", + "@chevrotain/gast": "13.2.0", + "@chevrotain/regexp-to-ast": "13.2.0", + "@chevrotain/types": "13.2.0", + "@chevrotain/utils": "13.2.0" + }, "engines": { - "node": ">= 16" + "node": ">=22.0.0" } }, + "node_modules/chevrotain/node_modules/@chevrotain/types": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-13.2.0.tgz", + "integrity": "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg==", + "license": "Apache-2.0" + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://mirrors.tencent.com/npm/classnames/-/classnames-2.5.1.tgz", @@ -5657,27 +5581,6 @@ "node": ">=6" } }, - "node_modules/codepage": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", - "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/color": { - "version": "5.0.3", - "resolved": "https://mirrors.tencent.com/npm/color/-/color-5.0.3.tgz", - "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", - "dependencies": { - "color-convert": "^3.1.3", - "color-string": "^2.1.3" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://mirrors.tencent.com/npm/color-convert/-/color-convert-2.0.1.tgz", @@ -5696,44 +5599,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/color-string": { - "version": "2.1.4", - "resolved": "https://mirrors.tencent.com/npm/color-string/-/color-string-2.1.4.tgz", - "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-string/node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://mirrors.tencent.com/npm/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "3.1.3", - "resolved": "https://mirrors.tencent.com/npm/color-convert/-/color-convert-3.1.3.tgz", - "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=14.6" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://mirrors.tencent.com/npm/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", - "engines": { - "node": ">=12.20" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5796,7 +5661,7 @@ }, "node_modules/cookie": { "version": "1.1.1", - "resolved": "https://mirrors.tencent.com/npm/cookie/-/cookie-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { @@ -5877,18 +5742,6 @@ "node": ">= 6" } }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://mirrors.tencent.com/npm/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5920,14 +5773,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cssmin": { - "version": "0.3.2", - "resolved": "https://mirrors.tencent.com/npm/cssmin/-/cssmin-0.3.2.tgz", - "integrity": "sha512-bynxGIAJ8ybrnFobjsQotIjA8HFDDgPwbeUWNXXXfR+B4f9kkxdcUyagJoQCSUOfMV+ZZ6bMn8bvbozlCzUGwQ==", - "bin": { - "cssmin": "bin/cssmin" - } - }, "node_modules/cssstyle": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", @@ -5956,9 +5801,9 @@ "license": "MIT" }, "node_modules/cytoscape": { - "version": "3.33.4", - "resolved": "https://mirrors.tencent.com/npm/cytoscape/-/cytoscape-3.33.4.tgz", - "integrity": "sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww==", + "version": "3.34.3", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.3.tgz", + "integrity": "sha512-yfYGhRcGAntq6YBD583j4n0Eg3jIxvWmZtz/5uz9UYkeIStSlMxuUja+ec5j3iBD8nv1rwaOAYMW09tBdkSeaQ==", "license": "MIT", "peer": true, "engines": { @@ -6604,16 +6449,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://mirrors.tencent.com/npm/deep-is/-/deep-is-0.1.4.tgz", @@ -6733,9 +6568,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.7", - "resolved": "https://mirrors.tencent.com/npm/dompurify/-/dompurify-3.4.7.tgz", - "integrity": "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==", + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", + "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -6756,13 +6591,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/echarts": { "version": "6.1.0", "resolved": "https://mirrors.tencent.com/npm/echarts/-/echarts-6.1.0.tgz", @@ -6795,10 +6623,17 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.364", - "resolved": "https://mirrors.tencent.com/npm/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", - "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", - "dev": true + "version": "1.5.445", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.445.tgz", + "integrity": "sha512-iX7o1nF+qNb3Bv5mLYgqfgF9Znh9e/8MKXdmbb8Gv2Q47OKIzTwEPD1oda1HrgEwYatTLJj0wQjHvIFt8Db53Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "license": "EPL-2.0" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -6806,12 +6641,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://mirrors.tencent.com/npm/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT" - }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://mirrors.tencent.com/npm/entities/-/entities-6.0.1.tgz", @@ -6935,9 +6764,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -7300,9 +7129,9 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://mirrors.tencent.com/npm/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { @@ -7313,7 +7142,8 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/fault": { "version": "1.0.4", @@ -7346,12 +7176,6 @@ } } }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://mirrors.tencent.com/npm/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://mirrors.tencent.com/npm/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -7375,10 +7199,11 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://mirrors.tencent.com/npm/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -7436,12 +7261,6 @@ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://mirrors.tencent.com/npm/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://mirrors.tencent.com/npm/for-each/-/for-each-0.3.5.tgz", @@ -7500,15 +7319,6 @@ "node": ">=0.4.x" } }, - "node_modules/frac": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", - "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://mirrors.tencent.com/npm/fs-extra/-/fs-extra-9.1.0.tgz", @@ -7717,15 +7527,16 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://mirrors.tencent.com/npm/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/glob/node_modules/minimatch": { @@ -8730,6 +8541,7 @@ "version": "2.0.1", "resolved": "https://mirrors.tencent.com/npm/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8924,21 +8736,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -8999,9 +8796,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://mirrors.tencent.com/npm/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -9110,18 +8907,6 @@ "node": ">=6" } }, - "node_modules/jsmin": { - "version": "1.0.1", - "resolved": "https://mirrors.tencent.com/npm/jsmin/-/jsmin-1.0.1.tgz", - "integrity": "sha512-OPuL5X/bFKgVdMvEIX3hnpx3jbVpFCrEM8pKPXjFkZUqg521r41ijdyTz7vACOhW6o1neVlcLyd+wkbK5fNHRg==", - "license": "Doug Crockford's license that allows this module to be used for Good but not for Evil", - "bin": { - "jsmin": "bin/jsmin" - }, - "engines": { - "node": ">=0.1.93" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://mirrors.tencent.com/npm/json-buffer/-/json-buffer-3.0.1.tgz", @@ -9201,51 +8986,29 @@ "setimmediate": "^1.0.5" } }, - "node_modules/jxLoader": { - "version": "0.1.1", - "resolved": "https://mirrors.tencent.com/npm/jxLoader/-/jxLoader-0.1.1.tgz", - "integrity": "sha512-ClEvAj3K68y8uKhub3RgTmcRPo5DfIWvtxqrKQdDPyZ1UVHIIKvVvjrAsJFSVL5wjv0rt5iH9SMCZ0XRKNzeUA==", - "dependencies": { - "js-yaml": "0.3.x", - "moo-server": "1.3.x", - "promised-io": "*", - "walker": "1.x" - }, - "engines": { - "node": ">v0.4.10" - } - }, - "node_modules/jxLoader/node_modules/js-yaml": { - "version": "0.3.7", - "resolved": "https://mirrors.tencent.com/npm/js-yaml/-/js-yaml-0.3.7.tgz", - "integrity": "sha512-/7PsVDNP2tVe2Z1cF9kTEkjamIwz4aooDpRKmN1+g/9eePCgcxsv4QDvEbxO0EH+gdDD7MLyDoR6BASo3hH51g==", - "engines": { - "node": "> 0.4.11" - } - }, "node_modules/katex": { - "version": "0.16.47", - "resolved": "https://mirrors.tencent.com/npm/katex/-/katex-0.16.47.tgz", - "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.19.0.tgz", + "integrity": "sha512-v6Tznz3zJ7u3niRCoDTsumM2+HA2XXcCu+WAacCeHD2z3p9A9Ks987o5FzfTGBN0e8A0vjEgIDvLbICrXpdw/Q==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], "license": "MIT", "dependencies": { - "commander": "^8.3.0" + "commander": "^15.0.0" }, "bin": { "katex": "cli.js" } }, "node_modules/katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://mirrors.tencent.com/npm/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=22.12.0" } }, "node_modules/keyv": { @@ -9262,12 +9025,6 @@ "resolved": "https://mirrors.tencent.com/npm/khroma/-/khroma-2.1.0.tgz", "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://mirrors.tencent.com/npm/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, "node_modules/layout-base": { "version": "1.0.2", "resolved": "https://mirrors.tencent.com/npm/layout-base/-/layout-base-1.0.2.tgz", @@ -9393,23 +9150,6 @@ "dev": true, "license": "MIT" }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://mirrors.tencent.com/npm/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "license": "MIT", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://mirrors.tencent.com/npm/longest-streak/-/longest-streak-3.1.0.tgz", @@ -9431,13 +9171,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lowlight": { "version": "1.20.0", "resolved": "https://mirrors.tencent.com/npm/lowlight/-/lowlight-1.20.0.tgz", @@ -9491,15 +9224,15 @@ } }, "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" } }, "node_modules/make-cancellable-promise": { @@ -9545,14 +9278,6 @@ "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" } }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://mirrors.tencent.com/npm/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dependencies": { - "tmpl": "1.0.5" - } - }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://mirrors.tencent.com/npm/markdown-table/-/markdown-table-3.0.4.tgz", @@ -9902,39 +9627,63 @@ } }, "node_modules/mermaid": { - "version": "11.15.0", - "resolved": "https://mirrors.tencent.com/npm/mermaid/-/mermaid-11.15.0.tgz", - "integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-12.1.0.tgz", + "integrity": "sha512-wlVCp+8eTupfCeeFvoZNNiTuHrvag0P2jz/ILgb/f/6jkVokUefOcujefi8qUe/j2asHiSePncVsz/xzzA80LQ==", + "license": "MIT", "dependencies": { - "@braintree/sanitize-url": "^7.1.1", + "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", - "@mermaid-js/parser": "^1.1.1", + "@mermaid-js/parser": "^2.0.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", - "cytoscape": "^3.33.1", + "chevrotain": "~13.2.0", + "cytoscape": "^3.34.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", - "dayjs": "^1.11.19", - "dompurify": "^3.3.1", + "dayjs": "^1.11.21", + "dompurify": "^3.4.12", + "elkjs": "^0.9.3", "es-toolkit": "^1.45.1", - "katex": "^0.16.25", + "katex": "^0.16.47", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + }, + "engines": { + "node": ">=22.12.0" } }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://mirrors.tencent.com/npm/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { + "node_modules/mermaid/node_modules/@chevrotain/types": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-13.2.0.tgz", + "integrity": "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg==", + "license": "Apache-2.0" + }, + "node_modules/mermaid/node_modules/@mermaid-js/parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-2.0.1.tgz", + "integrity": "sha512-j37dj/h6p6pots1L7pLPPNuABEEu9WP4OYg9pW1g+nYG331j0+3canE9OagtYdytYvdSAMG+2fKU5cxep6LgjA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~13.2.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://mirrors.tencent.com/npm/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, @@ -10579,24 +10328,15 @@ } }, "node_modules/monaco-editor": { - "version": "0.55.1", - "resolved": "https://mirrors.tencent.com/npm/monaco-editor/-/monaco-editor-0.55.1.tgz", - "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.57.0.tgz", + "integrity": "sha512-5BkI9KGoqrNvBGUe15/QlZq3OooZ8WLg1AxTpaqHRCP3HNpzPPZKE2EDz8M7c+VRmCeUw1Brp4cx/PWm3kI/5A==", "license": "MIT", "dependencies": { - "dompurify": "3.2.7", + "dompurify": "3.4.15", "marked": "14.0.0" } }, - "node_modules/monaco-editor/node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://mirrors.tencent.com/npm/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, "node_modules/monaco-editor/node_modules/marked": { "version": "14.0.0", "resolved": "https://mirrors.tencent.com/npm/marked/-/marked-14.0.0.tgz", @@ -10608,23 +10348,15 @@ "node": ">= 18" } }, - "node_modules/moo-server": { - "version": "1.3.0", - "resolved": "https://mirrors.tencent.com/npm/moo-server/-/moo-server-1.3.0.tgz", - "integrity": "sha512-9A8/eor2DXwpv1+a4pZAAydqLFVrWoKoO1fzdzqLUhYVXAO1Kgd1FR2gFZi7YdHzF0s4W8cDNwCfKJQrvLqxDw==", - "engines": { - "node": ">v0.4.10" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://mirrors.tencent.com/npm/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://mirrors.tencent.com/npm/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "dev": true, "funding": [ { @@ -10665,10 +10397,11 @@ } }, "node_modules/node-releases": { - "version": "2.0.46", - "resolved": "https://mirrors.tencent.com/npm/node-releases/-/node-releases-2.0.46.tgz", - "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -10721,13 +10454,18 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://mirrors.tencent.com/npm/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", - "dependencies": { - "fn.name": "1.x.x" + "engines": { + "node": ">=12.20.0" } }, "node_modules/open": { @@ -10978,16 +10716,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/pdfjs-dist": { "version": "5.4.296", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", @@ -11067,9 +10795,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://mirrors.tencent.com/npm/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", "dev": true, "funding": [ { @@ -11085,10 +10813,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.19", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -11181,11 +10910,6 @@ "resolved": "https://mirrors.tencent.com/npm/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "node_modules/promised-io": { - "version": "0.3.6", - "resolved": "https://mirrors.tencent.com/npm/promised-io/-/promised-io-0.3.6.tgz", - "integrity": "sha512-bNwZusuNIW4m0SPR8jooSyndD35ggirHlxVl/UhIaZD/F0OBv9ebfc6tNmbpZts3QXHggkjIBH8lvtnzhtcz0A==" - }, "node_modules/property-information": { "version": "7.2.0", "resolved": "https://mirrors.tencent.com/npm/property-information/-/property-information-7.2.0.tgz", @@ -12011,9 +11735,9 @@ } }, "node_modules/react-router": { - "version": "7.16.0", - "resolved": "https://mirrors.tencent.com/npm/react-router/-/react-router-7.16.0.tgz", - "integrity": "sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", + "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -12033,12 +11757,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.16.0", - "resolved": "https://mirrors.tencent.com/npm/react-router-dom/-/react-router-dom-7.16.0.tgz", - "integrity": "sha512-kMUAbimWB5FVbF4Bce4bJsiKJWLIUHq/mEG8+CFDnCSgltptBiG5nguducmsJeGKytlCvQud9Qhzpn49iduTlA==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz", + "integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==", "license": "MIT", "dependencies": { - "react-router": "7.16.0" + "react-router": "7.18.4" }, "engines": { "node": ">=20.0.0" @@ -12620,15 +12344,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://mirrors.tencent.com/npm/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://mirrors.tencent.com/npm/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -12702,7 +12417,7 @@ }, "node_modules/set-cookie-parser": { "version": "2.7.2", - "resolved": "https://mirrors.tencent.com/npm/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, @@ -12896,10 +12611,11 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://mirrors.tencent.com/npm/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -12935,27 +12651,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/ssf": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", - "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", - "license": "Apache-2.0", - "dependencies": { - "frac": "~1.1.2" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://mirrors.tencent.com/npm/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -12970,9 +12665,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "dev": true, "license": "MIT" }, @@ -13020,22 +12715,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://mirrors.tencent.com/npm/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -13162,20 +12841,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-comments": { "version": "2.0.1", "resolved": "https://mirrors.tencent.com/npm/strip-comments/-/strip-comments-2.0.1.tgz", @@ -13211,26 +12876,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://mirrors.tencent.com/npm/style-to-js/-/style-to-js-1.1.21.tgz", @@ -13341,264 +12986,6 @@ "dev": true, "license": "MIT" }, - "node_modules/test-exclude": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", - "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^10.2.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/test-exclude/node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/test-exclude/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/test-exclude/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/test-exclude/node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/test-exclude/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/test-exclude/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/test-exclude/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://mirrors.tencent.com/npm/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, "node_modules/throttle-debounce": { "version": "5.0.2", "resolved": "https://mirrors.tencent.com/npm/throttle-debounce/-/throttle-debounce-5.0.2.tgz", @@ -13608,14 +12995,6 @@ "node": ">=12.22" } }, - "node_modules/timespan": { - "version": "2.3.0", - "resolved": "https://mirrors.tencent.com/npm/timespan/-/timespan-2.3.0.tgz", - "integrity": "sha512-0Jq9+58T2wbOyLth0EU+AUb6JMGCLaTWIykJFa7hyAybjVH9gpVMTfUAwo5fWAvtFt2Tjh/Elg8JtgNpnMnM8g==", - "engines": { - "node": ">= 0.2.0" - } - }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://mirrors.tencent.com/npm/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -13655,30 +13034,10 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", "dev": true, "license": "MIT", "engines": { @@ -13705,11 +13064,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://mirrors.tencent.com/npm/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" - }, "node_modules/toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", @@ -13748,14 +13102,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://mirrors.tencent.com/npm/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "engines": { - "node": ">= 14.0.0" - } - }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://mirrors.tencent.com/npm/trough/-/trough-2.2.0.tgz", @@ -13932,14 +13278,6 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/uglify-js": { - "version": "1.3.5", - "resolved": "https://mirrors.tencent.com/npm/uglify-js/-/uglify-js-1.3.5.tgz", - "integrity": "sha512-YPX1DjKtom8l9XslmPFQnqWzTBkvI4N0pbkzLuPZZ4QTyig0uQqvZz9NgUdfEV+qccJzi7fVcGWdESvRIjWptQ==", - "bin": { - "uglifyjs": "bin/uglifyjs" - } - }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://mirrors.tencent.com/npm/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -14157,9 +13495,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://mirrors.tencent.com/npm/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "dev": true, "funding": [ { @@ -14373,29 +13711,6 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vite-plugin-pwa": { "version": "0.21.2", "resolved": "https://mirrors.tencent.com/npm/vite-plugin-pwa/-/vite-plugin-pwa-0.21.2.tgz", @@ -14428,66 +13743,80 @@ } }, "node_modules/vitest": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", - "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.6", - "@vitest/mocker": "3.2.6", - "@vitest/pretty-format": "^3.2.6", - "@vitest/runner": "3.2.6", - "@vitest/snapshot": "3.2.6", - "@vitest/spy": "3.2.6", - "@vitest/utils": "3.2.6", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.6", - "@vitest/ui": "3.2.6", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -14498,16 +13827,12 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/vitest/node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://mirrors.tencent.com/npm/void-elements/-/void-elements-3.1.0.tgz", @@ -14530,14 +13855,6 @@ "node": ">=18" } }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://mirrors.tencent.com/npm/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dependencies": { - "makeerror": "1.0.12" - } - }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -14727,85 +14044,6 @@ "node": ">=8" } }, - "node_modules/winston": { - "version": "3.19.0", - "resolved": "https://mirrors.tencent.com/npm/winston/-/winston-3.19.0.tgz", - "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.8", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://mirrors.tencent.com/npm/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://mirrors.tencent.com/npm/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/winston/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://mirrors.tencent.com/npm/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/wmf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", - "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/word": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", - "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://mirrors.tencent.com/npm/word-wrap/-/word-wrap-1.2.5.tgz", @@ -15107,34 +14345,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrench": { - "version": "1.3.9", - "resolved": "https://mirrors.tencent.com/npm/wrench/-/wrench-1.3.9.tgz", - "integrity": "sha512-srTJQmLTP5YtW+F5zDuqjMEZqLLr/eJOZfDI5ibfPfRMeDh3oBUefAscuH0q5wBKE339ptH/S/0D18ZkfOfmKQ==", - "deprecated": "wrench.js is deprecated! You should check out fs-extra (https://github.com/jprichardson/node-fs-extra) for any operations you were using wrench for. Thanks for all the usage over the years.", - "engines": { - "node": ">=0.1.97" - } - }, "node_modules/ws": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", @@ -15158,19 +14368,10 @@ } }, "node_modules/xlsx": { - "version": "0.18.5", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", - "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "version": "0.20.3", + "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "cfb": "~1.2.1", - "codepage": "~1.15.0", - "crc-32": "~1.2.1", - "ssf": "~0.11.2", - "wmf": "~1.0.1", - "word": "~0.3.0" - }, "bin": { "xlsx": "bin/xlsx.njs" }, diff --git a/dashboard/package.json b/dashboard/package.json index 4d62cdec..e0e6d198 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -29,12 +29,11 @@ "ahooks": "^3.9.6", "antd": "^5.29.1", "antd-style": "^3.7.1", - "build": "^0.1.4", "docx-preview": "^0.3.2", "i18next": "^25.8.4", "jszip": "^3.10.1", "lucide-react": "^0.562.0", - "mermaid": "^11.13.0", + "mermaid": "^12.1.0", "qrcode.react": "^4.2.0", "react": "^18", "react-dom": "^18", @@ -42,18 +41,20 @@ "react-json-view-lite": "^2.5.0", "react-markdown": "^10.1.0", "react-pdf": "^10.5.0", - "react-router-dom": "^7.13.0", + "react-router-dom": "^7.18.4", "react-syntax-highlighter": "^16.1.1", "react-virtuoso": "^4.18.10", "recharts": "^3.10.1", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", - "xlsx": "^0.18.5" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" }, "overrides": { "serialize-javascript": "^7.0.3", - "@rollup/plugin-terser": "^1.0.0" + "@rollup/plugin-terser": "^1.0.0", + "katex": "^0.19.0", + "dompurify": "^3.4.16" }, "devDependencies": { "@eslint/js": "^9.25.0", @@ -67,7 +68,7 @@ "@types/react-dom": "^18", "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^4.4.1", - "@vitest/coverage-v8": "^3.2.6", + "@vitest/coverage-v8": "^4.1.11", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", @@ -81,6 +82,6 @@ "typescript-eslint": "^8.30.1", "vite": "^6.3.5", "vite-plugin-pwa": "^0.21.2", - "vitest": "^3.2.6" + "vitest": "^4.1.11" } } diff --git a/dashboard/src/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/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/portable/bundle-org-sidecar.sh b/desktop/portable/bundle-org-sidecar.sh index d275c458..d688c518 100755 --- a/desktop/portable/bundle-org-sidecar.sh +++ b/desktop/portable/bundle-org-sidecar.sh @@ -17,7 +17,7 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # shellcheck source=desktop/portable/_common.sh source "${REPO_ROOT}/desktop/portable/_common.sh" -NODE_VERSION="${NODE_VERSION:-20.19.5}" +NODE_VERSION="${NODE_VERSION:-24.21.0}" NODE_BASE_URL="${NODE_BASE_URL:-https://nodejs.org/dist/v${NODE_VERSION}}" node_archive_name() { diff --git a/desktop/portable/verify_imports.py b/desktop/portable/verify_imports.py index 7f08cf8c..a5179a0c 100755 --- a/desktop/portable/verify_imports.py +++ b/desktop/portable/verify_imports.py @@ -27,6 +27,9 @@ "cryptography.fernet", "langchain_core", "langchain_openai", + "octop.infra.agents.middleware.system_message_cleanup", + "octop.infra.server", + "octop.api.app", ) @@ -117,11 +120,12 @@ def main() -> int: except Exception as exc: errors.append(f"import {mod}: {type(exc).__name__}: {exc}") - # Sanity: launch.py must stay importable as a file (no compile errors). + # Compile the launcher shipped alongside packages/, not this checker. + launcher = packages.parent / "launch.py" try: - ast.parse(Path(__file__).read_text(encoding="utf-8")) - except SyntaxError as exc: - errors.append(f"self-parse: {exc}") + ast.parse(launcher.read_text(encoding="utf-8"), filename=str(launcher)) + except (OSError, SyntaxError) as exc: + errors.append(f"launch.py: {exc}") if errors: print("verify_imports FAILED:", file=sys.stderr) diff --git a/docs/cli.md b/docs/cli.md index b2a78e48..ae5249eb 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,6 +1,6 @@ # CLI Reference -`octop` is a single Click app installed as `octop` on `pip install octop`. +`octop` and `freeos` are compatibility command names for the FreeOS CLI. The supported installer obtains FreeOS from the XYAIStudio GitHub repository; Python release artifacts are attached to GitHub Releases. It is registered in `src/octop/cli/main.py`; commands are loaded lazily from `src/octop/cli/registry.py`. @@ -363,8 +363,7 @@ retention only deletes those files and never touches manual ## `octop update` -Check for and install a newer Octop release from the configured -channel (PyPI by default). +Check for and install a newer FreeOS desktop release from GitHub Releases. ``` Usage: octop update [OPTIONS] @@ -373,7 +372,9 @@ Usage: octop update [OPTIONS] Options: --check / --no-check Only check; do not install. - --channel TEXT Release channel (pypi, testpypi, custom index). + --allow-prerelease Include alpha / beta / rc / dev releases. + --yes, -y Skip upgrade confirmation. + --verbose, -v Show installer output. -h, --help Show this message and exit. ``` diff --git a/docs/node-runtime.md b/docs/node-runtime.md index 78aaa7c2..c9f6cd51 100644 --- a/docs/node-runtime.md +++ b/docs/node-runtime.md @@ -65,15 +65,15 @@ expanding `SHIP_OPENXYOS_RUNTIME=1` to new flavors. | GHCR image | `ghcr.io/xyaistudio/freeos:{version\|latest}` | | Local Compose tag | `octop:latest` (compatibility alias; one Python process) | | Security advisories | [XYAIStudio/FreeOS](https://github.com/XYAIStudio/FreeOS/security/advisories/new) | -| PyPI package name | `octop` (intentional remaining identifier; CLI is `freeos`) | +| Python distribution identifier | `octop` (internal compatibility name; distributed through GitHub Releases only) | -### Release / PyPI / Docker gates +### Release / Docker gates - **Docker Publish** pushes GHCR always. Docker Hub login is **optional** (`DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN`). Missing Hub secrets must not fail the workflow. -- **PyPI** still publishes the `octop` distribution (`PYPI_API_TOKEN` on the - `pypi` environment). That name is the Python package, not a pointer at - Tencent Cloud advisories. +- **Python distributions** are attached to GitHub Releases. Do not publish the + `octop` distribution to PyPI: that name is already used by the upstream + Octop project and would create an installation identity collision. - **FnOS Docker `.fpk`** compose must pull `ghcr.io/xyaistudio/freeos`, not `ghcr.io/tencentcloud/octop`. diff --git a/docs/node-runtime.zh-CN.md b/docs/node-runtime.zh-CN.md index 73647c4a..b24736fe 100644 --- a/docs/node-runtime.zh-CN.md +++ b/docs/node-runtime.zh-CN.md @@ -60,13 +60,13 @@ GitHub 工作流 **FreeOS Desktop Package**(`.github/workflows/octop-desktop.y | GHCR 镜像 | `ghcr.io/xyaistudio/freeos:{version\|latest}` | | 本地 Compose 标签 | `octop:latest`(兼容别名;单 Python 进程) | | 安全公告 | [XYAIStudio/FreeOS](https://github.com/XYAIStudio/FreeOS/security/advisories/new) | -| PyPI 包名 | `octop`(有意保留的标识;CLI 是 `freeos`) | +| Python 发行包标识 | `octop`(内部兼容名;仅通过 GitHub Releases 分发) | -### 发版 / PyPI / Docker 门禁 +### 发版 / Docker 门禁 - **Docker Publish** 始终推 GHCR。Docker Hub 登录是 **可选** (`DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN`)。没有 Hub secrets 不得让工作流变红。 -- **PyPI** 仍发布名为 `octop` 的发行版(`pypi` 环境的 `PYPI_API_TOKEN`)。这是 - Python 包名,不是腾讯云安全公告地址。 +- **Python 发行包**随 GitHub Release 附件发布。不要将 `octop` 发行包发布到 + PyPI:该名称已被上游 Octop 项目使用,会造成安装身份冲突。 - **飞牛 Docker `.fpk`** 的 compose 必须拉 `ghcr.io/xyaistudio/freeos`,不是 `ghcr.io/tencentcloud/octop`。 diff --git a/docs/user-guide.md b/docs/user-guide.md index 960404d8..1f3bd784 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -100,7 +100,7 @@ source ~/.bashrc # Bash 安装脚本会把 `octop` 命令放入 `~/.octop/bin` 并加入 PATH,并在 `~/.octop/venv` 创建隔离环境;**不会改动系统 Python**。 -> **可选附加组件**:安装脚本支持通过 `--extras` 追加能力,例如浏览器自动化 `--extras browser`、飞书通道 `--extras channels-feishu`;也可用 `--version` 指定版本、`--mirror ` 使用国内 PyPI 镜像。更多选项见 [scripts/README.md](scripts/README.md)。 +> **可选附加组件**:安装脚本默认从 FreeOS GitHub 仓库安装,支持通过 `--extras` 追加能力,例如浏览器自动化 `--extras browser`、飞书通道 `--extras channels-feishu`;也可用 `--version` 指定 GitHub 版本标签、`--mirror ` 使用国内 PyPI 镜像下载 Python 依赖。更多选项见 [scripts/README.md](../scripts/README.md)。 ### 2.3 验证安装 @@ -466,7 +466,7 @@ Octop 支持两个方向的 ACP 集成: ``` **Q:如何升级?** -- `octop update`(若通过一键安装);或从 PyPI / 源码重新安装后重启服务。 +- `octop update`(若通过一键安装);或下载 GitHub Release 中的 Python 制品 / 从 FreeOS 源码重新安装后重启服务。 **Q:如何加入客户企业微信服务群?** - 请扫描下方二维码加入: diff --git a/modules/openxyos/backend/services/doc-parser.ts b/modules/openxyos/backend/services/doc-parser.ts index 8c322942..c2aed9f0 100644 --- a/modules/openxyos/backend/services/doc-parser.ts +++ b/modules/openxyos/backend/services/doc-parser.ts @@ -5,11 +5,15 @@ export async function extractText(filePath: string, fileType: string): Promise=20.19.0" }, @@ -1896,6 +1898,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1917,6 +1920,7 @@ "resolved": "https://registry.npmmirror.com/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1953,29 +1957,6 @@ "react": ">=16.8.0" } }, - "node_modules/@emnapi/core": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", - "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.3", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", @@ -2752,9 +2733,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", - "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -2771,13 +2752,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.3" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", - "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -2794,13 +2775,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.3" + "@img/sharp-libvips-darwin-x64": "1.3.4" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", - "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -2808,7 +2789,7 @@ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -2818,9 +2799,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", - "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -2835,9 +2816,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", - "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -2852,9 +2833,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", - "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], @@ -2869,9 +2850,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", - "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], @@ -2886,9 +2867,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", - "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], @@ -2903,9 +2884,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", - "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], @@ -2920,9 +2901,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", - "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], @@ -2937,9 +2918,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", - "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], @@ -2954,9 +2935,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", - "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], @@ -2971,9 +2952,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", - "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], @@ -2988,9 +2969,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", - "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], @@ -3007,13 +2988,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.3" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", - "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], @@ -3030,13 +3011,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.3" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", - "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], @@ -3053,13 +3034,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.3" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", - "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], @@ -3076,13 +3057,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.3" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", - "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], @@ -3099,13 +3080,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.3" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", - "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], @@ -3122,13 +3103,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.3" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", - "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], @@ -3145,13 +3126,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", - "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], @@ -3168,13 +3149,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", - "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, @@ -3189,9 +3170,9 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", - "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], @@ -3199,7 +3180,7 @@ "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -3209,9 +3190,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", - "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -3229,9 +3210,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", - "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -3249,9 +3230,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", - "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -4791,6 +4772,7 @@ "resolved": "https://registry.npmmirror.com/@types/express/-/express-5.0.6.tgz", "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -4929,6 +4911,7 @@ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5048,6 +5031,7 @@ "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.64.0", "@typescript-eslint/types": "8.64.0", @@ -5202,9 +5186,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5358,6 +5342,7 @@ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5393,6 +5378,7 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5506,13 +5492,10 @@ } }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", @@ -5617,13 +5600,13 @@ } }, "node_modules/axios": { - "version": "1.18.1", - "resolved": "https://registry.npmmirror.com/axios/-/axios-1.18.1.tgz", - "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } @@ -5835,9 +5818,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -5888,6 +5871,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.11.20", "caniuse-lite": "^1.0.30001810", @@ -5945,6 +5929,7 @@ "version": "1.1.2", "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, "license": "MIT" }, "node_modules/buffer-indexof-polyfill": { @@ -6228,31 +6213,16 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, "node_modules/concurrently": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", - "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.5.tgz", + "integrity": "sha512-YMZlvWrTPQEhjKp60XSmMGt60+pffbv6DyRnOMvVO98+J7aF8w8Ui9FhdcOwvSixc/kyR6SoVvmejcn/2lZBqg==", "dev": true, "license": "MIT", "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", - "shell-quote": "1.9.0", + "shell-quote": "1.12.0", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" @@ -6484,6 +6454,7 @@ "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -6941,6 +6912,7 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -7324,9 +7296,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz", - "integrity": "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==", + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", + "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -7738,6 +7710,7 @@ "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8007,15 +7980,6 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/exceljs/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/express": { "version": "4.22.2", "resolved": "https://registry.npmmirror.com/express/-/express-4.22.2.tgz", @@ -8140,9 +8104,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { @@ -8214,9 +8178,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { @@ -9017,9 +8981,9 @@ "license": "MIT" }, "node_modules/ip-address": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", - "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", + "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", "license": "MIT", "engines": { "node": ">= 12" @@ -9582,13 +9546,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/js-yaml/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/jsdom": { "version": "29.1.1", "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-29.1.1.tgz", @@ -9881,9 +9838,9 @@ } }, "node_modules/katex": { - "version": "0.16.47", - "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.47.tgz", - "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "version": "0.18.10", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.18.10.tgz", + "integrity": "sha512-/B6p9eY9DX7aHBfpkHdpirDTZ5QH9xTwL9y837PWuzuv41O5jFdqNQwVQEQsimY0/iVNkwBY4+4hMfhQ7d4Dbw==", "dev": true, "funding": [ "https://opencollective.com/katex", @@ -9891,20 +9848,20 @@ ], "license": "MIT", "dependencies": { - "commander": "^8.3.0" + "commander": "^15.0.0" }, "bin": { "katex": "cli.js" } }, "node_modules/katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=22.12.0" } }, "node_modules/keyv": { @@ -10486,19 +10443,17 @@ } }, "node_modules/mammoth": { - "version": "1.12.0", - "resolved": "https://registry.npmmirror.com/mammoth/-/mammoth-1.12.0.tgz", - "integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.13.0.tgz", + "integrity": "sha512-2cawcYFcP7ISfXbBcL9lmVgTNWo88v/52ND1gnywHtVT/pX0//puPnf0iMPV9ZgmuvNOE+zkIhDgZ9M2c2dDDg==", "license": "BSD-2-Clause", "dependencies": { "@xmldom/xmldom": "^0.8.6", "argparse": "~1.0.3", "base64-js": "^1.5.1", - "bluebird": "~3.4.0", "dingbat-to-unicode": "^1.0.1", "jszip": "^3.7.1", "lop": "^0.4.2", - "path-is-absolute": "^1.0.0", "underscore": "^1.13.1", "xmlbuilder": "^10.0.0" }, @@ -10699,14 +10654,13 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.3.0.tgz", - "integrity": "sha512-cjNbm3sttszgZeGfJR124D+jFEfkXCVAsoPBmFn9X7UxmDSFHWqE2CoEj0vrmSpuAFnqWR1Szcm9QTsiHr60Xw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz", + "integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", - "concat-stream": "^2.0.0", "type-is": "^1.6.18" }, "engines": { @@ -11129,6 +11083,7 @@ "resolved": "https://registry.npmmirror.com/pg/-/pg-8.23.0.tgz", "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", @@ -11386,9 +11341,9 @@ "license": "MIT" }, "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -11396,6 +11351,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/proxy-from-env": { @@ -11484,6 +11443,7 @@ "resolved": "https://registry.npmmirror.com/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11493,6 +11453,7 @@ "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -11588,9 +11549,9 @@ } }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -12145,9 +12106,9 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", - "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -12162,31 +12123,31 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.4", - "@img/sharp-darwin-x64": "0.35.4", - "@img/sharp-freebsd-wasm32": "0.35.4", - "@img/sharp-libvips-darwin-arm64": "1.3.3", - "@img/sharp-libvips-darwin-x64": "1.3.3", - "@img/sharp-libvips-linux-arm": "1.3.3", - "@img/sharp-libvips-linux-arm64": "1.3.3", - "@img/sharp-libvips-linux-ppc64": "1.3.3", - "@img/sharp-libvips-linux-riscv64": "1.3.3", - "@img/sharp-libvips-linux-s390x": "1.3.3", - "@img/sharp-libvips-linux-x64": "1.3.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", - "@img/sharp-libvips-linuxmusl-x64": "1.3.3", - "@img/sharp-linux-arm": "0.35.4", - "@img/sharp-linux-arm64": "0.35.4", - "@img/sharp-linux-ppc64": "0.35.4", - "@img/sharp-linux-riscv64": "0.35.4", - "@img/sharp-linux-s390x": "0.35.4", - "@img/sharp-linux-x64": "0.35.4", - "@img/sharp-linuxmusl-arm64": "0.35.4", - "@img/sharp-linuxmusl-x64": "0.35.4", - "@img/sharp-webcontainers-wasm32": "0.35.4", - "@img/sharp-win32-arm64": "0.35.4", - "@img/sharp-win32-ia32": "0.35.4", - "@img/sharp-win32-x64": "0.35.4" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" }, "peerDependenciesMeta": { "@types/node": { @@ -12231,9 +12192,9 @@ } }, "node_modules/shell-quote": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", - "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", "dev": true, "license": "MIT", "engines": { @@ -12353,9 +12314,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -12392,12 +12353,6 @@ "node": ">= 10.x" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, "node_modules/sql.js": { "version": "1.14.1", "resolved": "https://registry.npmmirror.com/sql.js/-/sql.js-1.14.1.tgz", @@ -12757,6 +12712,7 @@ "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -12928,6 +12884,7 @@ "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.28.0" }, @@ -13064,18 +13021,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13347,16 +13299,16 @@ } }, "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/vary": { @@ -13374,6 +13326,7 @@ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -14203,9 +14156,9 @@ } }, "node_modules/workbox-build/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/modules/openxyos/package.json b/modules/openxyos/package.json index 9a9bf822..84671b5b 100644 --- a/modules/openxyos/package.json +++ b/modules/openxyos/package.json @@ -11,7 +11,7 @@ "postinstall": "node scripts/patch-express-types.cjs", "typecheck": "tsc --noEmit", "lint": "eslint . --ext .ts,.tsx --max-warnings 0", - "test": "npm run test:db-persistence && npm run test:runtime-config && npm run test:cors-origins && npm run test:embed-trap && npm run test:hero-mascot && npm run test:r0-access-control && npm run test:module-settings && npm run test:llm-onboarding && npm run test:agent-studio && npm run test:freeos-ingest && npm run test:file-security && npm run test:download-token && npm run test:ws-ticket && npm run test:auth-provider && npm run test:pep && npm run test:audit-bundle && npm run test:tool-gateway && npm run test:v100", + "test": "npm run test:db-persistence && npm run test:runtime-config && npm run test:cors-origins && npm run test:embed-trap && npm run test:hero-mascot && npm run test:r0-access-control && npm run test:module-settings && npm run test:llm-onboarding && npm run test:agent-studio && npm run test:freeos-ingest && npm run test:file-security && npm run test:doc-parser && npm run test:download-token && npm run test:ws-ticket && npm run test:auth-provider && npm run test:pep && npm run test:audit-bundle && npm run test:tool-gateway && npm run test:v100", "test:db-persistence": "tsx scripts/test-db-persistence.ts", "test:runtime-config": "tsx scripts/test-runtime-config.ts", "test:cors-origins": "tsx scripts/test-cors-origins.ts", @@ -35,7 +35,8 @@ "test:module-settings": "tsx scripts/test-module-settings.ts", "test:agent-studio": "tsx scripts/test-agent-studio.ts", "test:llm-onboarding": "tsx scripts/test-llm-onboarding.ts", - "test:freeos-ingest": "tsx scripts/test-freeos-ingest.ts" + "test:freeos-ingest": "tsx scripts/test-freeos-ingest.ts", + "test:doc-parser": "tsx scripts/test-doc-parser.ts" }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -65,7 +66,7 @@ "sql.js": "^1.11.0", "ssh2": "^1.17.0", "unzipper": "^0.12.3", - "uuid": "^10.0.0", + "uuid": "^11.1.1", "ws": "^8.21.0" }, "devDependencies": { @@ -108,6 +109,13 @@ "node": ">=20.19" }, "overrides": { - "qs": "6.16.0" + "qs": "6.16.0", + "katex": "^0.18.2", + "exceljs": { + "uuid": "^11.1.1" + }, + "mammoth": { + "argparse": "^2.0.1" + } } } diff --git a/modules/openxyos/scripts/test-doc-parser.ts b/modules/openxyos/scripts/test-doc-parser.ts new file mode 100644 index 00000000..eb6791d3 --- /dev/null +++ b/modules/openxyos/scripts/test-doc-parser.ts @@ -0,0 +1,47 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import PDFDocument from "pdfkit"; +import { Document, Packer, Paragraph } from "docx"; +import ExcelJS from "exceljs"; +import { extractText } from "../backend/services/doc-parser"; + +async function main() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "freeos-doc-parser-")); + try { + const text = "FreeOS document import acceptance"; + const pdf = new PDFDocument(); + const chunks: Buffer[] = []; + const finished = new Promise((resolve, reject) => { + pdf.on("data", (chunk: Buffer) => chunks.push(chunk)); + pdf.on("end", () => resolve(Buffer.concat(chunks))); + pdf.on("error", reject); + }); + pdf.text(text); + pdf.end(); + const pdfPath = path.join(root, "sample.pdf"); + await fs.writeFile(pdfPath, await finished); + assert.ok((await extractText(pdfPath, ".pdf")).includes(text)); + + const docxPath = path.join(root, "sample.docx"); + const docx = new Document({ sections: [{ children: [new Paragraph(text)] }] }); + await fs.writeFile(docxPath, await Packer.toBuffer(docx)); + assert.equal(await extractText(docxPath, ".docx"), text); + + // Exercise ExcelJS with the patched, CommonJS-compatible UUID dependency. + const workbook = new ExcelJS.Workbook(); + workbook.addWorksheet("Import").addRow([text]); + const restored = new ExcelJS.Workbook(); + await restored.xlsx.load(await workbook.xlsx.writeBuffer()); + assert.equal(restored.getWorksheet("Import")?.getCell("A1").value, text); + console.log("PDF, DOCX and XLSX import/export acceptance passed"); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } +} + +main().catch((error: unknown) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/pyproject.toml b/pyproject.toml index 63fae65b..0b9c75a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "octop" -version = "0.0.10" +version = "0.1.0" description = "FreeOS — self-hosted multi-user multi-agent assistant with an optional organization OS module (Octop-compatible package name)" readme = "README.md" license = { text = "MIT" } diff --git a/scripts/README.md b/scripts/README.md index 72997813..556c04cb 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -25,12 +25,15 @@ ### 常用选项 ```bash -# 从 PyPI 安装最新版(默认) +# 从 FreeOS GitHub 源码安装最新版(默认) bash scripts/install.sh -# 指定版本 +# 从 FreeOS GitHub v0.1.0 标签安装 bash scripts/install.sh --version 0.1.0 +# 显式安装 PyPI 上游 Octop(不包含 FreeOS 下游修改) +bash scripts/install.sh --from-pypi + # 安装可选附加组件 bash scripts/install.sh --extras browser bash scripts/install.sh --extras browser,channels-feishu @@ -39,11 +42,11 @@ bash scripts/install.sh --extras browser,channels-feishu bash scripts/install.sh --from-source bash scripts/install.sh --from-source /path/to/orca -# 使用国内 PyPI 镜像加速依赖 +# 使用国内 PyPI 镜像加速 Python 依赖下载 bash scripts/install.sh --mirror https://mirrors.cloud.tencent.com/pypi/simple ``` -Windows PowerShell 等价参数:`-Version`、`-FromSource`、`-SourceDir`、`-Extras`。 +Windows PowerShell 等价参数:`-Version`、`-FromSource`、`-FromPyPI`、`-SourceDir`、`-Extras`。 ### 环境变量 @@ -65,7 +68,7 @@ octop run # 启动服务 → http://127.0.0.1:8088 --- -## 构建 PyPI wheel +## 构建 Python wheel(随 GitHub Release 分发) 先构建前端(产物写入 `src/octop/dashboard/`,与 `dashboard/vite.config.ts` 的 `outDir` 一致),再打包 wheel。 @@ -81,7 +84,7 @@ powershell -File scripts/wheel_build.ps1 输出:`dist/*.whl`、`dist/*.tar.gz` -发布前请确认 `pyproject.toml` 中的 `orcakit-harness-agent`、`harness-gateway` 等依赖已发布到 PyPI(`[tool.uv.sources]` 仅对 uv 本地开发生效,pip/PyPI 不读取)。 +Release workflow 将构建出的 wheel 和源码包附加到 GitHub Release。`octop` 是内部兼容用的发行包名,不要把 FreeOS 制品发布到 PyPI;安装脚本默认从 FreeOS GitHub 仓库安装。 --- diff --git a/scripts/install-octop.sh b/scripts/install-octop.sh index 493da44e..17703d34 100644 --- a/scripts/install-octop.sh +++ b/scripts/install-octop.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Octop 安装脚本 (macOS / Linux) -# 用法: bash scripts/install.sh # 从 PyPI 安装(默认) -# 或: bash scripts/install.sh --from-source # 从本地源码安装 +# 用法: bash scripts/install-octop.sh # 从 FreeOS GitHub 源码安装(默认) +# 或: bash scripts/install-octop.sh --from-pypi # 从 PyPI 安装 Octop(不含 FreeOS 下游改动) # 或: curl -fsSL /install.sh | bash # 远程安装 # # 将 Octop 安装到 ~/.octop,使用 uv 管理 Python 环境。 @@ -34,7 +34,7 @@ else fi VERSION="" -FROM_SOURCE=false +FROM_SOURCE=true EXTRAS="" PYPI_MIRROR="" @@ -81,9 +81,9 @@ Octop installer (macOS / Linux) Usage: bash install.sh [OPTIONS] Options: - --version Install a specific version (e.g. 0.1.0) [PyPI only] + --version Install a specific GitHub tag (e.g. 0.1.0; tag v0.1.0) --from-source [DIR] Install from source; clones the git repo if DIR is omitted - --from-pypi Install from PyPI (default) + --from-pypi Install upstream Octop from PyPI (does not include FreeOS changes) --extras Extra optional components (e.g. desktop, browser) --mirror Use a specific PyPI mirror (e.g. https://mirrors.aliyun.com/pypi/simple) -h, --help Show this help @@ -648,7 +648,11 @@ _clone_source_workspace() { git clone --depth 1 "$HARNESS_AGENT_REPO" "$workdir/harness-agent" git clone --depth 1 "$HARNESS_GATEWAY_REPO" "$workdir/harness-gateway" git clone --depth 1 "$HARNESS_BROWSER_REPO" "$workdir/harness-browser" - git clone --depth 1 "$OCTOP_REPO" "$workdir/orca" + if [ -n "$VERSION" ]; then + git clone --depth 1 --branch "v$VERSION" "$OCTOP_REPO" "$workdir/orca" + else + git clone --depth 1 "$OCTOP_REPO" "$workdir/orca" + fi } if [ "$FROM_SOURCE" = true ]; then diff --git a/scripts/install.bat b/scripts/install.bat index 8c500688..3980721e 100644 --- a/scripts/install.bat +++ b/scripts/install.bat @@ -2,16 +2,16 @@ setlocal EnableDelayedExpansion REM Octop Installer for Windows (cmd.exe) -REM Usage: install.bat [-Version X.Y.Z] [-FromSource] [-SourceDir DIR] [-Extras browser] [-Help] +REM Usage: install.bat [-Version X.Y.Z] [-FromSource | -FromPyPI] [-SourceDir DIR] [-Extras browser] [-Help] if defined OCTOP_HOME (set "OCTOP_HOME=%OCTOP_HOME%") else (set "OCTOP_HOME=%USERPROFILE%\.octop") set "OCTOP_VENV=%OCTOP_HOME%\venv" set "OCTOP_BIN=%OCTOP_HOME%\bin" set "PYTHON_VERSION=3.12" -if defined OCTOP_REPO (set "OCTOP_REPO=%OCTOP_REPO%") else (set "OCTOP_REPO=https://github.com/TencentCloud/Octop.git") +if defined OCTOP_REPO (set "OCTOP_REPO=%OCTOP_REPO%") else (set "OCTOP_REPO=https://github.com/XYAIStudio/FreeOS.git") set "ARG_VERSION=" -set "ARG_FROM_SOURCE=0" +set "ARG_FROM_SOURCE=1" set "ARG_SOURCE_DIR=" set "ARG_EXTRAS=" set "ARG_UV_PATH=" @@ -21,6 +21,7 @@ set "CONSOLE_AVAILABLE=0" if "%~1"=="" goto :done_args if /i "%~1"=="-Version" (set "ARG_VERSION=%~2" & shift & shift & goto :parse_args) if /i "%~1"=="-FromSource" (set "ARG_FROM_SOURCE=1" & shift & goto :parse_args) +if /i "%~1"=="-FromPyPI" (set "ARG_FROM_SOURCE=0" & shift & goto :parse_args) if /i "%~1"=="-SourceDir" (set "ARG_SOURCE_DIR=%~2" & shift & shift & goto :parse_args) if /i "%~1"=="-Extras" (set "ARG_EXTRAS=%~2" & shift & shift & goto :parse_args) if /i "%~1"=="-UvPath" (set "ARG_UV_PATH=%~2" & shift & shift & goto :parse_args) @@ -34,6 +35,7 @@ echo. echo Usage: install.bat [OPTIONS] echo -Version ^ Install specific version echo -FromSource Install from source +echo -FromPyPI Install upstream Octop from PyPI (does not include FreeOS changes) echo -SourceDir ^ Local source directory echo -Extras ^ Extra components (e.g. desktop, browser) echo -UvPath ^ Pre-installed uv.exe @@ -86,7 +88,11 @@ goto :install_verify where git >nul 2>&1 if errorlevel 1 (echo [octop] ERROR: git required & exit /b 1) set "CLONE_DIR=%TEMP%\octop-install-%RANDOM%" -git clone --depth 1 %OCTOP_REPO% "%CLONE_DIR%" +if defined ARG_VERSION ( + git clone --depth 1 --branch "v%ARG_VERSION%" %OCTOP_REPO% "%CLONE_DIR%" +) else ( + git clone --depth 1 %OCTOP_REPO% "%CLONE_DIR%" +) if errorlevel 1 exit /b 1 call :prepare_console "%CLONE_DIR%" uv pip install "%CLONE_DIR%%EXTRAS_SUFFIX%" --python "%VENV_PYTHON%" diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 48c4cb1c..4b891c1d 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -8,6 +8,7 @@ param( [string]$Version = "", [switch]$FromSource, + [switch]$FromPyPI, [string]$SourceDir = "", [string]$Extras = "", [string]$UvPath = "", @@ -20,12 +21,15 @@ $OctopHome = if ($env:OCTOP_HOME) { $env:OCTOP_HOME } else { Join-Path $HOME $OctopVenv = Join-Path $OctopHome "venv" $OctopBin = Join-Path $OctopHome "bin" $PythonVersion = "3.12" -$OctopRepo = if ($env:OCTOP_REPO) { $env:OCTOP_REPO } else { "https://github.com/TencentCloud/Octop.git" } +$OctopRepo = if ($env:OCTOP_REPO) { $env:OCTOP_REPO } else { "https://github.com/XYAIStudio/FreeOS.git" } function Write-Info { param([string]$Message) Write-Host "[octop] " -ForegroundColor Green -NoNewline; Write-Host $Message } function Write-Warn { param([string]$Message) Write-Host "[octop] " -ForegroundColor Yellow -NoNewline; Write-Host $Message } function Stop-WithError { param([string]$Message) Write-Host "[octop] ERROR: $Message" -ForegroundColor Red; exit 1 } +if ($FromSource -and $FromPyPI) { Stop-WithError "Choose only one of -FromSource or -FromPyPI." } +if (-not $FromPyPI) { $FromSource = $true } + if ($Help) { @" Octop Installer for Windows @@ -35,6 +39,7 @@ Usage: .\install.ps1 [OPTIONS] Options: -Version Install a specific version (e.g. 0.1.0) -FromSource Install from source (requires git, or use -SourceDir) + -FromPyPI Install upstream Octop from PyPI (does not include FreeOS changes) -SourceDir

Local source directory (used with -FromSource) -Extras Extra optional components (e.g. desktop, browser) -UvPath Path to a pre-installed uv.exe @@ -226,7 +231,11 @@ if ($FromSource) { } $cloneDir = Join-Path $env:TEMP "octop-install-$(Get-Random)" try { + if ($Version) { + git clone --depth 1 --branch "v$Version" $OctopRepo $cloneDir + } else { git clone --depth 1 $OctopRepo $cloneDir + } Prepare-Console $cloneDir uv pip install "${cloneDir}${ExtrasSuffix}" --python $VenvPython } finally { diff --git a/scripts/install.sh b/scripts/install.sh index 15138c8c..c90570ca 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Octop 安装脚本 (macOS / Linux) -# 用法: bash scripts/install.sh # 从 PyPI 安装(默认) -# 或: bash scripts/install.sh --from-source # 从本地源码安装 +# 用法: bash scripts/install.sh # 从 FreeOS GitHub 源码安装(默认) +# 或: bash scripts/install.sh --from-pypi # 从 PyPI 安装 Octop(不含 FreeOS 下游改动) # 或: curl -fsSL /install.sh | bash # 远程安装 # # 将 Octop 安装到 ~/.octop,使用 uv 管理 Python 环境。 @@ -34,7 +34,7 @@ else fi VERSION="" -FROM_SOURCE=false +FROM_SOURCE=true EXTRAS="" PYPI_MIRROR="" @@ -81,9 +81,9 @@ Octop installer (macOS / Linux) Usage: bash install.sh [OPTIONS] Options: - --version Install a specific version (e.g. 0.1.0) [PyPI only] + --version Install a specific GitHub tag (e.g. 0.1.0; tag v0.1.0) --from-source [DIR] Install from source; clones the git repo if DIR is omitted - --from-pypi Install from PyPI (default) + --from-pypi Install upstream Octop from PyPI (does not include FreeOS changes) --extras Extra optional components (e.g. desktop, browser) --mirror Use a specific PyPI mirror (e.g. https://mirrors.cloud.tencent.com/pypi/simple) -h, --help Show this help @@ -648,7 +648,11 @@ _clone_source_workspace() { git clone --depth 1 "$HARNESS_AGENT_REPO" "$workdir/harness-agent" git clone --depth 1 "$HARNESS_GATEWAY_REPO" "$workdir/harness-gateway" git clone --depth 1 "$HARNESS_BROWSER_REPO" "$workdir/harness-browser" - git clone --depth 1 "$OCTOP_REPO" "$workdir/orca" + if [ -n "$VERSION" ]; then + git clone --depth 1 --branch "v$VERSION" "$OCTOP_REPO" "$workdir/orca" + else + git clone --depth 1 "$OCTOP_REPO" "$workdir/orca" + fi } if [ "$FROM_SOURCE" = true ]; then diff --git a/src/octop/__init__.py b/src/octop/__init__.py index 307eb09e..d7857b72 100644 --- a/src/octop/__init__.py +++ b/src/octop/__init__.py @@ -2,4 +2,4 @@ from __future__ import annotations -__version__ = "0.0.10" +__version__ = "0.1.0" diff --git a/src/octop/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/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/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/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_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/uv.lock b/uv.lock index 75ee7d32..6b273614 100644 --- a/uv.lock +++ b/uv.lock @@ -2503,7 +2503,7 @@ wheels = [ [[package]] name = "octop" -version = "0.0.10" +version = "0.1.0" source = { editable = "." } dependencies = [ { name = "acme" },