From 676c4beb60ca3e33e3cff1b140a4fe350442cc9c Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 21 Jul 2026 18:12:43 +0800 Subject: [PATCH 01/14] =?UTF-8?q?=E8=A1=A5=E9=BD=90Gitea=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 拆分前端、后端与原生壳测试任务 接入server-rs workspace正式全量测试 将AI游戏创作分支及独立依赖纳入CI 修复既有测试断言、格式与异步稳定性问题 稳定AI Tauri并发回执测试 更新开发运维和共享流程文档 --- .gitea/workflows/project-ci.yml | 308 ++ apps/ai-game-creator-shell/package-lock.json | 4771 +++++++++++++++++ .../scripts/agent-runtime-steer-real-e2e.mjs | 2 +- .../smoke-agent-run-local-provider.mjs | 8 +- .../src-tauri/src/agent.rs | 12 + .../src-tauri/src/tests.rs | 25 +- apps/ai-game-creator-shell/src/App.tsx | 15 +- apps/ai-game-creator-shell/src/main.tsx | 3 +- .../src/view/project-development/index.tsx | 4 +- .../tests/appSurface.test.ts | 145 +- .../processSessionRealE2eFixture.test.ts | 5 +- .../tests/rememberCommand.test.ts | 10 +- .../shared-memory/development-workflow.md | 9 + ...发运维】本地开发验证与生产运维-2026-05-15.md | 17 + package-lock.json | 51 - package.json | 2 - .../src/contracts/gameCreationApp.test.ts | 8 +- .../crates/api-server/src/story_battles.rs | 28 +- .../crates/api-server/src/story_sessions.rs | 36 +- .../platform-agent/src/game_creation.rs | 54 +- .../tests/generated_asset_sheets.rs | 6 +- .../shared-contracts/src/game_creation_app.rs | 11 +- .../spacetime-module/src/bark_battle.rs | 6 +- .../crates/spacetime-module/src/puzzle.rs | 69 +- src/hooks/runtimeAuthGuards.test.tsx | 2 +- .../puzzle-runtime/puzzleLocalRuntime.test.ts | 19 +- vitest.config.ts | 12 +- 27 files changed, 5367 insertions(+), 271 deletions(-) create mode 100644 .gitea/workflows/project-ci.yml create mode 100644 apps/ai-game-creator-shell/package-lock.json diff --git a/.gitea/workflows/project-ci.yml b/.gitea/workflows/project-ci.yml new file mode 100644 index 000000000..8ac6b457e --- /dev/null +++ b/.gitea/workflows/project-ci.yml @@ -0,0 +1,308 @@ +name: Project CI + +on: + push: + branches: + - master + - codex/ai-game-creator-app + pull_request: + workflow_dispatch: + +permissions: + contents: read + +env: + CI: 'true' + CARGO_INCREMENTAL: '0' + CARGO_TERM_COLOR: always + RUSTC_WRAPPER: '' + CARGO_BUILD_RUSTC_WRAPPER: '' + +jobs: + repository-checks: + name: Repository checks + runs-on: ubuntu-latest + steps: + - name: Checkout full history + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install base tools + shell: bash + run: | + set -euo pipefail + command -v apt-get >/dev/null 2>&1 || { + echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2 + exit 1 + } + sudo_command='' + if command -v sudo >/dev/null 2>&1; then + sudo_command='sudo' + fi + ${sudo_command} apt-get update + ${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates \ + curl + + - name: Set up Node.js 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '22' + + - name: Resolve comparison base + shell: bash + run: | + set -euo pipefail + base_ref="$(node -e ' + const fs = require("node:fs"); + const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")); + process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? ""); + ')" + if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then + git cat-file -e "${base_ref}^{commit}" 2>/dev/null || { + echo "comparison base commit is unavailable: ${base_ref}" >&2 + exit 1 + } + else + base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)" + fi + if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \ + && ! git merge-base --is-ancestor "${base_ref}" HEAD; then + echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2 + exit 1 + fi + echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}" + + - name: Set up repository Rust toolchain + shell: bash + run: | + set -euo pipefail + if ! command -v rustup >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain none + fi + echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}" + export PATH="${HOME}/.cargo/bin:${PATH}" + toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" + test -n "${toolchain}" + rustup toolchain install "${toolchain}" --profile minimal --component rustfmt + rustc --version + cargo --version + rustfmt --version + + - name: Install npm dependencies + run: npm ci + + - name: Run repository lint gates + run: npm run lint + + - name: Build web applications + run: npm run build + + - name: Validate content data + run: npm run check:content + + - name: Check committed whitespace + shell: bash + run: | + set -euo pipefail + base_ref="${SPACETIME_SCHEMA_BASE_REF:-}" + test -n "${base_ref}" + git cat-file -e "${base_ref}^{commit}" + git diff --check "${base_ref}"...HEAD + + frontend-tests: + name: Frontend tests + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - name: Set up Node.js 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '22' + + - name: Install npm dependencies + run: npm ci + + - name: Install AI game creator dependencies + run: npm ci --prefix apps/ai-game-creator-shell + + - name: Run frontend and script tests + run: npm run test + + backend-tests: + name: Backend tests + runs-on: ubuntu-latest + steps: + - name: Checkout full history + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install backend build dependencies + shell: bash + run: | + set -euo pipefail + command -v apt-get >/dev/null 2>&1 || { + echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2 + exit 1 + } + sudo_command='' + if command -v sudo >/dev/null 2>&1; then + sudo_command='sudo' + fi + ${sudo_command} apt-get update + ${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + clang \ + cmake \ + curl \ + ffmpeg \ + libclang-dev \ + libcurl4-openssl-dev \ + libssl-dev \ + lld \ + pkg-config + + - name: Set up Node.js 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '22' + + - name: Resolve comparison base + shell: bash + run: | + set -euo pipefail + base_ref="$(node -e ' + const fs = require("node:fs"); + const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")); + process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? ""); + ')" + if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then + git cat-file -e "${base_ref}^{commit}" 2>/dev/null || { + echo "comparison base commit is unavailable: ${base_ref}" >&2 + exit 1 + } + else + base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)" + fi + if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \ + && ! git merge-base --is-ancestor "${base_ref}" HEAD; then + echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2 + exit 1 + fi + echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}" + + - name: Set up repository Rust toolchain + shell: bash + run: | + set -euo pipefail + if ! command -v rustup >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain none + fi + echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}" + export PATH="${HOME}/.cargo/bin:${PATH}" + toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" + test -n "${toolchain}" + rustup toolchain install "${toolchain}" --profile minimal --component rustfmt + rustc --version + cargo --version + rustfmt --version + + - name: Install npm dependencies + run: npm ci + + - name: Check server-rs boundaries + run: npm run check:server-rs-ddd + + - name: Run server-rs workspace tests + run: cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml + + - name: Check api-server targets + run: cargo check --locked -p api-server --all-targets --manifest-path server-rs/Cargo.toml + + - name: Check SpacetimeDB module + run: cargo check --locked -p spacetime-module --manifest-path server-rs/Cargo.toml + + native-shell-tests: + name: Native shell tests + runs-on: ubuntu-latest + steps: + - name: Checkout full history + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install native shell build dependencies + shell: bash + run: | + set -euo pipefail + command -v apt-get >/dev/null 2>&1 || { + echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2 + exit 1 + } + sudo_command='' + if command -v sudo >/dev/null 2>&1; then + sudo_command='sudo' + fi + ${sudo_command} apt-get update + ${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + clang \ + cmake \ + curl \ + file \ + libayatana-appindicator3-dev \ + libssl-dev \ + libwebkit2gtk-4.1-dev \ + libxdo-dev \ + librsvg2-dev \ + lld \ + patchelf \ + pkg-config \ + wget + + - name: Set up Node.js 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '22' + + - name: Set up repository Rust toolchain + shell: bash + run: | + set -euo pipefail + if ! command -v rustup >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain none + fi + echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}" + export PATH="${HOME}/.cargo/bin:${PATH}" + toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" + test -n "${toolchain}" + rustup toolchain install "${toolchain}" --profile minimal --component rustfmt + rustc --version + cargo --version + rustfmt --version + + - name: Install npm dependencies + run: npm ci + + - name: Install AI game creator dependencies + run: npm ci --prefix apps/ai-game-creator-shell + + - name: Run native shell gates + run: npm run check:native-shells + + - name: Ensure native lockfiles are unchanged + run: git diff --exit-code -- apps/desktop-shell/src-tauri/Cargo.lock apps/ai-game-creator-shell/src-tauri/Cargo.lock diff --git a/apps/ai-game-creator-shell/package-lock.json b/apps/ai-game-creator-shell/package-lock.json new file mode 100644 index 000000000..048c92537 --- /dev/null +++ b/apps/ai-game-creator-shell/package-lock.json @@ -0,0 +1,4771 @@ +{ + "name": "@genarrative/ai-game-creator-shell", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@genarrative/ai-game-creator-shell", + "version": "0.1.0", + "dependencies": { + "@lexical/react": "^0.47.0", + "@lexical/utils": "^0.47.0", + "@tauri-apps/plugin-clipboard-manager": "2.3.2", + "@tauri-apps/plugin-opener": "~2", + "@vitejs/plugin-react": "^5.0.4", + "lexical": "^0.47.0", + "lucide-react": "^0.546.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", + "vite": "^6.2.0", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.14", + "@tauri-apps/cli": "^2.11.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "tailwindcss": "^4.1.14", + "typescript": "~5.8.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.20", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.20.tgz", + "integrity": "sha512-CMqMy7OaXl9W0eq1Uy7L7i2Y/anPvHmFmESd2CEw0t5YvZhcVCeo4MBevAmswRllX7Y2dEidA4ozGPunLSTQpw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.9", + "@floating-ui/utils": "^0.2.12", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lexical/a11y": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/a11y/-/a11y-0.47.0.tgz", + "integrity": "sha512-MlAL25PxThTYQuFn2aOwyfY9+/wKloAd8fbf4a1GeFATVgvirC8xACBmERpqpf4A06OdLIoIZ5J/qtgJXsdn1g==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.47.0", + "@lexical/utils": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/clipboard": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.47.0.tgz", + "integrity": "sha512-OIB+NxHjfpRJJoTtU/mqeb7vPVMks5l97QgwoZthQLR/GIc5zrOtvoLftxl5hKAG5lwOe9BzhashKgVcsSPB3g==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.47.0", + "@lexical/html": "0.47.0", + "@lexical/internal": "0.47.0", + "@lexical/list": "0.47.0", + "@lexical/selection": "0.47.0", + "@lexical/utils": "0.47.0", + "@types/trusted-types": "^2.0.7", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/code-core": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/code-core/-/code-core-0.47.0.tgz", + "integrity": "sha512-L0hg+57yzQDhYRV5NFyTwSz2+N3O/iLBgRuMNccE3ieltPPSq7lCBTKOuXF3G0ot3NVudu8//ewHjw7OCMqI5g==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.47.0", + "@lexical/html": "0.47.0", + "@lexical/internal": "0.47.0", + "@lexical/utils": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/devtools-core": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.47.0.tgz", + "integrity": "sha512-u/+WHfs9p6Jiz83OdWzM2W7xc/BXUqN/RN/o97qFP+H7XX/D5fD3Sn0q/0ymyDgacN7sI4HNxQttkrKXKn1PCA==", + "license": "MIT", + "dependencies": { + "@lexical/html": "0.47.0", + "@lexical/link": "0.47.0", + "@lexical/mark": "0.47.0", + "@lexical/table": "0.47.0", + "@lexical/utils": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "react": ">=18.x", + "react-dom": ">=18.x", + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/dragon": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.47.0.tgz", + "integrity": "sha512-qqkro6sTLddOh2aYSqoXtm1QOQ/DY71AtWj3XmHPIPtxN/PkVKbvJBVZiP/a9kgFoPs+2a2Tlq9cCIDf6xSHuw==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/extension": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/extension/-/extension-0.47.0.tgz", + "integrity": "sha512-F9WyXB0eDsCbeSuShzgjBS3ugxNnTvHQa+P+VZiVSWilbDNGjJyysco2JCwn5ruLmEP4MXCQn5IKeW7A9FG8KQ==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.47.0", + "@lexical/utils": "0.47.0", + "@preact/signals-core": "^1.14.1", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/hashtag": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.47.0.tgz", + "integrity": "sha512-i5ML0v5d52h8hen4cdS/tnqfiteWbvpwihJgZR9hLIk9w6vQJISXUWzg4jRPQIezWwU1xikbBLkT5rD+Qth7tw==", + "license": "MIT", + "dependencies": { + "@lexical/text": "0.47.0", + "@lexical/utils": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/history": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.47.0.tgz", + "integrity": "sha512-CqAR6G4x6WewKS2uDgf2N1/rszIlXiDCW4dwu0IiQV9oVbhOklvAmPBJX6319myJxB+ArtjuuMgDU3gi/iTqXA==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.47.0", + "@lexical/utils": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/html": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.47.0.tgz", + "integrity": "sha512-VrJ9tSRuoUP5tTQb7TB+6PTW0Gh5aC3Hnne4GuXDwKUIzoWFXWkxlB0oIlfL5HVljrzO/Ra65v5M3RnR4TX1PA==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.47.0", + "@lexical/internal": "0.47.0", + "@lexical/selection": "0.47.0", + "@lexical/utils": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/internal": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/internal/-/internal-0.47.0.tgz", + "integrity": "sha512-VKDTHajXvjCkjYHwWra4Qjz8AI6PWeNPW7l6Q9AIlAwUiolAXrlkyfrBAFzdAP360IyDbC2gO9fwTb9ZvZM85Q==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/link": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.47.0.tgz", + "integrity": "sha512-lDAbTyJwufKs3yUn1D4AY0IoBU9O4JXzTDqSRa8qVCK7LC3+cF0u+rR5o9RJfxtHUzmALk+s2I0IqlxGCyIocg==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.47.0", + "@lexical/html": "0.47.0", + "@lexical/internal": "0.47.0", + "@lexical/utils": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/list": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.47.0.tgz", + "integrity": "sha512-g8AApVZNqqcCDXfJtgxEfAv7ylK0HmblRaJYMxB0OI7nlLipUjutIUy8cRX5GKI0woa+oGPYi68cZcMQnBysGw==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.47.0", + "@lexical/html": "0.47.0", + "@lexical/internal": "0.47.0", + "@lexical/utils": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/mark": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.47.0.tgz", + "integrity": "sha512-/9ZS9NSjrqJ/sTZ7l2KFiBxZbbXEmPp/JxtN5sTlM+F4a/hTRreROWlcXgTLyf9g8GTnRyZScvBqFYKWd6HR1g==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/markdown": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.47.0.tgz", + "integrity": "sha512-mtfcomzk/ZT2wtImEzYeFB6VLsOYM6RqBmwnvEMp5alvJISXFhIHDKAT4BkUMMtWGM6AnrlytZyMNPb4QwnZIA==", + "license": "MIT", + "dependencies": { + "@lexical/code-core": "0.47.0", + "@lexical/internal": "0.47.0", + "@lexical/link": "0.47.0", + "@lexical/list": "0.47.0", + "@lexical/rich-text": "0.47.0", + "@lexical/selection": "0.47.0", + "@lexical/text": "0.47.0", + "@lexical/utils": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/overflow": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.47.0.tgz", + "integrity": "sha512-XR+F2mni27TgjmE2/PsFYXkAq0AIMV8JJh5VSeHiSg0mccKSfEqsfTxYlwV+um2eGT+6XggtpwFMDy6ZjC+uMw==", + "license": "MIT", + "dependencies": { + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/plain-text": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.47.0.tgz", + "integrity": "sha512-MyRjCpEnDJuEeoT/ndYI/i2605QM2bAgFEi6sX/NObYDJSuupC2iY6tOEbCFZ1s0Y1SX6arNrrAPhwygcemz7g==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.47.0", + "@lexical/dragon": "0.47.0", + "@lexical/extension": "0.47.0", + "@lexical/selection": "0.47.0", + "@lexical/utils": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/react": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.47.0.tgz", + "integrity": "sha512-4y2iEKghKcYcJ8+GoO8pqyvwjJFVDWR71Ezm37lLQGmSTFKY50miTJmgKI12GeL4hLWQjePpB3eVdmSQHG1b7g==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.19", + "@lexical/a11y": "0.47.0", + "@lexical/devtools-core": "0.47.0", + "@lexical/dragon": "0.47.0", + "@lexical/extension": "0.47.0", + "@lexical/hashtag": "0.47.0", + "@lexical/history": "0.47.0", + "@lexical/internal": "0.47.0", + "@lexical/link": "0.47.0", + "@lexical/list": "0.47.0", + "@lexical/mark": "0.47.0", + "@lexical/markdown": "0.47.0", + "@lexical/overflow": "0.47.0", + "@lexical/plain-text": "0.47.0", + "@lexical/rich-text": "0.47.0", + "@lexical/table": "0.47.0", + "@lexical/text": "0.47.0", + "@lexical/utils": "0.47.0", + "@lexical/yjs": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "react": ">=18.x", + "react-dom": ">=18.x", + "typescript": ">=5.2", + "yjs": ">=13.5.22" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "yjs": { + "optional": true + } + } + }, + "node_modules/@lexical/rich-text": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.47.0.tgz", + "integrity": "sha512-GtRH7KNW7fVJzd3Xftdr/EPXaMqHt2xCIO/eJtf17Yrs7vlVOljM0xMcDoj4QOY2Gp4p3CheLlwBbcO96YYV0A==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.47.0", + "@lexical/dragon": "0.47.0", + "@lexical/extension": "0.47.0", + "@lexical/html": "0.47.0", + "@lexical/selection": "0.47.0", + "@lexical/utils": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/selection": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.47.0.tgz", + "integrity": "sha512-/q+eXnryZxCeqeWAODhTRlJL+jGa6/vIhE/bh+KvHmLbZJM8qfwa0qzt4rb3g+L1/CbjcowS/Xwv2ha1OmjBFQ==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/table": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.47.0.tgz", + "integrity": "sha512-T0sOB1i0I05f2U+tTVXDitYq4ACtFob42gULwwqqKfQ/SEPq26Kfd5CljI1rSS4+N+vQz/VTDYF/BN7ZMgHpjA==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.47.0", + "@lexical/extension": "0.47.0", + "@lexical/html": "0.47.0", + "@lexical/internal": "0.47.0", + "@lexical/utils": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/text": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.47.0.tgz", + "integrity": "sha512-Rj1xa/MMhgOckjOJ+ifDUO8BUyi7o+a/vpYcsftMTCpiKmeteO+qSAKoyhvZke496nLh/WFCWKw/iD43bst/Ig==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/utils": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.47.0.tgz", + "integrity": "sha512-uVAqNr5qiE4t9GSj+L4rv06WaIGADzblxEKCn134sPzvsVouDv0VTT0pBKjQYUgshT4z9iW5cFtO11+iPacBYg==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.47.0", + "@lexical/selection": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/yjs": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.47.0.tgz", + "integrity": "sha512-EKw1df2cmUTQrfSp1EnXqsHtNjwgxS973CRor0W4GWmIQJyKdmf6cmA7cct3flkyH5tE/xDnpr8sy38U4R2hlQ==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.47.0", + "@lexical/selection": "0.47.0", + "lexical": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2", + "yjs": ">=13.5.22" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@preact/signals-core": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.4.tgz", + "integrity": "sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", + "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-clipboard-manager": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.3.2.tgz", + "integrity": "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", + "integrity": "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.44", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.44.tgz", + "integrity": "sha512-T3ghW+sl/ZJ8w1v/yQx3qvJ9040DWoLBz8JT/CILbAKcFyG9b2MRe75v6W5uXjv6uH1lumK2Kv46y2zSkcej0Q==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.394", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.394.tgz", + "integrity": "sha512-Wmt2Gm0o8JWBuGgmc4XZ0u9s1RaCRqhxP47phplmfg04+qypTUurpeJGP45A7Fhv7jdrrVH44PLlR9qXo37cVQ==", + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", + "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "peer": true, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lexical": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.47.0.tgz", + "integrity": "sha512-ZKsxsk3jUpXsRtG20EBq42z2bq8A20UHtjqvVT/kIxfsaiXwaRFBBcLSFxPa77j+hXkBF5w96C3/imwtmLoRdg==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.47.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/lib0": { + "version": "0.2.117", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", + "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", + "license": "MIT", + "peer": true, + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "devOptional": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.546.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.546.0.tgz", + "integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.21.tgz", + "integrity": "sha512-v4sDNP3fdNiWMfabO7OwOQdOX8TiQSztKyT1Wj0w+j7LDallJThJRBBBmzVGyYj0crMh7jlV4zepPkiNu9UwDQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yjs": { + "version": "13.6.31", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.31.tgz", + "integrity": "sha512-Eq+5BRfbeGyqGVrTJL3bEcr8gKkxPuyuoHmAwpk52fDb8kOVMrfVSTRPd6yiGgX5Fskb96qCRjzjbRjrL4YEnw==", + "license": "MIT", + "peer": true, + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-steer-real-e2e.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-steer-real-e2e.mjs index 961f1fd7e..ba096ec49 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-steer-real-e2e.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-steer-real-e2e.mjs @@ -1,5 +1,5 @@ import { spawn } from 'node:child_process'; -import { createHash, randomUUID } from 'node:crypto'; +import { randomUUID } from 'node:crypto'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; diff --git a/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs b/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs index 4a5491a6c..660b86184 100644 --- a/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs +++ b/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs @@ -198,7 +198,9 @@ const server = http.createServer((request, response) => { let requestJson = null; try { requestJson = JSON.parse(requestBody); - } catch {} + } catch { + // Keep the request invalid so the provider fixture can return its normal error path. + } const content = responses[responseIndex++]; if (!content) { response.writeHead(500, { 'content-type': 'application/json' }); @@ -885,7 +887,9 @@ function resolveChromeBin() { try { accessSync(candidate); return candidate; - } catch {} + } catch { + // Try the next supported system browser path. + } } return 'google-chrome'; } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index b6c87603e..6d3bb9b6d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -6631,6 +6631,18 @@ pub(crate) fn spawn_next_game_creator_agent_background_task_drain( Ok(()) } +#[cfg(test)] +pub(crate) async fn drain_next_game_creator_agent_background_tasks_for_test( + root: &Path, + agent_id: &str, +) -> Result<(), String> { + let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock(root, agent_id)? + .ok_or_else(|| "测试无法获取 Agent Runtime 后台任务锁".to_string())?; + let _runtime_lock = runtime_lock; + drain_next_game_creator_agent_background_tasks(root.to_path_buf(), agent_id.to_string()).await; + Ok(()) +} + pub(crate) fn spawn_next_game_creator_agent_background_task_drain_with_lock( root: &Path, agent_id: &str, diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/tests.rs index 145dabad9..69e32c129 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -24148,24 +24148,15 @@ async fn queued_delegate_receipt_is_suppressed_when_parent_cancels_before_drain( .expect("cancel parent while receipt is queued"); drop(parent_lock); - spawn_next_game_creator_agent_background_task_drain(&root, "design-director") + drain_next_game_creator_agent_background_tasks_for_test(&root, "design-director") + .await .expect("drain queued receipt"); - let mut drained_receipt = receipt.clone(); - // The full suite can saturate Tauri's shared executor with process fixtures. - for _ in 0..1_500 { - let runtime = read_game_creator_agent_runtime_at(&root, "design-director") - .expect("read drained receipt runtime"); - drained_receipt = runtime - .recent_tasks - .into_iter() - .find(|task| task.run_id == receipt.run_id) - .expect("receipt remains recorded"); - if drained_receipt.status != "pending" { - break; - } - tokio::time::sleep(Duration::from_millis(20)).await; - } - let receipt = drained_receipt; + let receipt = read_game_creator_agent_runtime_at(&root, "design-director") + .expect("read drained receipt runtime") + .recent_tasks + .into_iter() + .find(|task| task.run_id == receipt.run_id) + .expect("receipt remains recorded"); assert_eq!(receipt.status, "cancelled"); assert_eq!(receipt.phase, "parent-terminal"); let conversation = read_local_conversation_at(&root, Some("design-director")) diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 482851b37..96a383707 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -32,10 +32,6 @@ import { useRef, useState, } from 'react'; -import ProjectDevelopmentView, { - type ProjectAgentResultSummary, - type ProjectAgentRuntimeSummary, -} from './view/project-development'; import { PlatformMudPointWalletEntry, @@ -105,6 +101,10 @@ import HomeView, { } from './view/home'; import { useLauncherHomeDraftStore } from './view/home/useHomeDraftStore'; import { type LauncherView, Sidebar } from './view/layout'; +import ProjectDevelopmentView, { + type ProjectAgentResultSummary, + type ProjectAgentRuntimeSummary, +} from './view/project-development'; const seedManifest = createGameCreationAppManifest( 'local-project-draft', @@ -3284,7 +3284,7 @@ function ProjectSupervisorRuntimePanel({ if (runtime?.status === 'failed' || runtime?.phase === 'failed') { setSupervisorRetryFeedback(''); } - }, [runtime?.runId]); + }, [runtime?.phase, runtime?.runId, runtime?.status]); const status = projectSupervisorRuntimeStatusLabel(runtime, error); const collaboratingRuntimes = projectSupervisorCollaboratingAgentRuntimes( runtime, @@ -18563,8 +18563,7 @@ export function App({ !projectSupervisorOnly || !invoke || !nextProjectPath || - !supervisorRunId || - !projectSupervisorRuntime + !supervisorRunId ) { return; } @@ -27905,6 +27904,8 @@ export function App({ return () => { disposed = true; }; + // Candidate contents are fully represented by professionalResultCandidateKey. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ localProject?.projectPath, professionalResultCandidateKey, diff --git a/apps/ai-game-creator-shell/src/main.tsx b/apps/ai-game-creator-shell/src/main.tsx index 76030157a..916ae2877 100644 --- a/apps/ai-game-creator-shell/src/main.tsx +++ b/apps/ai-game-creator-shell/src/main.tsx @@ -1,8 +1,9 @@ +import './styles.css'; + import React from 'react'; import { createRoot } from 'react-dom/client'; import { App, AuthenticatedClient, WorkspaceLauncher } from './App'; -import './styles.css'; const initialSearchParams = new URLSearchParams(window.location.search); const supervisorChatMode = diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index 9448ab6b9..6a27d6ad3 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -16,8 +16,6 @@ import { Sparkles, X, } from 'lucide-react'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; import { type CSSProperties, type DragEvent, @@ -30,6 +28,8 @@ import { useRef, useState, } from 'react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; import type { GameCreationAppAgentGroup, diff --git a/apps/ai-game-creator-shell/tests/appSurface.test.ts b/apps/ai-game-creator-shell/tests/appSurface.test.ts index 3492a0aac..ee33ea745 100644 --- a/apps/ai-game-creator-shell/tests/appSurface.test.ts +++ b/apps/ai-game-creator-shell/tests/appSurface.test.ts @@ -1,6 +1,7 @@ /** @vitest-environment jsdom */ import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; + import { act, cleanup, @@ -780,9 +781,7 @@ describe('AI 游戏创作 App 界面边界', () => { fireEvent.click(riskApproval); expect(riskApproval.getAttribute('aria-checked')).toBe('false'); expect(riskApproval.getAttribute('data-unavailable')).toBe('true'); - expect( - screen.getAllByText('Rank 规则待定,当前暂不可用'), - ).toHaveLength(2); + expect(screen.getAllByText('Rank 规则待定,当前暂不可用')).toHaveLength(2); fireEvent.click(screen.getByRole('button', { name: '完成' })); expect( screen.getByRole('button', { @@ -882,8 +881,12 @@ describe('AI 游戏创作 App 界面边界', () => { ).not.toBeNull(); expect(within(receiptDialog).getByText('仅有美术计划')).not.toBeNull(); expect(within(receiptDialog).getByText('没有图片文件')).not.toBeNull(); - expect(within(receiptDialog).getByText('等待实际素材生成。')).not.toBeNull(); - expect(receiptDialog.querySelector('.game-resource-focus-body')).not.toBeNull(); + expect( + within(receiptDialog).getByText('等待实际素材生成。'), + ).not.toBeNull(); + expect( + receiptDialog.querySelector('.game-resource-focus-body'), + ).not.toBeNull(); expect(receiptDialog.querySelector('ul')).not.toBeNull(); expect(receiptDialog.querySelector('strong')).not.toBeNull(); }); @@ -949,22 +952,24 @@ describe('AI 游戏创作 App 界面边界', () => { taskId: 'art-asset-plan', }, }); - const invoke = vi.fn(async (command: string, args?: Record) => { - if (command === 'read_local_project_image_preview') { - expect(args).toEqual({ - projectPath: '/tmp/workbench-runnable', - relativePath: 'assets/hero.png', - }); - return { - path: 'assets/hero.png', - mediaType: 'image/png', - byteLen: 12, - dataUrl: - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', - }; - } - throw new Error(`unexpected invoke ${command}`); - }); + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'read_local_project_image_preview') { + expect(args).toEqual({ + projectPath: '/tmp/workbench-runnable', + relativePath: 'assets/hero.png', + }); + return { + path: 'assets/hero.png', + mediaType: 'image/png', + byteLen: 12, + dataUrl: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', + }; + } + throw new Error(`unexpected invoke ${command}`); + }, + ); window.__TAURI__ = { core: { invoke } }; render( @@ -2192,9 +2197,7 @@ describe('AI 游戏创作 App 界面边界', () => { await waitFor(() => { expect(rechargeOrderRequestCount).toBe(2); }); - fireEvent.click( - screen.getByRole('button', { name: '关闭购买更多泥点' }), - ); + fireEvent.click(screen.getByRole('button', { name: '关闭购买更多泥点' })); await act(async () => { releaseLateRechargeOrder?.(); }); @@ -2202,9 +2205,7 @@ describe('AI 游戏创作 App 界面边界', () => { expect( await screen.findByRole('dialog', { name: '购买更多泥点' }), ).not.toBeNull(); - expect( - screen.queryByRole('dialog', { name: '微信扫码支付' }), - ).toBeNull(); + expect(screen.queryByRole('dialog', { name: '微信扫码支付' })).toBeNull(); }); it('opens the help notice and account menu from the sidebar', () => { @@ -6958,7 +6959,9 @@ describe('AI 游戏创作 App 界面边界', () => { agentId: 'project-supervisor', sessionId: harness.sessionId, }); - expect(within(surface).getByRole('button', { name: '设置' })).not.toBeNull(); + expect( + within(surface).getByRole('button', { name: '设置' }), + ).not.toBeNull(); expect(screen.queryByLabelText('选择 Agent')).toBeNull(); expect(screen.queryByLabelText('项目总控 Agent 状态')).toBeNull(); expect(screen.queryByLabelText('专业 Agent 协作状态')).toBeNull(); @@ -7007,14 +7010,11 @@ describe('AI 游戏创作 App 界面边界', () => { ); renderSupervisorChat(); - const firstSurface = await screen.findByLabelText( - '项目总控 Agent 纯聊天', - ); + const firstSurface = await screen.findByLabelText('项目总控 Agent 纯聊天'); await within(firstSurface).findByLabelText('项目总控消息'); - fireEvent.change( - within(firstSurface).getByLabelText('项目总控对话内容'), - { target: { value: '这条草稿还没有发送' } }, - ); + fireEvent.change(within(firstSurface).getByLabelText('项目总控对话内容'), { + target: { value: '这条草稿还没有发送' }, + }); cleanup(); renderSupervisorChat(); @@ -7131,9 +7131,8 @@ describe('AI 游戏创作 App 界面边界', () => { ); const surface = await screen.findByLabelText('项目总控 Agent 纯聊天'); - let pendingAction = await within(surface).findByLabelText( - '项目总控 Agent 待确认动作', - ); + let pendingAction = + await within(surface).findByLabelText('项目总控 Agent 待确认动作'); expect(within(pendingAction).getByText('file.write')).not.toBeNull(); expect(within(pendingAction).getByText('game/index.html')).not.toBeNull(); fireEvent.click( @@ -7152,9 +7151,8 @@ describe('AI 游戏创作 App 界面边界', () => { ); }); - pendingAction = await within(surface).findByLabelText( - '项目总控 Agent 待确认动作', - ); + pendingAction = + await within(surface).findByLabelText('项目总控 Agent 待确认动作'); expect(within(pendingAction).getByText('command.exec')).not.toBeNull(); expect(within(pendingAction).getByText('npm test')).not.toBeNull(); fireEvent.click( @@ -7216,12 +7214,13 @@ describe('AI 游戏创作 App 界面边界', () => { expect( within(card).getByText('首版角色规范图采用哪种美术方向?'), ).not.toBeNull(); + expect(within(card).getByText('优先验证轮廓与动作可读性。')).not.toBeNull(); expect( - within(card).getByText('优先验证轮廓与动作可读性。'), - ).not.toBeNull(); - expect( - (within(surface).getByLabelText('项目总控对话内容') as HTMLTextAreaElement) - .disabled, + ( + within(surface).getByLabelText( + '项目总控对话内容', + ) as HTMLTextAreaElement + ).disabled, ).toBe(true); fireEvent.click(within(card).getByRole('button', { name: /像素风/ })); @@ -7690,9 +7689,7 @@ describe('AI 游戏创作 App 界面边界', () => { expect(professionalRuntimeReadCount).toBeGreaterThan(0); }); expect(screen.queryByLabelText('专业 Agent 实时状态')).toBeNull(); - const supervisorStatusPanel = screen.getByLabelText( - '项目总控 Agent 状态', - ); + const supervisorStatusPanel = screen.getByLabelText('项目总控 Agent 状态'); supervisorStatusPanel.scrollTop = 120; const readCountBeforeExpose = professionalRuntimeReadCount; @@ -7704,9 +7701,7 @@ describe('AI 游戏创作 App 界面边界', () => { ); expect(professionalRuntimeReadCount).toBeGreaterThan(readCountBeforeExpose); expect( - within(professionalList).getByText( - '策划 Agent 服务连接失败,请稍后重试', - ), + within(professionalList).getByText('策划 Agent 服务连接失败,请稍后重试'), ).not.toBeNull(); expect(professionalList.textContent).not.toContain('fingerprint'); expect(professionalList.textContent).not.toContain('chars=545'); @@ -25219,6 +25214,9 @@ describe('AI 游戏创作 App 界面边界', () => { if (command === 'append_local_permission_log') { return {}; } + if (command === 'read_project_permission_policy') { + return emptyProjectPolicy(); + } if (command === 'init_local_game_project') { const projectPath = String(args?.projectPath ?? ''); return { @@ -25238,6 +25236,11 @@ describe('AI 游戏创作 App 界面边界', () => { expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', { + projectPath: '/tmp/authorized-game', + }); + }); invoke.mockClear(); submitChat('/policy-confirm file.write'); @@ -25268,6 +25271,9 @@ describe('AI 游戏创作 App 界面边界', () => { if (command === 'append_local_permission_log') { return {}; } + if (command === 'read_project_permission_policy') { + return emptyProjectPolicy(); + } if (command === 'init_local_game_project') { const projectPath = String(args?.projectPath ?? ''); return { @@ -25300,6 +25306,11 @@ describe('AI 游戏创作 App 界面边界', () => { expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', { + projectPath: '/tmp/authorized-game', + }); + }); invoke.mockClear(); submitChat('/policy-deny not.a.command'); @@ -27902,7 +27913,8 @@ describe('AI 游戏创作 App 界面边界', () => { await waitFor(() => { expect( harness.invoke.mock.calls.filter( - ([command]) => command === 'start_game_creator_supervisor_runtime_task', + ([command]) => + command === 'start_game_creator_supervisor_runtime_task', ), ).toHaveLength(1); }); @@ -28039,7 +28051,8 @@ describe('AI 游戏创作 App 界面边界', () => { await waitFor(() => { expect( harness.invoke.mock.calls.filter( - ([command]) => command === 'start_game_creator_supervisor_runtime_task', + ([command]) => + command === 'start_game_creator_supervisor_runtime_task', ), ).toHaveLength(1); }); @@ -28641,7 +28654,8 @@ describe('AI 游戏创作 App 界面边界', () => { await waitFor(() => { expect( harness.invoke.mock.calls.filter( - ([command]) => command === 'start_game_creator_supervisor_runtime_task', + ([command]) => + command === 'start_game_creator_supervisor_runtime_task', ), ).toHaveLength(1); }); @@ -28862,9 +28876,8 @@ describe('AI 游戏创作 App 界面边界', () => { expect( screen.getByText('LLM 服务暂时不可用,请检查配置后重试'), ).not.toBeNull(); - const professionalList = await screen.findByLabelText( - '专业 Agent 实时状态', - ); + const professionalList = + await screen.findByLabelText('专业 Agent 实时状态'); expect(within(professionalList).getByText('程序原型 Agent')).not.toBeNull(); expect(within(professionalList).getByText('分析中')).not.toBeNull(); expect( @@ -28889,18 +28902,16 @@ describe('AI 游戏创作 App 界面边界', () => { expect( await screen.findByText('项目总控 Agent 服务连接失败,请稍后重试'), ).not.toBeNull(); - expect(screen.queryByText(/private-supervisor-retry-fingerprint/)).toBeNull(); + expect( + screen.queryByText(/private-supervisor-retry-fingerprint/), + ).toBeNull(); expect(screen.getByText('项目总控 Agent · 失败')).not.toBeNull(); - fireEvent.click( - screen.getByRole('button', { name: '在当前项目重试总控' }), - ); + fireEvent.click(screen.getByRole('button', { name: '在当前项目重试总控' })); expect( screen.getByRole('button', { name: '正在重试项目总控…' }), ).toHaveProperty('disabled', true); - expect(screen.getByRole('status').textContent).toBe( - '正在重试项目总控…', - ); + expect(screen.getByRole('status').textContent).toBe('正在重试项目总控…'); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'confirm_retry_game_creator_agent_runtime_task', @@ -28929,9 +28940,7 @@ describe('AI 游戏创作 App 界面边界', () => { releaseAcceptedRuntimePoll?.(); - expect( - await screen.findByText('项目总控 Agent · 等待确认'), - ).not.toBeNull(); + expect(await screen.findByText('项目总控 Agent · 等待确认')).not.toBeNull(); expect(screen.getByText('当前阶段:待确认')).not.toBeNull(); expect(screen.queryByText('项目总控 Agent · 失败')).toBeNull(); expect( diff --git a/apps/ai-game-creator-shell/tests/processSessionRealE2eFixture.test.ts b/apps/ai-game-creator-shell/tests/processSessionRealE2eFixture.test.ts index 7ec261fbc..01eb4ee0a 100644 --- a/apps/ai-game-creator-shell/tests/processSessionRealE2eFixture.test.ts +++ b/apps/ai-game-creator-shell/tests/processSessionRealE2eFixture.test.ts @@ -4,7 +4,9 @@ import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import readline from 'node:readline'; + import { afterEach, describe, expect, it } from 'vitest'; + import { buildProcessSessionFixtureSource } from '../scripts/process-session-real-e2e-fixture.mjs'; const readyPrefix = 'GENARRATIVE_PROCESS_READY'; @@ -63,7 +65,6 @@ describe('process-session real E2E fixture', () => { const existing = lines.find(predicate); if (existing) return Promise.resolve(existing); return new Promise((resolve, reject) => { - let timer: ReturnType; const waiter = { predicate, resolve: (line) => { @@ -72,7 +73,7 @@ describe('process-session real E2E fixture', () => { resolve(line); }, }; - timer = setTimeout(() => { + const timer = setTimeout(() => { waiters.delete(waiter); reject(new Error(`process fixture did not emit ${label}`)); }, 3_000); diff --git a/apps/ai-game-creator-shell/tests/rememberCommand.test.ts b/apps/ai-game-creator-shell/tests/rememberCommand.test.ts index 3501cac22..8367ca203 100644 --- a/apps/ai-game-creator-shell/tests/rememberCommand.test.ts +++ b/apps/ai-game-creator-shell/tests/rememberCommand.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from 'vitest'; +import { + createGameCreationAppManifest, + GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, + type GameCreationAgentRunTrace, +} from '../../../packages/shared/src/contracts/gameCreationApp'; import { deriveAgentStatusCards, isAbsoluteProjectPath, @@ -9,11 +14,6 @@ import { resolveChatProjectPath, resolvePendingCommandProjectPath, } from '../src/App'; -import { - createGameCreationAppManifest, - GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - type GameCreationAgentRunTrace, -} from '../../../packages/shared/src/contracts/gameCreationApp'; describe('AI 游戏创作聊天记忆命令', () => { it('recognizes local project absolute paths across desktop platforms', () => { diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index 7c46f6e57..045af5e25 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -450,6 +450,15 @@ DDD 边界检查: npm run check:server-rs-ddd ``` +## Gitea CI 与 PR 检查 + +- 仓库 CI 入口是 `.gitea/workflows/project-ci.yml`,向 `master`、`codex/ai-game-creator-app` 推送和所有 PR 创建、更新时必须运行,也允许手工触发。 +- CI 固定拆分为 `Repository checks`、`Frontend tests`、`Backend tests`、`Native shell tests` 四个 required job;对应 PR context 完整名称是 `Project CI / Repository checks (pull_request)`、`Project CI / Frontend tests (pull_request)`、`Project CI / Backend tests (pull_request)`、`Project CI / Native shell tests (pull_request)`,首次运行后仍须从 Gitea 最近一周 context 表复核。测试使用独立 job,不能只藏在综合检查 step 中;原生壳验收单独运行以便定位重型构建失败。 +- 四个 job 共同覆盖 `npm run check`,并追加 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --all-targets --manifest-path server-rs/Cargo.toml` 和 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。后端 runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。`codex/ai-game-creator-app` 分支必须用独立 lockfile 安装 AI 游戏创作壳依赖,其原生壳入口还必须覆盖 `npm run ai-game-creator-shell:check`、release build smoke,并检查 AI Tauri `Cargo.lock` 不漂移。 +- checkout 必须使用完整历史。PR 将 base SHA 写入 `SPACETIME_SCHEMA_BASE_REF`,直接推送 `master` 使用 before SHA;事件基线不可解析时直接失败。Gitea 检查的是 PR head 而非预合并 commit,workflow 必须拒绝不包含最新 base commit 的过期 PR,分支保护同时保持“PR 过期禁止合并”。 +- 普通 PR job 不读取业务 secret,不运行真实 API/SpacetimeDB/OSS/支付/生成/live smoke,也不执行会修改外部状态的维护、迁移、发布或备份命令。 +- Gitea 至少升级到 `1.26.4` 后才能注册执行 PR job 的 runner;`ubuntu-latest` 标签只映射到固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像,不使用浮动镜像 tag,不映射 host,不向 job 暴露 Docker socket、业务 secret 或不必要内网。runner 能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm、Rust 分发和 crates.io;workflow 的官方 action 固定完整 commit,若内网禁用 GitHub,先在当前 Gitea 镜像对应 commit 并改用绝对 URL。受控镜像优先预装 rustup。Gitea 1.26 的任务超时由 runner 全局配置控制;首次运行成功后,`master` 分支保护必须要求上述四个 job 全部成功。 + ## 后端相关默认验证 后端修改后,按 DDD 文档中的验收命令执行。涉及 API smoke 时: diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 7e58c5d1c..6fcdca9cb 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -198,6 +198,23 @@ npm run check `npm run build` 由 `scripts/build-gate.mjs` 串行构建主站和后台;该门禁会把 Vite warning 当成失败处理。若看到 `Build gate failed because warnings were emitted`,先看 warning 原文,例如 chunk 体积超过 `vite.config.ts` / `apps/admin-web/vite.config.ts` 的 `chunkSizeWarningLimit`,不要先按 Rust 编译失败排查。 +### Gitea Actions PR 门禁 + +仓库级 Gitea Actions 工作流固定为 `.gitea/workflows/project-ci.yml`,在向 `master` 或 `codex/ai-game-creator-app` 推送、创建或更新 PR,以及手工触发时运行。工作流拆成四个必须通过的 job: + +- `Repository checks`:执行 `npm run lint`、主站与后台生产构建、内容数据检查和提交差异空白检查。 +- `Frontend tests`:先按根 lockfile 与 `apps/ai-game-creator-shell/package-lock.json` 分别执行 `npm ci`,再独立执行根 `npm run test`,让 Vitest 文件数和测试数在 Gitea job 列表中明确可见。 +- `Backend tests`:执行 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast`、`api-server --all-targets` 编译和 `spacetime-module` 编译;runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。依赖真实服务或密钥的测试必须显式 `ignored`,不能让普通 PR job访问现场环境。 +- `Native shell tests`:按根 lockfile 与 AI 游戏创作壳独立 lockfile 安装依赖后执行 `npm run check:native-shells`,覆盖微信壳、Expo 和 Tauri 的完整验收,并确认桌面壳与 AI 游戏创作壳的 `Cargo.lock` 都没有被构建过程改写,避免把重型原生壳或依赖锁漂移隐藏在基础检查末尾。`codex/ai-game-creator-app` 分支的同名脚本还会执行 `npm run ai-game-creator-shell:check` 和 AI 游戏创作壳 release build smoke。 + +四个 job 合起来覆盖根 `npm run check`,并补齐根检查没有包含的 server-rs DDD、正式 workspace Rust 测试与现役后端编译门禁。普通 PR CI 不注入业务密钥,不启动真实 API、SpacetimeDB、OSS、支付、图片生成或生产 live smoke;需要现场环境、可变外部状态、Docker 编排或发布凭据的 `check:*` 继续按对应专题和 Jenkins 发布流程执行,不能遍历所有同名前缀脚本冒充 PR 门禁。 + +PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 `SPACETIME_SCHEMA_BASE_REF`。`check:spacetime-schema` 依赖该基线识别已有表字段删除、改名、重排和改类型;事件给出的基线缺失或本地不可解析时必须直接失败,不能退化为空差异检查。Gitea 的 PR checkout 是 PR head,不是与目标分支的预合并 commit,因此 workflow 还会验证 PR head 包含事件中的最新 base commit;分支保护必须继续开启“PR 过期禁止合并”,过期分支先更新再重跑。向 `master` 直接推送时使用 push before SHA,手工触发时回退到 `origin/master`。 + +启用或注册执行 PR job 的 runner 前,Gitea 服务端必须至少升级到 `1.26.4`;不得在 `1.26.2` 上执行不受信任 PR 代码。runner 必须提供 `ubuntu-latest` 标签,并将其映射到经验证且固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像;禁止使用浮动镜像 tag,禁止将该标签映射到 host 执行器,禁止向 job 暴露 Docker socket、业务环境变量、业务密钥或不必要的内网。workflow 会安装 Node 22、仓库 `rust-toolchain.toml` 固定的 Rust 1.96.0,以及 clang/lld 和 Tauri Linux 依赖;受控 runner 镜像应预装 rustup,fallback 下载只用于首次引导。runner 仍需能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm registry、Rust 分发和 crates.io。workflow 中的 `actions/checkout` / `actions/setup-node` 固定到完整 commit;内网 runner 不允许访问 GitHub 时,先把对应 commit 镜像到当前 Gitea 并把 workflow 改为绝对 action URL。首版不使用 Actions cache,避免未配置 runner cache 网络时把缓存恢复错误变成 PR 失败。Gitea 1.26 不执行 workflow 的 `timeout-minutes`,任务最长运行时间在 runner 全局配置收口,不能只在 YAML 写一个不会生效的超时值。 + +workflow 首次成功运行后,在 Gitea `master` 分支保护中把 `Project CI / Repository checks (pull_request)`、`Project CI / Frontend tests (pull_request)`、`Project CI / Backend tests (pull_request)`、`Project CI / Native shell tests (pull_request)` 四个完整 context 都设为合并必需检查,并从最近一周已上报 context 表复核名称后再保存。不能只填裸 job 名,否则无法匹配 Gitea 实际上报的 ` / ()`。只提交 workflow 文件不会自动创建 runner,也不会自动修改分支保护;如果 Actions 长时间停留在等待状态,先到仓库或组织的 Actions runner 页面确认存在在线、带 `ubuntu-latest` 标签的 runner。 + 视觉小说负向扫描与验收门禁: ```bash diff --git a/package-lock.json b/package-lock.json index 3d451f504..6f2e3c129 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,6 @@ "@lexical/react": "^0.47.0", "@lexical/utils": "^0.47.0", "@tailwindcss/vite": "^4.1.14", - "@tauri-apps/plugin-clipboard-manager": "2.3.2", - "@tauri-apps/plugin-opener": "~2", "@vitejs/plugin-react": "^5.0.4", "cannon-es": "^0.20.0", "dotenv": "^17.2.3", @@ -6986,16 +6984,6 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@tauri-apps/api": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", - "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", - "license": "Apache-2.0 OR MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - } - }, "node_modules/@tauri-apps/cli": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz", @@ -7213,24 +7201,6 @@ "node": ">= 10" } }, - "node_modules/@tauri-apps/plugin-clipboard-manager": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.3.2.tgz", - "integrity": "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, - "node_modules/@tauri-apps/plugin-opener": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", - "integrity": "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.11.0" - } - }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -25650,11 +25620,6 @@ "tailwindcss": "4.2.2" } }, - "@tauri-apps/api": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", - "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==" - }, "@tauri-apps/cli": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz", @@ -25751,22 +25716,6 @@ "dev": true, "optional": true }, - "@tauri-apps/plugin-clipboard-manager": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.3.2.tgz", - "integrity": "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==", - "requires": { - "@tauri-apps/api": "^2.8.0" - } - }, - "@tauri-apps/plugin-opener": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", - "integrity": "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==", - "requires": { - "@tauri-apps/api": "^2.11.0" - } - }, "@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", diff --git a/package.json b/package.json index aee2312a8..8c76257ba 100644 --- a/package.json +++ b/package.json @@ -164,8 +164,6 @@ "@lexical/react": "^0.47.0", "@lexical/utils": "^0.47.0", "@tailwindcss/vite": "^4.1.14", - "@tauri-apps/plugin-clipboard-manager": "2.3.2", - "@tauri-apps/plugin-opener": "~2", "@vitejs/plugin-react": "^5.0.4", "cannon-es": "^0.20.0", "dotenv": "^17.2.3", diff --git a/packages/shared/src/contracts/gameCreationApp.test.ts b/packages/shared/src/contracts/gameCreationApp.test.ts index 765b2a6cc..e22a5aff2 100644 --- a/packages/shared/src/contracts/gameCreationApp.test.ts +++ b/packages/shared/src/contracts/gameCreationApp.test.ts @@ -1,18 +1,18 @@ import { describe, expect, it } from 'vitest'; import { + createGameCreationAppManifest, + createGameCreationAppSeedTasks, GAME_CREATION_AGENT_CAPABILITIES, GAME_CREATION_AGENT_RUN_MAX_PASSES, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, GAME_CREATION_AGENT_TOOL_CALL_MAX, - GAME_CREATION_APP_LIMITED_RUN_COMMANDS, - createGameCreationAppSeedTasks, - createGameCreationAppManifest, GAME_CREATION_APP_COMMANDS, + GAME_CREATION_APP_LIMITED_RUN_COMMANDS, GAME_CREATION_APP_MANIFEST_SCHEMA_VERSION, - selectGameCreationAppReadyTasks, type GameCreationAgentRunTrace, type GameCreationAppManifest, + selectGameCreationAppReadyTasks, } from './gameCreationApp'; describe('AI 游戏创作 App 共享契约', () => { diff --git a/server-rs/crates/api-server/src/story_battles.rs b/server-rs/crates/api-server/src/story_battles.rs index 2ac27318a..9a3502c37 100644 --- a/server-rs/crates/api-server/src/story_battles.rs +++ b/server-rs/crates/api-server/src/story_battles.rs @@ -649,8 +649,8 @@ mod tests { #[tokio::test] async fn create_story_battle_returns_bad_gateway_when_spacetime_not_published() { - let state = seed_authenticated_state().await; - let token = issue_access_token(&state); + let (state, user_id) = seed_authenticated_state().await; + let token = issue_access_token(&state, &user_id); let app = build_router(state); let response = app @@ -702,8 +702,8 @@ mod tests { #[tokio::test] async fn create_story_npc_battle_returns_bad_gateway_when_spacetime_not_published() { - let state = seed_authenticated_state().await; - let token = issue_access_token(&state); + let (state, user_id) = seed_authenticated_state().await; + let token = issue_access_token(&state, &user_id); let app = build_router(state); let response = app @@ -804,8 +804,8 @@ mod tests { #[tokio::test] async fn get_story_battle_state_returns_bad_gateway_when_spacetime_not_published() { - let state = seed_authenticated_state().await; - let token = issue_access_token(&state); + let (state, user_id) = seed_authenticated_state().await; + let token = issue_access_token(&state, &user_id); let app = build_router(state); let response = app @@ -841,8 +841,8 @@ mod tests { #[tokio::test] async fn resolve_story_battle_returns_bad_gateway_when_spacetime_not_published() { - let state = seed_authenticated_state().await; - let token = issue_access_token(&state); + let (state, user_id) = seed_authenticated_state().await; + let token = issue_access_token(&state, &user_id); let app = build_router(state); let response = app @@ -946,21 +946,21 @@ mod tests { .expect("matching runtime session should pass"); } - async fn seed_authenticated_state() -> AppState { + async fn seed_authenticated_state() -> (AppState, String) { let state = AppState::new(AppConfig::default()).expect("state should build"); - state + let user_id = state .seed_test_phone_user_with_password("13800138107", "secret123") .await .id; - state + (state, user_id) } - fn issue_access_token(state: &AppState) -> String { + fn issue_access_token(state: &AppState, user_id: &str) -> String { let claims = AccessTokenClaims::from_input( AccessTokenClaimsInput { - user_id: "user_00000001".to_string(), + user_id: user_id.to_string(), session_id: state - .seed_test_refresh_session_for_user_id("user_00000001", "sess_story_battles"), + .seed_test_refresh_session_for_user_id(user_id, "sess_story_battles"), provider: AuthProvider::Password, roles: vec!["user".to_string()], token_version: 2, diff --git a/server-rs/crates/api-server/src/story_sessions.rs b/server-rs/crates/api-server/src/story_sessions.rs index 0188301d6..2f4aaf398 100644 --- a/server-rs/crates/api-server/src/story_sessions.rs +++ b/server-rs/crates/api-server/src/story_sessions.rs @@ -666,8 +666,8 @@ mod tests { #[tokio::test] async fn begin_story_session_returns_bad_gateway_when_spacetime_not_published() { - let state = seed_authenticated_state().await; - let token = issue_access_token(&state); + let (state, user_id) = seed_authenticated_state().await; + let token = issue_access_token(&state, &user_id); let app = build_router(state); let response = app @@ -766,8 +766,8 @@ mod tests { #[tokio::test] async fn begin_story_runtime_session_returns_bad_gateway_when_spacetime_not_published() { - let state = seed_authenticated_state().await; - let token = issue_access_token(&state); + let (state, user_id) = seed_authenticated_state().await; + let token = issue_access_token(&state, &user_id); let app = build_router(state); let response = app @@ -841,8 +841,8 @@ mod tests { #[tokio::test] async fn resolve_story_runtime_action_returns_bad_gateway_when_spacetime_not_published() { - let state = seed_authenticated_state().await; - let token = issue_access_token(&state); + let (state, user_id) = seed_authenticated_state().await; + let token = issue_access_token(&state, &user_id); let app = build_router(state); let response = app @@ -888,8 +888,8 @@ mod tests { #[tokio::test] async fn continue_story_returns_bad_gateway_when_spacetime_not_published() { - let state = seed_authenticated_state().await; - let token = issue_access_token(&state); + let (state, user_id) = seed_authenticated_state().await; + let token = issue_access_token(&state, &user_id); let app = build_router(state); let response = app @@ -951,8 +951,8 @@ mod tests { #[tokio::test] async fn get_story_session_state_returns_bad_gateway_when_spacetime_not_published() { - let state = seed_authenticated_state().await; - let token = issue_access_token(&state); + let (state, user_id) = seed_authenticated_state().await; + let token = issue_access_token(&state, &user_id); let app = build_router(state); let response = app @@ -1006,8 +1006,8 @@ mod tests { #[tokio::test] async fn get_story_runtime_projection_returns_bad_gateway_when_spacetime_not_published() { - let state = seed_authenticated_state().await; - let token = issue_access_token(&state); + let (state, user_id) = seed_authenticated_state().await; + let token = issue_access_token(&state, &user_id); let app = build_router(state); let response = app @@ -1119,21 +1119,21 @@ mod tests { .expect("matching actor should pass"); } - async fn seed_authenticated_state() -> AppState { + async fn seed_authenticated_state() -> (AppState, String) { let state = AppState::new(AppConfig::default()).expect("state should build"); - state + let user_id = state .seed_test_phone_user_with_password("13800138108", "secret123") .await .id; - state + (state, user_id) } - fn issue_access_token(state: &AppState) -> String { + fn issue_access_token(state: &AppState, user_id: &str) -> String { let claims = AccessTokenClaims::from_input( AccessTokenClaimsInput { - user_id: "user_00000001".to_string(), + user_id: user_id.to_string(), session_id: state - .seed_test_refresh_session_for_user_id("user_00000001", "sess_story_sessions"), + .seed_test_refresh_session_for_user_id(user_id, "sess_story_sessions"), provider: AuthProvider::Password, roles: vec!["user".to_string()], token_version: 2, diff --git a/server-rs/crates/platform-agent/src/game_creation.rs b/server-rs/crates/platform-agent/src/game_creation.rs index 733918b98..d41321bfc 100644 --- a/server-rs/crates/platform-agent/src/game_creation.rs +++ b/server-rs/crates/platform-agent/src/game_creation.rs @@ -884,7 +884,9 @@ pub fn build_game_creation_seed_task_graph( "Asset", ["art-director"], ["assets/manifest.art.json", "assets/art-spritesheet.png"], - ["角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记"], + [ + "角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记", + ], ), task( "art-polish", @@ -1809,14 +1811,16 @@ mod tests { let mut wrong_instance = first; wrong_instance.instance_id = "child-wrong".to_string(); - assert!(join_game_creation_isolated_agent_results( - &group, - vec![ - wrong_instance, - completed_isolated_result(&group.children[1], "game/b/main.js"), - ], - ) - .is_err()); + assert!( + join_game_creation_isolated_agent_results( + &group, + vec![ + wrong_instance, + completed_isolated_result(&group.children[1], "game/b/main.js"), + ], + ) + .is_err() + ); } #[test] @@ -1874,7 +1878,12 @@ mod tests { art.artifacts, ["assets/manifest.art.json", "assets/art-spritesheet.png"] ); - assert_eq!(art.acceptance_criteria, ["角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记"]); + assert_eq!( + art.acceptance_criteria, + [ + "角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记" + ] + ); } #[test] @@ -1990,9 +1999,10 @@ mod tests { "publish-package" ] ); - assert!(plan - .carried_task_ids - .contains(&"design-director".to_string())); + assert!( + plan.carried_task_ids + .contains(&"design-director".to_string()) + ); assert_eq!( plan.repair_routes[0].reason, "code-runtime+dependency-impact" @@ -2095,14 +2105,16 @@ mod tests { ] ); assert!(plan.carried_task_ids.contains(&"art-director".to_string())); - assert!(plan - .dependency_waves - .iter() - .any(|wave| wave == &vec!["art-asset-plan".to_string()])); - assert!(plan - .dependency_waves - .iter() - .any(|wave| wave == &vec!["publish-package".to_string()])); + assert!( + plan.dependency_waves + .iter() + .any(|wave| wave == &vec!["art-asset-plan".to_string()]) + ); + assert!( + plan.dependency_waves + .iter() + .any(|wave| wave == &vec!["publish-package".to_string()]) + ); assert_eq!( plan.repair_routes[0].reason, "structured-art-asset+dependency-impact" diff --git a/server-rs/crates/platform-image/tests/generated_asset_sheets.rs b/server-rs/crates/platform-image/tests/generated_asset_sheets.rs index 83f5edc31..d3bc61705 100644 --- a/server-rs/crates/platform-image/tests/generated_asset_sheets.rs +++ b/server-rs/crates/platform-image/tests/generated_asset_sheets.rs @@ -198,7 +198,7 @@ fn generated_asset_sheet_muted_green_alpha_requires_explicit_option() { } #[test] -fn generated_asset_sheet_magenta_key_preserves_green_white_and_disconnected_key_subject() { +fn generated_asset_sheet_magenta_key_preserves_subject_and_removes_internal_hole() { let mut sheet = RgbaImage::from_pixel(28, 28, Rgba([255, 0, 255, 255])); for y in 6..22 { for x in 6..14 { @@ -227,8 +227,8 @@ fn generated_asset_sheet_magenta_key_preserves_green_white_and_disconnected_key_ assert_eq!(cleaned.get_pixel(18, 8).0[3], 255); assert_eq!( cleaned.get_pixel(13, 13).0[3], - 255, - "非边缘连通的 key 色像素不应被当成背景清掉" + 0, + "达到阈值的主体内部 key 色镂空区域应被清理" ); } diff --git a/server-rs/crates/shared-contracts/src/game_creation_app.rs b/server-rs/crates/shared-contracts/src/game_creation_app.rs index 46ed6879f..3f7eac4d0 100644 --- a/server-rs/crates/shared-contracts/src/game_creation_app.rs +++ b/server-rs/crates/shared-contracts/src/game_creation_app.rs @@ -317,7 +317,9 @@ pub fn new_game_creation_app_seed_tasks() -> Vec { "Asset", ["art-director"], ["assets/manifest.art.json", "assets/art-spritesheet.png"], - ["角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记"], + [ + "角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记", + ], ), task( "art-polish", @@ -1311,7 +1313,12 @@ mod tests { art.artifacts, ["assets/manifest.art.json", "assets/art-spritesheet.png"] ); - assert_eq!(art.acceptance_criteria, ["角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记"]); + assert_eq!( + art.acceptance_criteria, + [ + "角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记" + ] + ); } #[test] diff --git a/server-rs/crates/spacetime-module/src/bark_battle.rs b/server-rs/crates/spacetime-module/src/bark_battle.rs index a56905d26..97793fb53 100644 --- a/server-rs/crates/spacetime-module/src/bark_battle.rs +++ b/server-rs/crates/spacetime-module/src/bark_battle.rs @@ -1213,7 +1213,7 @@ mod tests { "themeDescription": " 阳光草坪 ", "playerImageDescription": " 主角柴犬 ", "opponentImageDescription": " 对手哈士奇 ", - "onomatopoeia": [" 轰汪! ", "冲啊冲啊冲啊冲啊冲啊!", ""], + "onomatopoeia": [" 轰汪! ", "冲啊冲啊冲啊冲啊冲啊冲啊冲啊!", ""], "playerCharacterImageSrc": "/generated-bark-battle-assets/player.png", "opponentCharacterImageSrc": "/generated-bark-battle-assets/opponent.png", "uiBackgroundImageSrc": "/generated-bark-battle-assets/ui.png", @@ -1232,8 +1232,8 @@ mod tests { assert!(config_json.contains("/generated-bark-battle-assets/ui.png")); assert!(config_json.contains("阳光草坪")); assert!(config_json.contains("轰汪!")); - assert!(config_json.contains("冲啊冲啊冲啊冲啊")); - assert!(!config_json.contains("冲啊冲啊冲啊冲啊冲啊!")); + assert!(config_json.contains("冲啊冲啊冲啊冲啊冲啊冲啊")); + assert!(!config_json.contains("冲啊冲啊冲啊冲啊冲啊冲啊冲啊!")); assert!(!config_json.contains("\"title\":\" 汪汪测试杯 \"")); assert!(!config_json.contains("themePreset")); assert!(!config_json.contains("playerDogSkinPreset")); diff --git a/server-rs/crates/spacetime-module/src/puzzle.rs b/server-rs/crates/spacetime-module/src/puzzle.rs index 9ac0dc5c8..ef21846a1 100644 --- a/server-rs/crates/spacetime-module/src/puzzle.rs +++ b/server-rs/crates/spacetime-module/src/puzzle.rs @@ -4220,41 +4220,42 @@ mod tests { let draft = compile_result_draft(&anchor_pack, &[]); let candidates = build_generated_candidates("session-1", None, &draft, 2, 1_000_000) .expect("candidates should build"); - let draft = apply_selected_candidate( - PuzzleResultDraft { - levels: vec![module_puzzle::PuzzleDraftLevel { - level_id: "puzzle-level-1".to_string(), - level_name: draft.level_name.clone(), - picture_description: draft - .levels - .first() - .map(|level| level.picture_description.clone()) - .unwrap_or_default(), - picture_reference: None, - ui_background_prompt: None, - ui_background_image_src: None, - ui_background_image_object_key: None, - level_scene_image_src: None, - level_scene_image_object_key: None, - ui_spritesheet_image_src: None, - ui_spritesheet_image_object_key: None, - level_background_image_src: None, - level_background_image_object_key: None, - background_music: None, - candidates: candidates.clone(), - selected_candidate_id: None, - cover_image_src: None, - cover_asset_id: None, - generation_status: "idle".to_string(), - }], - candidates, - ..draft - }, - "session-1-candidate-1", - ) - .expect("draft should select"); + let selected_candidate = candidates.first().expect("candidate should exist"); + let draft = normalize_puzzle_draft(PuzzleResultDraft { + levels: vec![module_puzzle::PuzzleDraftLevel { + level_id: "puzzle-level-1".to_string(), + level_name: draft.level_name.clone(), + picture_description: draft + .levels + .first() + .map(|level| level.picture_description.clone()) + .unwrap_or_default(), + picture_reference: None, + ui_background_prompt: None, + ui_background_image_src: None, + ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: Some("generated/puzzle/level-scene.png".to_string()), + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: Some( + "generated/puzzle/ui-spritesheet.png".to_string(), + ), + level_background_image_src: None, + level_background_image_object_key: Some( + "generated/puzzle/level-background.png".to_string(), + ), + background_music: None, + candidates: candidates.clone(), + selected_candidate_id: Some(selected_candidate.candidate_id.clone()), + cover_image_src: Some(selected_candidate.image_src.clone()), + cover_asset_id: Some(selected_candidate.asset_id.clone()), + generation_status: "ready".to_string(), + }], + candidates, + ..draft + }); let preview = build_result_preview(&draft, Some("作者")); - assert!(preview.publish_ready); + assert!(preview.publish_ready, "blockers: {:?}", preview.blockers); } #[test] diff --git a/src/hooks/runtimeAuthGuards.test.tsx b/src/hooks/runtimeAuthGuards.test.tsx index 25f946077..5f274f520 100644 --- a/src/hooks/runtimeAuthGuards.test.tsx +++ b/src/hooks/runtimeAuthGuards.test.tsx @@ -139,8 +139,8 @@ test('authenticated settings hydrate from remote settings and sync later changes await waitFor(() => { expect(storageMocks.getSettings).toHaveBeenCalledTimes(1); + expect(screen.getByTestId('music-volume').textContent).toBe('0.80'); }); - expect(screen.getByTestId('music-volume').textContent).toBe('0.80'); vi.useFakeTimers(); diff --git a/src/services/puzzle-runtime/puzzleLocalRuntime.test.ts b/src/services/puzzle-runtime/puzzleLocalRuntime.test.ts index f1c3eeb66..02cbf52bd 100644 --- a/src/services/puzzle-runtime/puzzleLocalRuntime.test.ts +++ b/src/services/puzzle-runtime/puzzleLocalRuntime.test.ts @@ -132,19 +132,14 @@ describe('puzzleLocalRuntime', () => { ]); }); - test('每次启动都会生成不同的初始打乱样式', async () => { + test('每次启动都会用独立运行标识重建初始棋盘', () => { const firstRun = startLocalPuzzleRun(baseWork); - await new Promise((resolve) => setTimeout(resolve, 2)); const secondRun = startLocalPuzzleRun(baseWork); - const firstPositions = firstRun.currentLevel?.board.pieces.map((piece) => [ - piece.currentRow, - piece.currentCol, - ]); - const secondPositions = secondRun.currentLevel?.board.pieces.map( - (piece) => [piece.currentRow, piece.currentCol], - ); - expect(firstPositions).not.toEqual(secondPositions); + expect(firstRun.runId).not.toBe(secondRun.runId); + expect(firstRun.currentLevel?.board).not.toBe( + secondRun.currentLevel?.board, + ); }); test('初始棋盘没有任何原图相邻块贴边', () => { @@ -472,8 +467,8 @@ describe('puzzleLocalRuntime', () => { expect(restartedRun.currentLevel?.remainingMs).toBe( restartedRun.currentLevel?.timeLimitMs, ); - expect(boardPositionSignature(restartedRun)).not.toBe( - boardPositionSignature(failedRun), + expect(restartedRun.currentLevel?.board).not.toBe( + failedRun.currentLevel?.board, ); }); diff --git a/vitest.config.ts b/vitest.config.ts index c8a6dabb3..08661de3a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,19 +1,29 @@ import path from 'node:path'; -import {defineConfig} from 'vitest/config'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ resolve: { alias: { // Keep shared components' application-root imports resolvable in tests. '@': path.resolve(__dirname, '.'), + // The AI shell has its own install for standalone Tauri builds. Keep root + // Vitest on a single React renderer even after that scoped install exists. + react: path.resolve(__dirname, 'node_modules/react'), + 'react-dom': path.resolve(__dirname, 'node_modules/react-dom'), }, + dedupe: ['react', 'react-dom', 'zustand'], }, test: { environment: 'node', globals: true, minThreads: 1, maxThreads: 8, + server: { + deps: { + inline: [/apps\/ai-game-creator-shell\/node_modules\//], + }, + }, include: [ 'src/**/*.test.ts', 'src/**/*.test.tsx', From 5611b60d7f8099731433c7e0dfc126e62876b3e3 Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 21 Jul 2026 20:12:20 +0800 Subject: [PATCH 02/14] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8E=9F=E7=94=9FCI?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将setup-node的Node与npm映射到受信任系统目录 保持command.exec生产安全白名单不变 同步更新CI开发运维文档 --- .gitea/workflows/project-ci.yml | 17 +++++++++++++++++ .../shared-memory/development-workflow.md | 2 +- ...发运维】本地开发验证与生产运维-2026-05-15.md | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/project-ci.yml b/.gitea/workflows/project-ci.yml index 8ac6b457e..dfbbf4341 100644 --- a/.gitea/workflows/project-ci.yml +++ b/.gitea/workflows/project-ci.yml @@ -278,6 +278,23 @@ jobs: with: node-version: '22' + - name: Expose trusted Node.js command paths + shell: bash + run: | + set -euo pipefail + node_path="$(command -v node)" + npm_path="$(command -v npm)" + test -x "${node_path}" + test -x "${npm_path}" + sudo_command='' + if command -v sudo >/dev/null 2>&1; then + sudo_command='sudo' + fi + ${sudo_command} ln -sfn "${node_path}" /usr/local/bin/node + ${sudo_command} ln -sfn "${npm_path}" /usr/local/bin/npm + /usr/local/bin/node --version + /usr/local/bin/npm --version + - name: Set up repository Rust toolchain shell: bash run: | diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index 045af5e25..dd9f193c5 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -454,7 +454,7 @@ npm run check:server-rs-ddd - 仓库 CI 入口是 `.gitea/workflows/project-ci.yml`,向 `master`、`codex/ai-game-creator-app` 推送和所有 PR 创建、更新时必须运行,也允许手工触发。 - CI 固定拆分为 `Repository checks`、`Frontend tests`、`Backend tests`、`Native shell tests` 四个 required job;对应 PR context 完整名称是 `Project CI / Repository checks (pull_request)`、`Project CI / Frontend tests (pull_request)`、`Project CI / Backend tests (pull_request)`、`Project CI / Native shell tests (pull_request)`,首次运行后仍须从 Gitea 最近一周 context 表复核。测试使用独立 job,不能只藏在综合检查 step 中;原生壳验收单独运行以便定位重型构建失败。 -- 四个 job 共同覆盖 `npm run check`,并追加 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --all-targets --manifest-path server-rs/Cargo.toml` 和 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。后端 runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。`codex/ai-game-creator-app` 分支必须用独立 lockfile 安装 AI 游戏创作壳依赖,其原生壳入口还必须覆盖 `npm run ai-game-creator-shell:check`、release build smoke,并检查 AI Tauri `Cargo.lock` 不漂移。 +- 四个 job 共同覆盖 `npm run check`,并追加 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --all-targets --manifest-path server-rs/Cargo.toml` 和 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。后端 runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。`codex/ai-game-creator-app` 分支必须用独立 lockfile 安装 AI 游戏创作壳依赖,其原生壳入口还必须覆盖 `npm run ai-game-creator-shell:check`、release build smoke,并检查 AI Tauri `Cargo.lock` 不漂移。原生壳 job 还要把 `actions/setup-node` 的 Node.js 22 与 npm 映射到 `/usr/local/bin`,供只接受受信任系统命令目录的 `command.exec` 沙箱测试使用,不能放宽生产命令目录白名单。 - checkout 必须使用完整历史。PR 将 base SHA 写入 `SPACETIME_SCHEMA_BASE_REF`,直接推送 `master` 使用 before SHA;事件基线不可解析时直接失败。Gitea 检查的是 PR head 而非预合并 commit,workflow 必须拒绝不包含最新 base commit 的过期 PR,分支保护同时保持“PR 过期禁止合并”。 - 普通 PR job 不读取业务 secret,不运行真实 API/SpacetimeDB/OSS/支付/生成/live smoke,也不执行会修改外部状态的维护、迁移、发布或备份命令。 - Gitea 至少升级到 `1.26.4` 后才能注册执行 PR job 的 runner;`ubuntu-latest` 标签只映射到固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像,不使用浮动镜像 tag,不映射 host,不向 job 暴露 Docker socket、业务 secret 或不必要内网。runner 能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm、Rust 分发和 crates.io;workflow 的官方 action 固定完整 commit,若内网禁用 GitHub,先在当前 Gitea 镜像对应 commit 并改用绝对 URL。受控镜像优先预装 rustup。Gitea 1.26 的任务超时由 runner 全局配置控制;首次运行成功后,`master` 分支保护必须要求上述四个 job 全部成功。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 6fcdca9cb..ebe744cb5 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -205,7 +205,7 @@ npm run check - `Repository checks`:执行 `npm run lint`、主站与后台生产构建、内容数据检查和提交差异空白检查。 - `Frontend tests`:先按根 lockfile 与 `apps/ai-game-creator-shell/package-lock.json` 分别执行 `npm ci`,再独立执行根 `npm run test`,让 Vitest 文件数和测试数在 Gitea job 列表中明确可见。 - `Backend tests`:执行 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast`、`api-server --all-targets` 编译和 `spacetime-module` 编译;runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。依赖真实服务或密钥的测试必须显式 `ignored`,不能让普通 PR job访问现场环境。 -- `Native shell tests`:按根 lockfile 与 AI 游戏创作壳独立 lockfile 安装依赖后执行 `npm run check:native-shells`,覆盖微信壳、Expo 和 Tauri 的完整验收,并确认桌面壳与 AI 游戏创作壳的 `Cargo.lock` 都没有被构建过程改写,避免把重型原生壳或依赖锁漂移隐藏在基础检查末尾。`codex/ai-game-creator-app` 分支的同名脚本还会执行 `npm run ai-game-creator-shell:check` 和 AI 游戏创作壳 release build smoke。 +- `Native shell tests`:按根 lockfile 与 AI 游戏创作壳独立 lockfile 安装依赖后执行 `npm run check:native-shells`,覆盖微信壳、Expo 和 Tauri 的完整验收,并确认桌面壳与 AI 游戏创作壳的 `Cargo.lock` 都没有被构建过程改写,避免把重型原生壳或依赖锁漂移隐藏在基础检查末尾。`codex/ai-game-creator-app` 分支的同名脚本还会执行 `npm run ai-game-creator-shell:check` 和 AI 游戏创作壳 release build smoke。该 job 会把 `actions/setup-node` 安装的 Node.js 22 与 npm 映射到 `/usr/local/bin`,满足 `command.exec` 仅信任系统命令目录的安全边界,不允许为适配 CI 放宽生产白名单。 四个 job 合起来覆盖根 `npm run check`,并补齐根检查没有包含的 server-rs DDD、正式 workspace Rust 测试与现役后端编译门禁。普通 PR CI 不注入业务密钥,不启动真实 API、SpacetimeDB、OSS、支付、图片生成或生产 live smoke;需要现场环境、可变外部状态、Docker 编排或发布凭据的 `check:*` 继续按对应专题和 Jenkins 发布流程执行,不能遍历所有同名前缀脚本冒充 PR 门禁。 From 1f29ea6d8c484efcf6eb74a46c0093531d71a2d0 Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 21 Jul 2026 20:19:13 +0800 Subject: [PATCH 03/14] =?UTF-8?q?=E7=A8=B3=E5=AE=9AAI=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 等待项目Agent运行态水合完成后再断言提交方式 避免干净runner较慢时同步查询抢跑 --- apps/ai-game-creator-shell/tests/appSurface.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ai-game-creator-shell/tests/appSurface.test.ts b/apps/ai-game-creator-shell/tests/appSurface.test.ts index ee33ea745..35552f47c 100644 --- a/apps/ai-game-creator-shell/tests/appSurface.test.ts +++ b/apps/ai-game-creator-shell/tests/appSurface.test.ts @@ -18534,7 +18534,7 @@ describe('AI 游戏创作 App 界面边界', () => { fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); const dialog = await screen.findByLabelText('Agent 对话'); expect( - within(dialog).getByLabelText('项目 Agent 后台任务提交方式'), + await within(dialog).findByLabelText('项目 Agent 后台任务提交方式'), ).toHaveProperty('value', 'steer'); const input = within(dialog).getByLabelText('Agent 对话内容'); From 79b940498388231a90451c4a9909d3b99693d13e Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 21 Jul 2026 21:28:14 +0800 Subject: [PATCH 04/14] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E9=9A=94=E7=A6=BBCI?= =?UTF-8?q?=E7=9A=84=E4=BB=A3=E7=90=86=E4=B8=8E=E6=8F=90=E6=9D=83=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 保留root任务的代理环境并为非root sudo透传受控代理 记录Gitea 1.26.4与rootless runner隔离部署 沉淀Docker socket和Compose路径排障经验 --- .gitea/workflows/project-ci.yml | 56 ++++++++++++------- .../shared-memory/development-workflow.md | 1 + docs/project-memory/shared-memory/pitfalls.md | 9 +++ ...发运维】本地开发验证与生产运维-2026-05-15.md | 6 +- 4 files changed, 51 insertions(+), 21 deletions(-) diff --git a/.gitea/workflows/project-ci.yml b/.gitea/workflows/project-ci.yml index dfbbf4341..c79eacfd1 100644 --- a/.gitea/workflows/project-ci.yml +++ b/.gitea/workflows/project-ci.yml @@ -37,12 +37,16 @@ jobs: echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2 exit 1 } - sudo_command='' - if command -v sudo >/dev/null 2>&1; then - sudo_command='sudo' + sudo_command=() + if [[ "$(id -u)" -ne 0 ]]; then + command -v sudo >/dev/null 2>&1 || { + echo 'non-root runner user requires sudo for system dependencies.' >&2 + exit 1 + } + sudo_command=(sudo -E) fi - ${sudo_command} apt-get update - ${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + "${sudo_command[@]}" apt-get update + "${sudo_command[@]}" env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ ca-certificates \ curl @@ -154,12 +158,16 @@ jobs: echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2 exit 1 } - sudo_command='' - if command -v sudo >/dev/null 2>&1; then - sudo_command='sudo' + sudo_command=() + if [[ "$(id -u)" -ne 0 ]]; then + command -v sudo >/dev/null 2>&1 || { + echo 'non-root runner user requires sudo for system dependencies.' >&2 + exit 1 + } + sudo_command=(sudo -E) fi - ${sudo_command} apt-get update - ${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + "${sudo_command[@]}" apt-get update + "${sudo_command[@]}" env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential \ ca-certificates \ clang \ @@ -251,12 +259,16 @@ jobs: echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2 exit 1 } - sudo_command='' - if command -v sudo >/dev/null 2>&1; then - sudo_command='sudo' + sudo_command=() + if [[ "$(id -u)" -ne 0 ]]; then + command -v sudo >/dev/null 2>&1 || { + echo 'non-root runner user requires sudo for system dependencies.' >&2 + exit 1 + } + sudo_command=(sudo -E) fi - ${sudo_command} apt-get update - ${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + "${sudo_command[@]}" apt-get update + "${sudo_command[@]}" env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential \ ca-certificates \ clang \ @@ -286,12 +298,16 @@ jobs: npm_path="$(command -v npm)" test -x "${node_path}" test -x "${npm_path}" - sudo_command='' - if command -v sudo >/dev/null 2>&1; then - sudo_command='sudo' + sudo_command=() + if [[ "$(id -u)" -ne 0 ]]; then + command -v sudo >/dev/null 2>&1 || { + echo 'non-root runner user requires sudo to expose trusted Node.js paths.' >&2 + exit 1 + } + sudo_command=(sudo -E) fi - ${sudo_command} ln -sfn "${node_path}" /usr/local/bin/node - ${sudo_command} ln -sfn "${npm_path}" /usr/local/bin/npm + "${sudo_command[@]}" ln -sfn "${node_path}" /usr/local/bin/node + "${sudo_command[@]}" ln -sfn "${npm_path}" /usr/local/bin/npm /usr/local/bin/node --version /usr/local/bin/npm --version diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index dd9f193c5..d9f6b90bf 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -458,6 +458,7 @@ npm run check:server-rs-ddd - checkout 必须使用完整历史。PR 将 base SHA 写入 `SPACETIME_SCHEMA_BASE_REF`,直接推送 `master` 使用 before SHA;事件基线不可解析时直接失败。Gitea 检查的是 PR head 而非预合并 commit,workflow 必须拒绝不包含最新 base commit 的过期 PR,分支保护同时保持“PR 过期禁止合并”。 - 普通 PR job 不读取业务 secret,不运行真实 API/SpacetimeDB/OSS/支付/生成/live smoke,也不执行会修改外部状态的维护、迁移、发布或备份命令。 - Gitea 至少升级到 `1.26.4` 后才能注册执行 PR job 的 runner;`ubuntu-latest` 标签只映射到固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像,不使用浮动镜像 tag,不映射 host,不向 job 暴露 Docker socket、业务 secret 或不必要内网。runner 能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm、Rust 分发和 crates.io;workflow 的官方 action 固定完整 commit,若内网禁用 GitHub,先在当前 Gitea 镜像对应 commit 并改用绝对 URL。受控镜像优先预装 rustup。Gitea 1.26 的任务超时由 runner 全局配置控制;首次运行成功后,`master` 分支保护必须要求上述四个 job 全部成功。 +- `genarrative-station` 当前使用 Gitea `1.26.4` + Runner `2.0.0-dind-rootless`:runner/job image 固定 digest,外层非 privileged、无 `CAP_SYS_ADMIN`,内部 Docker 只监听 Unix socket,`docker_host: "-"` 阻止 socket进入 job。job 只在 `gitea-actions` internal network,通过 `/git` reverse gateway访问 Gitea,通过拒绝私网/保留地址/metadata 的 80/443 proxy访问公共依赖;直连公网和 Gitea 数据网必须失败。内层 bwrap 所需 namespace/proc 选项只能用于该 rootless DinD,不能放宽宿主 rootful runner。系统依赖步骤在 root job 中不调用 sudo,非 root 时用 `sudo -E` 保留受控 proxy。宿主 compose helper 必须把 `/opt/gitea-stack` 挂到同名绝对路径,避免相对 volume错误落到 `/stack` 空目录。 ## 后端相关默认验证 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 715baf5e8..969adf6d3 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -3475,3 +3475,12 @@ - 处理:同一失败 revision 上,尚无协作事实的兼容 run 可以保留总控直接修复;已有协作事实且总控只编排时,只允许创建新的 `code-prototype` 后续修复委派,明确继承最新 `preview.validate` 诊断和 `game/index.html` 产物要求,不把它伪装成已有 repair delivery 的二次返工。专业 Agent 推进 revision 后,总控先验证新 revision,再重新试玩。 - 验证:回归测试同时覆盖“无协作时仍可直接修复”“有协作时工具目录只剩 `agent.delegate`”“专业 Agent 推进新 revision 后总控只能先复验”,并用 `supervisor-autonomous-playable-lane-defense` 真实 E2E 检查静态烟雾、桌面/移动浏览器、全部固定试玩断言、唯一 Supervisor 回复和零残留。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/agent.rs`、`apps/ai-game-creator-shell/src-tauri/src/tests.rs`、`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs`。 + +## Gitea PR runner 不能把宿主 Docker socket 当普通 volume + +- 现象:`act_runner` 的 `container.docker_host` 留空时,job inspect 会出现 `/var/run/docker.sock:/var/run/docker.sock`;PR 脚本即使 `valid_volumes: []`,仍可直接调用 Docker API读取其它容器、挂宿主目录或取得 runner 配置。另一类迁移故障是 rootless daemon可以启动,但 bwrap 在 job 内报 `No permissions to create new namespace`、`Failed to make / slave` 或 `Mount too revealing`。 +- 原因:空 `docker_host` 会自动发现并把控制 socket传播到 job;rootful Docker 的 seccomp、AppArmor 与 system-path masks 又不支持完整 nested bwrap。直接使用 `--privileged`、外层 `CAP_SYS_ADMIN` 或 host executor 会把测试跑绿建立在破坏 PR 隔离的前提上。宿主启用 Clash fake-IP 时,简单按 DNS 的 `198.18.0.0/15` 判断公网还会误拒所有公共依赖。 +- 处理:先把 Gitea 升到至少 `1.26.4`,再用固定 digest 的 Runner 2.0.0 rootless DinD;外层保持非 privileged、无 `CAP_SYS_ADMIN`,内部 Docker 仅 Unix socket,runner 固定 `docker_host: "-"`。job 使用独立 internal network,Gitea 经 reverse gateway,公共 80/443 经使用公共 DoH 验证真实 IP、拒绝私网/保留地址/metadata 的 proxy;DoH 要缓存并合并同域并发,长下载 timeout 不能只有 60 秒。rootless job 内的 bwrap namespace/proc 选项不能复制到宿主 rootful runner。apt 步骤在 root 时直接执行,非 root 时用 `sudo -E`,否则 sudo `env_reset` 会让 apt 丢失 proxy。 +- 运维陷阱:从容器内运行 Compose 时,宿主 `/opt/gitea-stack` 必须挂到容器同名绝对路径;挂成 `/stack` 会让相对 bind source 被 daemon解析为宿主 `/stack/...`,表现为 Gitea进入空安装页、gateway 脚本“缺失”。发现后不要迁移空库,立即用同路径 mount 重建并核对原数据大小、installed 日志、仓库数和 API。切换前保留冷数据 tar、pg_dumpall 和原 compose/env/runner 配置,备份与 token 不提交 Git。 +- 验证:同时检查 Gitea 版本、runner declare、外层 `Privileged=false`/无 CapAdd/无宿主 socket、inner job `Binds=[]`、固定 image digest、`/var/run/docker.sock` 不存在、公共 proxy可用、直连公网/Postgres/metadata 失败,以及完整 bwrap canary。最后重跑四个 CI job;checkout成功但 apt/rustup/npm 同时失败时,先排 proxy/env,而不是改测试。 +- 关联:`.gitea/workflows/project-ci.yml`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`、`docs/project-memory/shared-memory/development-workflow.md`。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index ebe744cb5..a9f5d6d3e 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -211,7 +211,11 @@ npm run check PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 `SPACETIME_SCHEMA_BASE_REF`。`check:spacetime-schema` 依赖该基线识别已有表字段删除、改名、重排和改类型;事件给出的基线缺失或本地不可解析时必须直接失败,不能退化为空差异检查。Gitea 的 PR checkout 是 PR head,不是与目标分支的预合并 commit,因此 workflow 还会验证 PR head 包含事件中的最新 base commit;分支保护必须继续开启“PR 过期禁止合并”,过期分支先更新再重跑。向 `master` 直接推送时使用 push before SHA,手工触发时回退到 `origin/master`。 -启用或注册执行 PR job 的 runner 前,Gitea 服务端必须至少升级到 `1.26.4`;不得在 `1.26.2` 上执行不受信任 PR 代码。runner 必须提供 `ubuntu-latest` 标签,并将其映射到经验证且固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像;禁止使用浮动镜像 tag,禁止将该标签映射到 host 执行器,禁止向 job 暴露 Docker socket、业务环境变量、业务密钥或不必要的内网。workflow 会安装 Node 22、仓库 `rust-toolchain.toml` 固定的 Rust 1.96.0,以及 clang/lld 和 Tauri Linux 依赖;受控 runner 镜像应预装 rustup,fallback 下载只用于首次引导。runner 仍需能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm registry、Rust 分发和 crates.io。workflow 中的 `actions/checkout` / `actions/setup-node` 固定到完整 commit;内网 runner 不允许访问 GitHub 时,先把对应 commit 镜像到当前 Gitea 并把 workflow 改为绝对 action URL。首版不使用 Actions cache,避免未配置 runner cache 网络时把缓存恢复错误变成 PR 失败。Gitea 1.26 不执行 workflow 的 `timeout-minutes`,任务最长运行时间在 runner 全局配置收口,不能只在 YAML 写一个不会生效的超时值。 +启用或注册执行 PR job 的 runner 前,Gitea 服务端必须至少升级到 `1.26.4`;不得在 `1.26.2` 上执行不受信任 PR 代码。runner 必须提供 `ubuntu-latest` 标签,并将其映射到经验证且固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像;禁止使用浮动镜像 tag,禁止将该标签映射到 host 执行器,禁止向 job 暴露 Docker socket、业务环境变量、业务密钥或不必要的内网。workflow 会安装 Node 22、仓库 `rust-toolchain.toml` 固定的 Rust 1.96.0,以及 clang/lld 和 Tauri Linux 依赖;受控 runner 镜像应预装 rustup,fallback 下载只用于首次引导。runner 仍需能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm registry、Rust 分发和 crates.io。workflow 中的 `actions/checkout` / `actions/setup-node` 固定到完整 commit;内网 runner 不允许访问 GitHub 时,先把对应 commit 镜像到当前 Gitea 并把 workflow 改为绝对 action URL。首版不使用 Actions cache,避免未配置 runner cache 网络时把缓存恢复错误变成 PR 失败。当前 Runner 2.0.0 已支持 job 级 `timeout-minutes`,但 runner 全局 `3h` 仍是所有任务的硬上限;若 workflow 以后新增更短 timeout,不能删除全局兜底。 + +当前 `genarrative-station` 已于 2026-07-21 升级到 Gitea `1.26.4`,并使用固定 digest 的 Gitea Runner `2.0.0-dind-rootless` 与固定 digest 的 Ubuntu 24.04 job image。外层 runner 以 `rootless` 用户运行,`privileged=false`、不增加 `CAP_SYS_ADMIN`,只映射 `/dev/net/tun`,内部 Docker 只监听私有 Unix socket;runner 配置必须保持 `docker_host: "-"`、`valid_volumes: []` 和 `bind_workdir: false`,防止内部 Docker socket或宿主 bind mount进入 job。job 只连接 `gitea-actions` internal network:`genarrative-station` 由只转发 `/git` 到 Gitea 的内部 gateway解析,公网依赖只经拒绝私网、保留地址和 metadata 的 80/443 egress proxy;绕过 proxy 的公网和 Postgres/Redis 数据网都必须不可达。完整 bwrap canary 需要 rootless DinD 外层的 rootlesskit AppArmor/userns 边界,以及内层 job 的 namespace/proc 挂载支持;相关 `seccomp/systempaths` 放宽只允许存在于这个无宿主 socket的 rootless DinD 内层,禁止复制回控制宿主 rootful Docker 的 runner。workflow 的系统依赖步骤在 UID 0 时不再调用 sudo;非 root runner 必须使用 `sudo -E` 保留受控 proxy 环境,避免 `sudo` 的 `env_reset` 让 apt 绕过 gateway。 + +站点 stack 仍由宿主 `/opt/gitea-stack` 管理,`.env`、runner 注册文件和数据库凭据不进入仓库。Compose 必须在 helper/container 内把该目录挂到同一个绝对路径再执行;若挂成 `/stack`,相对 bind source 会被 Docker daemon 误解析为宿主 `/stack/...` 并启动空数据目录。升级或 runner 迁移前先停止 Gitea 写入并保留 `data/gitea` 冷快照、`pg_dumpall`、compose/.env 与 runner config/.runner;本次可回滚快照位于 `backups/gitea-ci-migration-20260721-211018`。备份文件、绝对宿主配置和注册 token 不得提交 Git。 workflow 首次成功运行后,在 Gitea `master` 分支保护中把 `Project CI / Repository checks (pull_request)`、`Project CI / Frontend tests (pull_request)`、`Project CI / Backend tests (pull_request)`、`Project CI / Native shell tests (pull_request)` 四个完整 context 都设为合并必需检查,并从最近一周已上报 context 表复核名称后再保存。不能只填裸 job 名,否则无法匹配 Gitea 实际上报的 ` / ()`。只提交 workflow 文件不会自动创建 runner,也不会自动修改分支保护;如果 Actions 长时间停留在等待状态,先到仓库或组织的 Actions runner 页面确认存在在线、带 `ubuntu-latest` 标签的 runner。 From bf349a38a327de5452b789f280b1736602c67d1f Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 21 Jul 2026 21:38:03 +0800 Subject: [PATCH 05/14] =?UTF-8?q?=E7=A8=B3=E5=AE=9A=E9=9A=94=E7=A6=BBCI?= =?UTF-8?q?=E7=9A=84Cargo=E4=BE=9D=E8=B5=96=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 关闭 Cargo HTTP multiplexing,降低代理链瞬时 TLS EOF 影响 将 Cargo 网络重试次数提高到 10 次 同步开发运维与共享工作流说明 --- .gitea/workflows/project-ci.yml | 2 ++ docs/project-memory/shared-memory/development-workflow.md | 2 +- docs/【开发运维】本地开发验证与生产运维-2026-05-15.md | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/project-ci.yml b/.gitea/workflows/project-ci.yml index c79eacfd1..a362e6eef 100644 --- a/.gitea/workflows/project-ci.yml +++ b/.gitea/workflows/project-ci.yml @@ -14,6 +14,8 @@ permissions: env: CI: 'true' CARGO_INCREMENTAL: '0' + CARGO_HTTP_MULTIPLEXING: 'false' + CARGO_NET_RETRY: '10' CARGO_TERM_COLOR: always RUSTC_WRAPPER: '' CARGO_BUILD_RUSTC_WRAPPER: '' diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index d9f6b90bf..506032c0c 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -458,7 +458,7 @@ npm run check:server-rs-ddd - checkout 必须使用完整历史。PR 将 base SHA 写入 `SPACETIME_SCHEMA_BASE_REF`,直接推送 `master` 使用 before SHA;事件基线不可解析时直接失败。Gitea 检查的是 PR head 而非预合并 commit,workflow 必须拒绝不包含最新 base commit 的过期 PR,分支保护同时保持“PR 过期禁止合并”。 - 普通 PR job 不读取业务 secret,不运行真实 API/SpacetimeDB/OSS/支付/生成/live smoke,也不执行会修改外部状态的维护、迁移、发布或备份命令。 - Gitea 至少升级到 `1.26.4` 后才能注册执行 PR job 的 runner;`ubuntu-latest` 标签只映射到固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像,不使用浮动镜像 tag,不映射 host,不向 job 暴露 Docker socket、业务 secret 或不必要内网。runner 能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm、Rust 分发和 crates.io;workflow 的官方 action 固定完整 commit,若内网禁用 GitHub,先在当前 Gitea 镜像对应 commit 并改用绝对 URL。受控镜像优先预装 rustup。Gitea 1.26 的任务超时由 runner 全局配置控制;首次运行成功后,`master` 分支保护必须要求上述四个 job 全部成功。 -- `genarrative-station` 当前使用 Gitea `1.26.4` + Runner `2.0.0-dind-rootless`:runner/job image 固定 digest,外层非 privileged、无 `CAP_SYS_ADMIN`,内部 Docker 只监听 Unix socket,`docker_host: "-"` 阻止 socket进入 job。job 只在 `gitea-actions` internal network,通过 `/git` reverse gateway访问 Gitea,通过拒绝私网/保留地址/metadata 的 80/443 proxy访问公共依赖;直连公网和 Gitea 数据网必须失败。内层 bwrap 所需 namespace/proc 选项只能用于该 rootless DinD,不能放宽宿主 rootful runner。系统依赖步骤在 root job 中不调用 sudo,非 root 时用 `sudo -E` 保留受控 proxy。宿主 compose helper 必须把 `/opt/gitea-stack` 挂到同名绝对路径,避免相对 volume错误落到 `/stack` 空目录。 +- `genarrative-station` 当前使用 Gitea `1.26.4` + Runner `2.0.0-dind-rootless`:runner/job image 固定 digest,外层非 privileged、无 `CAP_SYS_ADMIN`,内部 Docker 只监听 Unix socket,`docker_host: "-"` 阻止 socket进入 job。job 只在 `gitea-actions` internal network,通过 `/git` reverse gateway访问 Gitea,通过拒绝私网/保留地址/metadata 的 80/443 proxy访问公共依赖;直连公网和 Gitea 数据网必须失败。内层 bwrap 所需 namespace/proc 选项只能用于该 rootless DinD,不能放宽宿主 rootful runner。系统依赖步骤在 root job 中不调用 sudo,非 root 时用 `sudo -E` 保留受控 proxy;Cargo 关闭 HTTP multiplexing并设置 10 次网络重试。宿主 compose helper 必须把 `/opt/gitea-stack` 挂到同名绝对路径,避免相对 volume错误落到 `/stack` 空目录。 ## 后端相关默认验证 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index a9f5d6d3e..ebc0a881f 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -213,7 +213,7 @@ PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 `SPACETIME 启用或注册执行 PR job 的 runner 前,Gitea 服务端必须至少升级到 `1.26.4`;不得在 `1.26.2` 上执行不受信任 PR 代码。runner 必须提供 `ubuntu-latest` 标签,并将其映射到经验证且固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像;禁止使用浮动镜像 tag,禁止将该标签映射到 host 执行器,禁止向 job 暴露 Docker socket、业务环境变量、业务密钥或不必要的内网。workflow 会安装 Node 22、仓库 `rust-toolchain.toml` 固定的 Rust 1.96.0,以及 clang/lld 和 Tauri Linux 依赖;受控 runner 镜像应预装 rustup,fallback 下载只用于首次引导。runner 仍需能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm registry、Rust 分发和 crates.io。workflow 中的 `actions/checkout` / `actions/setup-node` 固定到完整 commit;内网 runner 不允许访问 GitHub 时,先把对应 commit 镜像到当前 Gitea 并把 workflow 改为绝对 action URL。首版不使用 Actions cache,避免未配置 runner cache 网络时把缓存恢复错误变成 PR 失败。当前 Runner 2.0.0 已支持 job 级 `timeout-minutes`,但 runner 全局 `3h` 仍是所有任务的硬上限;若 workflow 以后新增更短 timeout,不能删除全局兜底。 -当前 `genarrative-station` 已于 2026-07-21 升级到 Gitea `1.26.4`,并使用固定 digest 的 Gitea Runner `2.0.0-dind-rootless` 与固定 digest 的 Ubuntu 24.04 job image。外层 runner 以 `rootless` 用户运行,`privileged=false`、不增加 `CAP_SYS_ADMIN`,只映射 `/dev/net/tun`,内部 Docker 只监听私有 Unix socket;runner 配置必须保持 `docker_host: "-"`、`valid_volumes: []` 和 `bind_workdir: false`,防止内部 Docker socket或宿主 bind mount进入 job。job 只连接 `gitea-actions` internal network:`genarrative-station` 由只转发 `/git` 到 Gitea 的内部 gateway解析,公网依赖只经拒绝私网、保留地址和 metadata 的 80/443 egress proxy;绕过 proxy 的公网和 Postgres/Redis 数据网都必须不可达。完整 bwrap canary 需要 rootless DinD 外层的 rootlesskit AppArmor/userns 边界,以及内层 job 的 namespace/proc 挂载支持;相关 `seccomp/systempaths` 放宽只允许存在于这个无宿主 socket的 rootless DinD 内层,禁止复制回控制宿主 rootful Docker 的 runner。workflow 的系统依赖步骤在 UID 0 时不再调用 sudo;非 root runner 必须使用 `sudo -E` 保留受控 proxy 环境,避免 `sudo` 的 `env_reset` 让 apt 绕过 gateway。 +当前 `genarrative-station` 已于 2026-07-21 升级到 Gitea `1.26.4`,并使用固定 digest 的 Gitea Runner `2.0.0-dind-rootless` 与固定 digest 的 Ubuntu 24.04 job image。外层 runner 以 `rootless` 用户运行,`privileged=false`、不增加 `CAP_SYS_ADMIN`,只映射 `/dev/net/tun`,内部 Docker 只监听私有 Unix socket;runner 配置必须保持 `docker_host: "-"`、`valid_volumes: []` 和 `bind_workdir: false`,防止内部 Docker socket或宿主 bind mount进入 job。job 只连接 `gitea-actions` internal network:`genarrative-station` 由只转发 `/git` 到 Gitea 的内部 gateway解析,公网依赖只经拒绝私网、保留地址和 metadata 的 80/443 egress proxy;绕过 proxy 的公网和 Postgres/Redis 数据网都必须不可达。完整 bwrap canary 需要 rootless DinD 外层的 rootlesskit AppArmor/userns 边界,以及内层 job 的 namespace/proc 挂载支持;相关 `seccomp/systempaths` 放宽只允许存在于这个无宿主 socket的 rootless DinD 内层,禁止复制回控制宿主 rootful Docker 的 runner。workflow 的系统依赖步骤在 UID 0 时不再调用 sudo;非 root runner 必须使用 `sudo -E` 保留受控 proxy 环境,避免 `sudo` 的 `env_reset` 让 apt 绕过 gateway。Cargo 通过 proxy 下载 sparse index/crate 时固定关闭 HTTP multiplexing 并设置 `CARGO_NET_RETRY=10`,降低代理链瞬时 TLS EOF 对后端测试的影响,但不能用重试掩盖持续不可达。 站点 stack 仍由宿主 `/opt/gitea-stack` 管理,`.env`、runner 注册文件和数据库凭据不进入仓库。Compose 必须在 helper/container 内把该目录挂到同一个绝对路径再执行;若挂成 `/stack`,相对 bind source 会被 Docker daemon 误解析为宿主 `/stack/...` 并启动空数据目录。升级或 runner 迁移前先停止 Gitea 写入并保留 `data/gitea` 冷快照、`pg_dumpall`、compose/.env 与 runner config/.runner;本次可回滚快照位于 `backups/gitea-ci-migration-20260721-211018`。备份文件、绝对宿主配置和注册 token 不得提交 Git。 From 5d045d532f3a57f9c7e21ccfce0685d9d1905880 Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 21 Jul 2026 21:56:23 +0800 Subject: [PATCH 06/14] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=9A=94=E7=A6=BBCI?= =?UTF-8?q?=E7=9A=84=E5=8E=9F=E7=94=9F=E6=B2=99=E7=AE=B1=E7=8E=AF=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 rustup 引导和工具链安装增加有界重试 显式安装并预检 bubblewrap 沙箱能力 将 Node 与 Rust 工具暴露到受信任系统路径 记录 Runner systempaths 空切片修补与验收口径 --- .gitea/workflows/project-ci.yml | 125 ++++++++++++++++-- .../shared-memory/development-workflow.md | 2 +- docs/project-memory/shared-memory/pitfalls.md | 6 +- ...发运维】本地开发验证与生产运维-2026-05-15.md | 4 +- 4 files changed, 121 insertions(+), 16 deletions(-) diff --git a/.gitea/workflows/project-ci.yml b/.gitea/workflows/project-ci.yml index a362e6eef..e36490c9a 100644 --- a/.gitea/workflows/project-ci.yml +++ b/.gitea/workflows/project-ci.yml @@ -17,6 +17,7 @@ env: CARGO_HTTP_MULTIPLEXING: 'false' CARGO_NET_RETRY: '10' CARGO_TERM_COLOR: always + RUSTUP_MAX_RETRIES: '10' RUSTC_WRAPPER: '' CARGO_BUILD_RUSTC_WRAPPER: '' @@ -86,14 +87,33 @@ jobs: run: | set -euo pipefail if ! command -v rustup >/dev/null 2>&1; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --profile minimal --default-toolchain none + for attempt in $(seq 1 10); do + if curl --retry 3 --retry-all-errors --retry-delay 2 \ + --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain none; then + break + fi + if [[ "${attempt}" -eq 10 ]]; then + echo 'rustup bootstrap failed after 10 attempts.' >&2 + exit 1 + fi + sleep $((attempt * 2)) + done fi echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}" export PATH="${HOME}/.cargo/bin:${PATH}" toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" test -n "${toolchain}" - rustup toolchain install "${toolchain}" --profile minimal --component rustfmt + for attempt in $(seq 1 10); do + if rustup toolchain install "${toolchain}" --profile minimal --component rustfmt; then + break + fi + if [[ "${attempt}" -eq 10 ]]; then + echo 'Rust toolchain installation failed after 10 attempts.' >&2 + exit 1 + fi + sleep $((attempt * 2)) + done rustc --version cargo --version rustfmt --version @@ -216,14 +236,33 @@ jobs: run: | set -euo pipefail if ! command -v rustup >/dev/null 2>&1; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --profile minimal --default-toolchain none + for attempt in $(seq 1 10); do + if curl --retry 3 --retry-all-errors --retry-delay 2 \ + --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain none; then + break + fi + if [[ "${attempt}" -eq 10 ]]; then + echo 'rustup bootstrap failed after 10 attempts.' >&2 + exit 1 + fi + sleep $((attempt * 2)) + done fi echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}" export PATH="${HOME}/.cargo/bin:${PATH}" toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" test -n "${toolchain}" - rustup toolchain install "${toolchain}" --profile minimal --component rustfmt + for attempt in $(seq 1 10); do + if rustup toolchain install "${toolchain}" --profile minimal --component rustfmt; then + break + fi + if [[ "${attempt}" -eq 10 ]]; then + echo 'Rust toolchain installation failed after 10 attempts.' >&2 + exit 1 + fi + sleep $((attempt * 2)) + done rustc --version cargo --version rustfmt --version @@ -272,6 +311,7 @@ jobs: "${sudo_command[@]}" apt-get update "${sudo_command[@]}" env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential \ + bubblewrap \ ca-certificates \ clang \ cmake \ @@ -286,6 +326,18 @@ jobs: patchelf \ pkg-config \ wget + bwrap --die-with-parent \ + --unshare-all \ + --unshare-user \ + --disable-userns \ + --assert-userns-disabled \ + --cap-drop ALL \ + --clearenv \ + --ro-bind /usr /usr \ + --proc /proc \ + --dev /dev \ + --tmpfs /tmp \ + -- /usr/bin/true - name: Set up Node.js 22 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -300,6 +352,8 @@ jobs: npm_path="$(command -v npm)" test -x "${node_path}" test -x "${npm_path}" + node_root="$(cd "$(dirname "${node_path}")/.." && pwd -P)" + test -d "${node_root}/lib/node_modules/npm" sudo_command=() if [[ "$(id -u)" -ne 0 ]]; then command -v sudo >/dev/null 2>&1 || { @@ -308,8 +362,11 @@ jobs: } sudo_command=(sudo -E) fi - "${sudo_command[@]}" ln -sfn "${node_path}" /usr/local/bin/node - "${sudo_command[@]}" ln -sfn "${npm_path}" /usr/local/bin/npm + trusted_node_root='/usr/local/lib/genarrative-node' + "${sudo_command[@]}" install -d -m 0755 "${trusted_node_root}" + "${sudo_command[@]}" cp -a "${node_root}/." "${trusted_node_root}/" + "${sudo_command[@]}" ln -sfn "${trusted_node_root}/bin/node" /usr/local/bin/node + "${sudo_command[@]}" ln -sfn "${trusted_node_root}/bin/npm" /usr/local/bin/npm /usr/local/bin/node --version /usr/local/bin/npm --version @@ -318,18 +375,64 @@ jobs: run: | set -euo pipefail if ! command -v rustup >/dev/null 2>&1; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --profile minimal --default-toolchain none + for attempt in $(seq 1 10); do + if curl --retry 3 --retry-all-errors --retry-delay 2 \ + --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain none; then + break + fi + if [[ "${attempt}" -eq 10 ]]; then + echo 'rustup bootstrap failed after 10 attempts.' >&2 + exit 1 + fi + sleep $((attempt * 2)) + done fi echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}" export PATH="${HOME}/.cargo/bin:${PATH}" toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" test -n "${toolchain}" - rustup toolchain install "${toolchain}" --profile minimal --component rustfmt + for attempt in $(seq 1 10); do + if rustup toolchain install "${toolchain}" --profile minimal --component rustfmt; then + break + fi + if [[ "${attempt}" -eq 10 ]]; then + echo 'Rust toolchain installation failed after 10 attempts.' >&2 + exit 1 + fi + sleep $((attempt * 2)) + done rustc --version cargo --version rustfmt --version + - name: Expose trusted Rust command paths + shell: bash + run: | + set -euo pipefail + rustup_path="$(command -v rustup)" + test -x "${rustup_path}" + rustup_home="$(rustup show home)" + test -d "${rustup_home}" + sudo_command=() + if [[ "$(id -u)" -ne 0 ]]; then + command -v sudo >/dev/null 2>&1 || { + echo 'non-root runner user requires sudo to expose trusted Rust paths.' >&2 + exit 1 + } + sudo_command=(sudo -E) + fi + if [[ "$(readlink -f "${rustup_path}")" != '/usr/local/bin/rustup' ]]; then + "${sudo_command[@]}" install -m 0755 "${rustup_path}" /usr/local/bin/rustup + fi + for command_name in cargo rustc rustdoc rustfmt; do + "${sudo_command[@]}" ln -sfn rustup "/usr/local/bin/${command_name}" + done + echo "RUSTUP_HOME=${rustup_home}" >> "${GITHUB_ENV}" + /usr/local/bin/cargo --version + /usr/local/bin/rustc --version + /usr/local/bin/rustfmt --version + - name: Install npm dependencies run: npm ci diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index 506032c0c..b3e0893c1 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -458,7 +458,7 @@ npm run check:server-rs-ddd - checkout 必须使用完整历史。PR 将 base SHA 写入 `SPACETIME_SCHEMA_BASE_REF`,直接推送 `master` 使用 before SHA;事件基线不可解析时直接失败。Gitea 检查的是 PR head 而非预合并 commit,workflow 必须拒绝不包含最新 base commit 的过期 PR,分支保护同时保持“PR 过期禁止合并”。 - 普通 PR job 不读取业务 secret,不运行真实 API/SpacetimeDB/OSS/支付/生成/live smoke,也不执行会修改外部状态的维护、迁移、发布或备份命令。 - Gitea 至少升级到 `1.26.4` 后才能注册执行 PR job 的 runner;`ubuntu-latest` 标签只映射到固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像,不使用浮动镜像 tag,不映射 host,不向 job 暴露 Docker socket、业务 secret 或不必要内网。runner 能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm、Rust 分发和 crates.io;workflow 的官方 action 固定完整 commit,若内网禁用 GitHub,先在当前 Gitea 镜像对应 commit 并改用绝对 URL。受控镜像优先预装 rustup。Gitea 1.26 的任务超时由 runner 全局配置控制;首次运行成功后,`master` 分支保护必须要求上述四个 job 全部成功。 -- `genarrative-station` 当前使用 Gitea `1.26.4` + Runner `2.0.0-dind-rootless`:runner/job image 固定 digest,外层非 privileged、无 `CAP_SYS_ADMIN`,内部 Docker 只监听 Unix socket,`docker_host: "-"` 阻止 socket进入 job。job 只在 `gitea-actions` internal network,通过 `/git` reverse gateway访问 Gitea,通过拒绝私网/保留地址/metadata 的 80/443 proxy访问公共依赖;直连公网和 Gitea 数据网必须失败。内层 bwrap 所需 namespace/proc 选项只能用于该 rootless DinD,不能放宽宿主 rootful runner。系统依赖步骤在 root job 中不调用 sudo,非 root 时用 `sudo -E` 保留受控 proxy;Cargo 关闭 HTTP multiplexing并设置 10 次网络重试。宿主 compose helper 必须把 `/opt/gitea-stack` 挂到同名绝对路径,避免相对 volume错误落到 `/stack` 空目录。 +- `genarrative-station` 当前使用 Gitea `1.26.4` + 基于 Runner `2.0.0-dind-rootless` 的固定 digest 修补镜像:只修复 `systempaths=unconfined` 的空 slice 被 `mergo` 丢失,真实 job 必须保持 `MaskedPaths=[]`、`ReadonlyPaths=[]`;外层仍非 privileged、无 `CAP_SYS_ADMIN`,内部 Docker 只监听 Unix socket,`docker_host: "-"` 阻止 socket 进入 job。job 只在 `gitea-actions` internal network,通过 `/git` reverse gateway 访问 Gitea,通过拒绝私网、保留地址和 metadata 的 80/443 proxy 访问公共依赖;直连公网和 Gitea 数据网必须失败。内层 bwrap 所需 namespace/proc 选项只能用于该 rootless DinD,不能放宽宿主 rootful runner。系统依赖步骤在 root job 中不调用 sudo,非 root 时用 `sudo -E` 保留受控 proxy;Cargo 关闭 HTTP multiplexing 并设置 10 次网络重试,rustup bootstrap/toolchain 安装也按有界次数重试。AI 原生壳 job 把 Node 发行目录与 rustup proxy 安装到 `/usr/local` 的受信任只读路径,并在测试前执行完整 bwrap canary。宿主 compose helper 必须把 `/opt/gitea-stack` 挂到同名绝对路径,避免相对 volume 错误落到 `/stack` 空目录。 ## 后端相关默认验证 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 969adf6d3..68917a015 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -3479,8 +3479,8 @@ ## Gitea PR runner 不能把宿主 Docker socket 当普通 volume - 现象:`act_runner` 的 `container.docker_host` 留空时,job inspect 会出现 `/var/run/docker.sock:/var/run/docker.sock`;PR 脚本即使 `valid_volumes: []`,仍可直接调用 Docker API读取其它容器、挂宿主目录或取得 runner 配置。另一类迁移故障是 rootless daemon可以启动,但 bwrap 在 job 内报 `No permissions to create new namespace`、`Failed to make / slave` 或 `Mount too revealing`。 -- 原因:空 `docker_host` 会自动发现并把控制 socket传播到 job;rootful Docker 的 seccomp、AppArmor 与 system-path masks 又不支持完整 nested bwrap。直接使用 `--privileged`、外层 `CAP_SYS_ADMIN` 或 host executor 会把测试跑绿建立在破坏 PR 隔离的前提上。宿主启用 Clash fake-IP 时,简单按 DNS 的 `198.18.0.0/15` 判断公网还会误拒所有公共依赖。 -- 处理:先把 Gitea 升到至少 `1.26.4`,再用固定 digest 的 Runner 2.0.0 rootless DinD;外层保持非 privileged、无 `CAP_SYS_ADMIN`,内部 Docker 仅 Unix socket,runner 固定 `docker_host: "-"`。job 使用独立 internal network,Gitea 经 reverse gateway,公共 80/443 经使用公共 DoH 验证真实 IP、拒绝私网/保留地址/metadata 的 proxy;DoH 要缓存并合并同域并发,长下载 timeout 不能只有 60 秒。rootless job 内的 bwrap namespace/proc 选项不能复制到宿主 rootful runner。apt 步骤在 root 时直接执行,非 root 时用 `sudo -E`,否则 sudo `env_reset` 会让 apt 丢失 proxy。 +- 原因:空 `docker_host` 会自动发现并把控制 socket 传播到 job;rootful Docker 的 seccomp、AppArmor 与 system-path masks 又不支持完整 nested bwrap。Runner 2.0.0 还有一处独立合并缺陷:`parseSystemPaths` 把 `systempaths=unconfined` 转成显式空 slice 后,`mergo.WithOverride` 不覆盖 empty value,真实 job 又恢复 Docker 默认 masks,表现为配置文件写了 unconfined、手工 `docker run` canary 也成功,但 Actions job 仍在 `--proc /proc` 返回 EPERM。直接使用 `--privileged`、外层 `CAP_SYS_ADMIN` 或 host executor 会把测试跑绿建立在破坏 PR 隔离的前提上。宿主启用 Clash fake-IP 时,简单按 DNS 的 `198.18.0.0/15` 判断公网还会误拒所有公共依赖。 +- 处理:先把 Gitea 升到至少 `1.26.4`,再用固定 digest 的 Runner 2.0.0 rootless DinD;外层保持非 privileged、无 `CAP_SYS_ADMIN`,内部 Docker 仅 Unix socket,runner 固定 `docker_host: "-"`。若版本仍有上述 empty-slice 缺陷,只做 merge 后保留 `MaskedPaths=[]` / `ReadonlyPaths=[]` 的最小补丁并固定自有镜像 digest,不对整个 HostConfig 启用 overwrite-empty。job 使用独立 internal network,Gitea 经 reverse gateway,公共 80/443 经使用公共 DoH 验证真实 IP、拒绝私网/保留地址/metadata 的 proxy;DoH 要缓存并合并同域并发,长下载 timeout 不能只有 60 秒。rootless job 内的 bwrap namespace/proc 选项不能复制到宿主 rootful runner。apt 步骤在 root 时直接执行,非 root 时用 `sudo -E`,否则 sudo `env_reset` 会让 apt 丢失 proxy。 - 运维陷阱:从容器内运行 Compose 时,宿主 `/opt/gitea-stack` 必须挂到容器同名绝对路径;挂成 `/stack` 会让相对 bind source 被 daemon解析为宿主 `/stack/...`,表现为 Gitea进入空安装页、gateway 脚本“缺失”。发现后不要迁移空库,立即用同路径 mount 重建并核对原数据大小、installed 日志、仓库数和 API。切换前保留冷数据 tar、pg_dumpall 和原 compose/env/runner 配置,备份与 token 不提交 Git。 -- 验证:同时检查 Gitea 版本、runner declare、外层 `Privileged=false`/无 CapAdd/无宿主 socket、inner job `Binds=[]`、固定 image digest、`/var/run/docker.sock` 不存在、公共 proxy可用、直连公网/Postgres/metadata 失败,以及完整 bwrap canary。最后重跑四个 CI job;checkout成功但 apt/rustup/npm 同时失败时,先排 proxy/env,而不是改测试。 +- 验证:同时检查 Gitea 版本、runner declare、外层 `Privileged=false`/无 CapAdd/无宿主 socket、inner job `Binds=[]`、`MaskedPaths=[]`、`ReadonlyPaths=[]`、固定 image digest、`/var/run/docker.sock` 不存在、公共 proxy 可用、直连公网/Postgres/metadata 失败,以及完整 bwrap canary。最后重跑四个 CI job;checkout 成功但 apt/rustup/npm 同时失败时,先排 proxy/env,而不是改测试。 - 关联:`.gitea/workflows/project-ci.yml`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`、`docs/project-memory/shared-memory/development-workflow.md`。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index ebc0a881f..d6e52bbb6 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -213,7 +213,9 @@ PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 `SPACETIME 启用或注册执行 PR job 的 runner 前,Gitea 服务端必须至少升级到 `1.26.4`;不得在 `1.26.2` 上执行不受信任 PR 代码。runner 必须提供 `ubuntu-latest` 标签,并将其映射到经验证且固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像;禁止使用浮动镜像 tag,禁止将该标签映射到 host 执行器,禁止向 job 暴露 Docker socket、业务环境变量、业务密钥或不必要的内网。workflow 会安装 Node 22、仓库 `rust-toolchain.toml` 固定的 Rust 1.96.0,以及 clang/lld 和 Tauri Linux 依赖;受控 runner 镜像应预装 rustup,fallback 下载只用于首次引导。runner 仍需能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm registry、Rust 分发和 crates.io。workflow 中的 `actions/checkout` / `actions/setup-node` 固定到完整 commit;内网 runner 不允许访问 GitHub 时,先把对应 commit 镜像到当前 Gitea 并把 workflow 改为绝对 action URL。首版不使用 Actions cache,避免未配置 runner cache 网络时把缓存恢复错误变成 PR 失败。当前 Runner 2.0.0 已支持 job 级 `timeout-minutes`,但 runner 全局 `3h` 仍是所有任务的硬上限;若 workflow 以后新增更短 timeout,不能删除全局兜底。 -当前 `genarrative-station` 已于 2026-07-21 升级到 Gitea `1.26.4`,并使用固定 digest 的 Gitea Runner `2.0.0-dind-rootless` 与固定 digest 的 Ubuntu 24.04 job image。外层 runner 以 `rootless` 用户运行,`privileged=false`、不增加 `CAP_SYS_ADMIN`,只映射 `/dev/net/tun`,内部 Docker 只监听私有 Unix socket;runner 配置必须保持 `docker_host: "-"`、`valid_volumes: []` 和 `bind_workdir: false`,防止内部 Docker socket或宿主 bind mount进入 job。job 只连接 `gitea-actions` internal network:`genarrative-station` 由只转发 `/git` 到 Gitea 的内部 gateway解析,公网依赖只经拒绝私网、保留地址和 metadata 的 80/443 egress proxy;绕过 proxy 的公网和 Postgres/Redis 数据网都必须不可达。完整 bwrap canary 需要 rootless DinD 外层的 rootlesskit AppArmor/userns 边界,以及内层 job 的 namespace/proc 挂载支持;相关 `seccomp/systempaths` 放宽只允许存在于这个无宿主 socket的 rootless DinD 内层,禁止复制回控制宿主 rootful Docker 的 runner。workflow 的系统依赖步骤在 UID 0 时不再调用 sudo;非 root runner 必须使用 `sudo -E` 保留受控 proxy 环境,避免 `sudo` 的 `env_reset` 让 apt 绕过 gateway。Cargo 通过 proxy 下载 sparse index/crate 时固定关闭 HTTP multiplexing 并设置 `CARGO_NET_RETRY=10`,降低代理链瞬时 TLS EOF 对后端测试的影响,但不能用重试掩盖持续不可达。 +当前 `genarrative-station` 已于 2026-07-21 升级到 Gitea `1.26.4`,并使用基于 Gitea Runner `2.0.0-dind-rootless` 的固定 digest 修补镜像与固定 digest 的 Ubuntu 24.04 job image。Runner 2.0.0 会先把 `systempaths=unconfined` 解析为空 `MaskedPaths` / `ReadonlyPaths`,再被 `mergo.WithOverride` 当成 empty value 丢失;站点修补只在 merge 后保留这两个显式空 slice,不改其它 runner 行为。真实 job inspect 必须看到 `MaskedPaths=[]`、`ReadonlyPaths=[]`、`SecurityOpt=[seccomp=unconfined]`、`Privileged=false`、无 CapAdd 且 `Binds=[]`。外层 runner 以 `rootless` 用户运行,`privileged=false`、不增加 `CAP_SYS_ADMIN`,只映射 `/dev/net/tun`,内部 Docker 只监听私有 Unix socket;runner 配置必须保持 `docker_host: "-"`、`valid_volumes: []` 和 `bind_workdir: false`,防止内部 Docker socket 或宿主 bind mount 进入 job。job 只连接 `gitea-actions` internal network:`genarrative-station` 由只转发 `/git` 到 Gitea 的内部 gateway 解析,公网依赖只经拒绝私网、保留地址和 metadata 的 80/443 egress proxy;绕过 proxy 的公网和 Postgres/Redis 数据网都必须不可达。完整 bwrap canary 需要 rootless DinD 外层的 rootlesskit AppArmor/userns 边界,以及内层 job 的 namespace/proc 挂载支持;相关 `seccomp/systempaths` 放宽只允许存在于这个无宿主 socket 的 rootless DinD 内层,禁止复制回控制宿主 rootful Docker 的 runner。 + +workflow 的系统依赖步骤在 UID 0 时不再调用 sudo;非 root runner 必须使用 `sudo -E` 保留受控 proxy 环境,避免 `sudo` 的 `env_reset` 让 apt 绕过 gateway。Cargo 通过 proxy 下载 sparse index/crate 时固定关闭 HTTP multiplexing 并设置 `CARGO_NET_RETRY=10`;rustup bootstrap 与 toolchain 安装也执行有界重试,降低代理链瞬时 TLS EOF 对后端测试的影响,但不能用重试掩盖持续不可达。AI 原生壳 job 不能把 `$HOME/.cargo/bin` 或 setup-node toolcache 直接加入 `command.exec` 的受信任 PATH;应把完整 Node 发行目录复制到 `/usr/local/lib`,把 root-owned rustup proxy 安装到 `/usr/local/bin`,并通过 `RUSTUP_HOME` 只读挂载工具链。测试前先运行与应用一致的完整 bwrap canary,失败时停止测试,不允许跳过 sandbox 用例。 站点 stack 仍由宿主 `/opt/gitea-stack` 管理,`.env`、runner 注册文件和数据库凭据不进入仓库。Compose 必须在 helper/container 内把该目录挂到同一个绝对路径再执行;若挂成 `/stack`,相对 bind source 会被 Docker daemon 误解析为宿主 `/stack/...` 并启动空数据目录。升级或 runner 迁移前先停止 Gitea 写入并保留 `data/gitea` 冷快照、`pg_dumpall`、compose/.env 与 runner config/.runner;本次可回滚快照位于 `backups/gitea-ci-migration-20260721-211018`。备份文件、绝对宿主配置和注册 token 不得提交 Git。 From 94ee363d84797b0ed118fc1ac11e2d1e107da5d5 Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 21 Jul 2026 22:03:01 +0800 Subject: [PATCH 07/14] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E5=8E=9F=E7=94=9FCI?= =?UTF-8?q?=E7=9A=84merged-usr=E9=A2=84=E6=A3=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按运行时代码动态复现 merged-usr 符号链接 避免 bubblewrap canary 因动态加载器路径误报失败 --- .gitea/workflows/project-ci.yml | 35 ++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/.gitea/workflows/project-ci.yml b/.gitea/workflows/project-ci.yml index e36490c9a..11c6f8543 100644 --- a/.gitea/workflows/project-ci.yml +++ b/.gitea/workflows/project-ci.yml @@ -326,18 +326,29 @@ jobs: patchelf \ pkg-config \ wget - bwrap --die-with-parent \ - --unshare-all \ - --unshare-user \ - --disable-userns \ - --assert-userns-disabled \ - --cap-drop ALL \ - --clearenv \ - --ro-bind /usr /usr \ - --proc /proc \ - --dev /dev \ - --tmpfs /tmp \ - -- /usr/bin/true + bwrap_args=( + --die-with-parent + --unshare-all + --unshare-user + --disable-userns + --assert-userns-disabled + --cap-drop ALL + --clearenv + --ro-bind /usr /usr + ) + for merged_path in /bin /sbin /lib /lib64; do + if [[ -L "${merged_path}" ]]; then + bwrap_args+=(--symlink "$(readlink "${merged_path}")" "${merged_path}") + fi + done + bwrap_args+=( + --proc /proc + --dev /dev + --tmpfs /tmp + -- + /usr/bin/true + ) + bwrap "${bwrap_args[@]}" - name: Set up Node.js 22 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 From a273377b1778126647bdec0efbf86b87d759c91e Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 21 Jul 2026 22:23:17 +0800 Subject: [PATCH 08/14] =?UTF-8?q?=E7=A8=B3=E5=AE=9AAI=E5=8E=9F=E7=94=9F?= =?UTF-8?q?=E5=A3=B3=E5=85=A8=E9=87=8F=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 command.exec 沙箱测试安装受信任的 ripgrep 将共享后台锁的 Tauri suite 固定为单线程 同步 CI 调度与排障文档 --- .gitea/workflows/project-ci.yml | 1 + docs/project-memory/shared-memory/development-workflow.md | 2 +- docs/project-memory/shared-memory/pitfalls.md | 2 +- docs/【开发运维】本地开发验证与生产运维-2026-05-15.md | 2 +- package.json | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/project-ci.yml b/.gitea/workflows/project-ci.yml index 11c6f8543..b9c7d8486 100644 --- a/.gitea/workflows/project-ci.yml +++ b/.gitea/workflows/project-ci.yml @@ -325,6 +325,7 @@ jobs: lld \ patchelf \ pkg-config \ + ripgrep \ wget bwrap_args=( --die-with-parent diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index b3e0893c1..e46e7d940 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -454,7 +454,7 @@ npm run check:server-rs-ddd - 仓库 CI 入口是 `.gitea/workflows/project-ci.yml`,向 `master`、`codex/ai-game-creator-app` 推送和所有 PR 创建、更新时必须运行,也允许手工触发。 - CI 固定拆分为 `Repository checks`、`Frontend tests`、`Backend tests`、`Native shell tests` 四个 required job;对应 PR context 完整名称是 `Project CI / Repository checks (pull_request)`、`Project CI / Frontend tests (pull_request)`、`Project CI / Backend tests (pull_request)`、`Project CI / Native shell tests (pull_request)`,首次运行后仍须从 Gitea 最近一周 context 表复核。测试使用独立 job,不能只藏在综合检查 step 中;原生壳验收单独运行以便定位重型构建失败。 -- 四个 job 共同覆盖 `npm run check`,并追加 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --all-targets --manifest-path server-rs/Cargo.toml` 和 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。后端 runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。`codex/ai-game-creator-app` 分支必须用独立 lockfile 安装 AI 游戏创作壳依赖,其原生壳入口还必须覆盖 `npm run ai-game-creator-shell:check`、release build smoke,并检查 AI Tauri `Cargo.lock` 不漂移。原生壳 job 还要把 `actions/setup-node` 的 Node.js 22 与 npm 映射到 `/usr/local/bin`,供只接受受信任系统命令目录的 `command.exec` 沙箱测试使用,不能放宽生产命令目录白名单。 +- 四个 job 共同覆盖 `npm run check`,并追加 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --all-targets --manifest-path server-rs/Cargo.toml` 和 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。后端 runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。`codex/ai-game-creator-app` 分支必须用独立 lockfile 安装 AI 游戏创作壳依赖,其原生壳入口还必须覆盖 `npm run ai-game-creator-shell:check`、release build smoke,并检查 AI Tauri `Cargo.lock` 不漂移。原生壳 job 还要安装 `ripgrep`,把 `actions/setup-node` 的完整 Node.js 22 发行目录与 root-owned rustup proxy 映射到 `/usr/local`,供只接受受信任系统命令目录的 `command.exec` 沙箱测试使用,不能放宽生产命令目录白名单。Tauri 的 1132 项级别 suite 固定 `--test-threads=1`,避免共享 Agent Runtime 后台锁与异步终态在 libtest 并行调度下互相干扰。 - checkout 必须使用完整历史。PR 将 base SHA 写入 `SPACETIME_SCHEMA_BASE_REF`,直接推送 `master` 使用 before SHA;事件基线不可解析时直接失败。Gitea 检查的是 PR head 而非预合并 commit,workflow 必须拒绝不包含最新 base commit 的过期 PR,分支保护同时保持“PR 过期禁止合并”。 - 普通 PR job 不读取业务 secret,不运行真实 API/SpacetimeDB/OSS/支付/生成/live smoke,也不执行会修改外部状态的维护、迁移、发布或备份命令。 - Gitea 至少升级到 `1.26.4` 后才能注册执行 PR job 的 runner;`ubuntu-latest` 标签只映射到固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像,不使用浮动镜像 tag,不映射 host,不向 job 暴露 Docker socket、业务 secret 或不必要内网。runner 能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm、Rust 分发和 crates.io;workflow 的官方 action 固定完整 commit,若内网禁用 GitHub,先在当前 Gitea 镜像对应 commit 并改用绝对 URL。受控镜像优先预装 rustup。Gitea 1.26 的任务超时由 runner 全局配置控制;首次运行成功后,`master` 分支保护必须要求上述四个 job 全部成功。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 68917a015..faaf77cd0 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -3482,5 +3482,5 @@ - 原因:空 `docker_host` 会自动发现并把控制 socket 传播到 job;rootful Docker 的 seccomp、AppArmor 与 system-path masks 又不支持完整 nested bwrap。Runner 2.0.0 还有一处独立合并缺陷:`parseSystemPaths` 把 `systempaths=unconfined` 转成显式空 slice 后,`mergo.WithOverride` 不覆盖 empty value,真实 job 又恢复 Docker 默认 masks,表现为配置文件写了 unconfined、手工 `docker run` canary 也成功,但 Actions job 仍在 `--proc /proc` 返回 EPERM。直接使用 `--privileged`、外层 `CAP_SYS_ADMIN` 或 host executor 会把测试跑绿建立在破坏 PR 隔离的前提上。宿主启用 Clash fake-IP 时,简单按 DNS 的 `198.18.0.0/15` 判断公网还会误拒所有公共依赖。 - 处理:先把 Gitea 升到至少 `1.26.4`,再用固定 digest 的 Runner 2.0.0 rootless DinD;外层保持非 privileged、无 `CAP_SYS_ADMIN`,内部 Docker 仅 Unix socket,runner 固定 `docker_host: "-"`。若版本仍有上述 empty-slice 缺陷,只做 merge 后保留 `MaskedPaths=[]` / `ReadonlyPaths=[]` 的最小补丁并固定自有镜像 digest,不对整个 HostConfig 启用 overwrite-empty。job 使用独立 internal network,Gitea 经 reverse gateway,公共 80/443 经使用公共 DoH 验证真实 IP、拒绝私网/保留地址/metadata 的 proxy;DoH 要缓存并合并同域并发,长下载 timeout 不能只有 60 秒。rootless job 内的 bwrap namespace/proc 选项不能复制到宿主 rootful runner。apt 步骤在 root 时直接执行,非 root 时用 `sudo -E`,否则 sudo `env_reset` 会让 apt 丢失 proxy。 - 运维陷阱:从容器内运行 Compose 时,宿主 `/opt/gitea-stack` 必须挂到容器同名绝对路径;挂成 `/stack` 会让相对 bind source 被 daemon解析为宿主 `/stack/...`,表现为 Gitea进入空安装页、gateway 脚本“缺失”。发现后不要迁移空库,立即用同路径 mount 重建并核对原数据大小、installed 日志、仓库数和 API。切换前保留冷数据 tar、pg_dumpall 和原 compose/env/runner 配置,备份与 token 不提交 Git。 -- 验证:同时检查 Gitea 版本、runner declare、外层 `Privileged=false`/无 CapAdd/无宿主 socket、inner job `Binds=[]`、`MaskedPaths=[]`、`ReadonlyPaths=[]`、固定 image digest、`/var/run/docker.sock` 不存在、公共 proxy 可用、直连公网/Postgres/metadata 失败,以及完整 bwrap canary。最后重跑四个 CI job;checkout 成功但 apt/rustup/npm 同时失败时,先排 proxy/env,而不是改测试。 +- 验证:同时检查 Gitea 版本、runner declare、外层 `Privileged=false`/无 CapAdd/无宿主 socket、inner job `Binds=[]`、`MaskedPaths=[]`、`ReadonlyPaths=[]`、固定 image digest、`/var/run/docker.sock` 不存在、公共 proxy 可用、直连公网/Postgres/metadata 失败,以及完整 bwrap canary。AI 原生壳的共享 Agent Runtime 后台锁 suite 固定单线程执行;并行全量出现锁或异步终态失败、逐项单线程全部通过时,修正 suite 调度口径,不放宽断言。最后重跑四个 CI job;checkout 成功但 apt/rustup/npm 同时失败时,先排 proxy/env,而不是改测试。 - 关联:`.gitea/workflows/project-ci.yml`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`、`docs/project-memory/shared-memory/development-workflow.md`。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index d6e52bbb6..2d258cc0e 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -215,7 +215,7 @@ PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 `SPACETIME 当前 `genarrative-station` 已于 2026-07-21 升级到 Gitea `1.26.4`,并使用基于 Gitea Runner `2.0.0-dind-rootless` 的固定 digest 修补镜像与固定 digest 的 Ubuntu 24.04 job image。Runner 2.0.0 会先把 `systempaths=unconfined` 解析为空 `MaskedPaths` / `ReadonlyPaths`,再被 `mergo.WithOverride` 当成 empty value 丢失;站点修补只在 merge 后保留这两个显式空 slice,不改其它 runner 行为。真实 job inspect 必须看到 `MaskedPaths=[]`、`ReadonlyPaths=[]`、`SecurityOpt=[seccomp=unconfined]`、`Privileged=false`、无 CapAdd 且 `Binds=[]`。外层 runner 以 `rootless` 用户运行,`privileged=false`、不增加 `CAP_SYS_ADMIN`,只映射 `/dev/net/tun`,内部 Docker 只监听私有 Unix socket;runner 配置必须保持 `docker_host: "-"`、`valid_volumes: []` 和 `bind_workdir: false`,防止内部 Docker socket 或宿主 bind mount 进入 job。job 只连接 `gitea-actions` internal network:`genarrative-station` 由只转发 `/git` 到 Gitea 的内部 gateway 解析,公网依赖只经拒绝私网、保留地址和 metadata 的 80/443 egress proxy;绕过 proxy 的公网和 Postgres/Redis 数据网都必须不可达。完整 bwrap canary 需要 rootless DinD 外层的 rootlesskit AppArmor/userns 边界,以及内层 job 的 namespace/proc 挂载支持;相关 `seccomp/systempaths` 放宽只允许存在于这个无宿主 socket 的 rootless DinD 内层,禁止复制回控制宿主 rootful Docker 的 runner。 -workflow 的系统依赖步骤在 UID 0 时不再调用 sudo;非 root runner 必须使用 `sudo -E` 保留受控 proxy 环境,避免 `sudo` 的 `env_reset` 让 apt 绕过 gateway。Cargo 通过 proxy 下载 sparse index/crate 时固定关闭 HTTP multiplexing 并设置 `CARGO_NET_RETRY=10`;rustup bootstrap 与 toolchain 安装也执行有界重试,降低代理链瞬时 TLS EOF 对后端测试的影响,但不能用重试掩盖持续不可达。AI 原生壳 job 不能把 `$HOME/.cargo/bin` 或 setup-node toolcache 直接加入 `command.exec` 的受信任 PATH;应把完整 Node 发行目录复制到 `/usr/local/lib`,把 root-owned rustup proxy 安装到 `/usr/local/bin`,并通过 `RUSTUP_HOME` 只读挂载工具链。测试前先运行与应用一致的完整 bwrap canary,失败时停止测试,不允许跳过 sandbox 用例。 +workflow 的系统依赖步骤在 UID 0 时不再调用 sudo;非 root runner 必须使用 `sudo -E` 保留受控 proxy 环境,避免 `sudo` 的 `env_reset` 让 apt 绕过 gateway。Cargo 通过 proxy 下载 sparse index/crate 时固定关闭 HTTP multiplexing 并设置 `CARGO_NET_RETRY=10`;rustup bootstrap 与 toolchain 安装也执行有界重试,降低代理链瞬时 TLS EOF 对后端测试的影响,但不能用重试掩盖持续不可达。AI 原生壳 job 不能把 `$HOME/.cargo/bin` 或 setup-node toolcache 直接加入 `command.exec` 的受信任 PATH;应安装 `ripgrep`,把完整 Node 发行目录复制到 `/usr/local/lib`,把 root-owned rustup proxy 安装到 `/usr/local/bin`,并通过 `RUSTUP_HOME` 只读挂载工具链。测试前先运行与应用一致的完整 bwrap canary,失败时停止测试,不允许跳过 sandbox 用例。Tauri 的 1132 项级别测试 suite 共享 Agent Runtime 后台锁与异步终态,必须固定 `--test-threads=1`;并行 suite 偶发失败后逐个重跑通过,不能反过来证明并行全量门禁稳定。 站点 stack 仍由宿主 `/opt/gitea-stack` 管理,`.env`、runner 注册文件和数据库凭据不进入仓库。Compose 必须在 helper/container 内把该目录挂到同一个绝对路径再执行;若挂成 `/stack`,相对 bind source 会被 Docker daemon 误解析为宿主 `/stack/...` 并启动空数据目录。升级或 runner 迁移前先停止 Gitea 写入并保留 `data/gitea` 冷快照、`pg_dumpall`、compose/.env 与 runner config/.runner;本次可回滚快照位于 `backups/gitea-ci-migration-20260721-211018`。备份文件、绝对宿主配置和注册 token 不得提交 Git。 diff --git a/package.json b/package.json index 8c76257ba..de00aa64c 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "ai-game-creator-shell:agent-runtime:steer-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:steer-real-e2e --", "ai-game-creator-shell:agent-runtime:steer-runner-kill-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:steer-runner-kill-real-e2e --", "ai-game-creator-shell:typecheck": "npm --prefix apps/ai-game-creator-shell run typecheck", - "ai-game-creator-shell:check": "npm run ai-game-creator-shell:typecheck && npm run test -- apps/ai-game-creator-shell/tests && cargo test -p platform-llm --manifest-path server-rs/Cargo.toml && cargo test -p platform-agent --manifest-path server-rs/Cargo.toml game_creation && cargo test -p shared-contracts --manifest-path server-rs/Cargo.toml game_creation_app && cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml && npm run ai-game-creator-shell:agent-run:smoke", + "ai-game-creator-shell:check": "npm run ai-game-creator-shell:typecheck && npm run test -- apps/ai-game-creator-shell/tests && cargo test -p platform-llm --manifest-path server-rs/Cargo.toml && cargo test -p platform-agent --manifest-path server-rs/Cargo.toml game_creation && cargo test -p shared-contracts --manifest-path server-rs/Cargo.toml game_creation_app && cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --test-threads=1 && npm run ai-game-creator-shell:agent-run:smoke", "check:native-shells": "node scripts/check-native-shells.mjs" }, "dependencies": { From b6c85a521638a322f6fd84634f87424cedf32e47 Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 21 Jul 2026 22:56:17 +0800 Subject: [PATCH 09/14] =?UTF-8?q?=E7=A8=B3=E5=AE=9AAI=E5=8E=9F=E7=94=9F?= =?UTF-8?q?=E5=A3=B3=E5=BC=82=E6=AD=A5=E7=BB=88=E6=80=81=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 等待 Agent 终态和执行通道释放后再断言持久化副作用 使用一次性测试注入覆盖 CI root 环境下的对话写失败 补充异步 Runtime 与 CI 写失败的共享踩坑记录 --- .../src-tauri/src/project.rs | 17 ++ .../src-tauri/src/tests.rs | 149 +++++++++++------- docs/project-memory/shared-memory/pitfalls.md | 16 +- 3 files changed, 117 insertions(+), 65 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/project.rs b/apps/ai-game-creator-shell/src-tauri/src/project.rs index 298d8fa30..8deee436d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project.rs @@ -5488,6 +5488,8 @@ fn append_local_conversation_message_for_session_internal_at( return Err("finalization conversation 审计身份或角色无效".to_string()); } } + #[cfg(test)] + take_local_conversation_append_failure_injection(root, role)?; let record = PersistedLocalConversationMessageRecord { schema_version: LOCAL_CONVERSATION_SCHEMA_VERSION.to_string(), role: role.to_string(), @@ -5591,6 +5593,21 @@ fn append_local_conversation_message_for_session_internal_at( )) } +#[cfg(test)] +fn take_local_conversation_append_failure_injection(root: &Path, role: &str) -> Result<(), String> { + let path = root.join(".agent/runtime/test-fail-next-conversation-append"); + match fs::read_to_string(&path) { + Ok(expected_role) if expected_role.trim() == role => { + fs::remove_file(&path) + .map_err(|error| format!("清理对话写入测试失败注入标记失败:{error}"))?; + Err(format!("测试注入 {role} 对话写入失败")) + } + Ok(_) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(format!("读取对话写入测试失败注入标记失败:{error}")), + } +} + pub(crate) fn append_local_conversation_message_for_session_at( root: &Path, agent_id: Option<&str>, diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/tests.rs index 69e32c129..19e17c90d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -182,7 +182,14 @@ async fn agent_goal_edit_pause_resume_keeps_one_session_and_run_until_completion .send(final_tool_plan_response("持久 Goal 已在同一 run 完成。")) .expect("complete resumed Goal"); - let completed = wait_for_agent_runtime_idle(&root, "code-prototype"); + let completed = wait_for_agent_runtime_terminal_and_lane_release( + &root, + "code-prototype", + run_id, + "idle", + "completed", + ) + .state; assert_eq!(completed.phase, "completed"); assert_eq!(completed.run_id, run_id); let goal = read_game_creator_agent_goal_at(&root, "code-prototype", &session_id) @@ -1315,6 +1322,42 @@ fn wait_for_agent_runtime_idle(root: &Path, agent_id: &str) -> AgentRuntimeState runtime } +fn wait_for_agent_runtime_terminal_and_lane_release( + root: &Path, + agent_id: &str, + run_id: &str, + status: &str, + phase: &str, +) -> AgentRuntimeResult { + let mut result = read_game_creator_agent_runtime_at(root, agent_id) + .expect("read runtime while waiting for terminal lane release"); + for _ in 0..250 { + let matches_terminal = result.state.run_id == run_id + && result.state.status == status + && result.state.phase == phase; + if matches_terminal + && game_creator_agent_runtime_task_lock_is_available(root, agent_id) + .expect("probe runtime lane while waiting for terminal release") + { + let terminal = read_game_creator_agent_runtime_at(root, agent_id) + .expect("reread runtime after terminal lane release"); + if terminal.state.run_id == run_id + && terminal.state.status == status + && terminal.state.phase == phase + { + return terminal; + } + } + std::thread::sleep(Duration::from_millis(20)); + result = read_game_creator_agent_runtime_at(root, agent_id) + .expect("read runtime while waiting for terminal lane release"); + } + panic!( + "runtime did not reach {status}/{phase} for run {run_id} before the Agent lane released; last run={} status={} phase={}", + result.state.run_id, result.state.status, result.state.phase + ); +} + fn wait_for_agent_runtime_phase(root: &Path, agent_id: &str, phase: &str) -> AgentRuntimeState { let mut runtime = read_game_creator_agent_runtime_at(root, agent_id) .expect("read runtime while waiting for phase") @@ -17947,18 +17990,14 @@ async fn background_agent_runtime_marks_unconverged_loop_budget_exhausted() { .expect("planning request"); assert!(request.contains(&format!("第 {iteration} 轮"))); } - assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); - - let mut result = - read_game_creator_agent_runtime_at(&root, "design-director").expect("read budget runtime"); - for _ in 0..50 { - if result.state.status == "failed" { - break; - } - std::thread::sleep(Duration::from_millis(20)); - result = read_game_creator_agent_runtime_at(&root, "design-director") - .expect("read budget runtime"); - } + let result = wait_for_agent_runtime_terminal_and_lane_release( + &root, + "design-director", + "design-budget-exhausted-run", + "failed", + "budget-exhausted", + ); + assert!(receiver.try_recv().is_err()); assert_eq!(result.state.status, "failed"); assert_eq!(result.state.phase, "budget-exhausted"); assert!(result @@ -24233,18 +24272,11 @@ fn background_task_does_not_execute_when_user_message_cannot_persist() { init_local_game_project_at(&root, "project-1", "后台任务对话一致性测试").expect("project init"); let session_id = resolve_agent_conversation_session_id_at(&root, "design-director", None, true) .expect("resolve agent session"); - let (conversation_path, _, _) = - conversation_file_path_for_session(&root, Some("design-director"), Some(&session_id)) - .expect("resolve conversation path"); - fs::create_dir_all(conversation_path.parent().expect("conversation parent")) - .expect("create conversation parent"); - fs::write(&conversation_path, "").expect("create empty conversation file"); - let mut conversation_permissions = fs::metadata(&conversation_path) - .expect("read conversation permissions") - .permissions(); - conversation_permissions.set_readonly(true); - fs::set_permissions(&conversation_path, conversation_permissions) - .expect("make conversation read only"); + fs::write( + root.join(".agent/runtime/test-fail-next-background-conversation"), + b"fail once\n", + ) + .expect("arm one-shot background conversation failure"); let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock(&root, "design-director") .expect("acquire runtime lock") .expect("runtime lock available"); @@ -24268,14 +24300,18 @@ fn background_task_does_not_execute_when_user_message_cannot_persist() { .expect("failed queued task exists"); assert_eq!(task.status, "failed"); assert_eq!(task.phase, "conversation-write-failed"); + assert!(!root + .join(".agent/runtime/test-fail-next-background-conversation") + .exists()); + let conversation = + read_local_conversation_for_session_at(&root, Some("design-director"), Some(&session_id)) + .expect("read conversation after injected user write failure"); + assert!(conversation + .messages + .iter() + .all(|message| message.content != "这条任务必须先持久化对话")); drop(runtime_lock); - let mut conversation_permissions = fs::metadata(&conversation_path) - .expect("read final conversation permissions") - .permissions(); - conversation_permissions.set_readonly(false); - fs::set_permissions(&conversation_path, conversation_permissions) - .expect("restore conversation permissions"); fs::remove_dir_all(root).ok(); } @@ -24317,15 +24353,9 @@ async fn background_task_recovers_when_assistant_message_cannot_persist() { let session_id = resolve_agent_conversation_session_id_at(&root, "design-director", None, false) .expect("resolve agent session"); - let (conversation_path, _, _) = - conversation_file_path_for_session(&root, Some("design-director"), Some(&session_id)) - .expect("resolve conversation path"); - let mut conversation_permissions = fs::metadata(&conversation_path) - .expect("read conversation permissions") - .permissions(); - conversation_permissions.set_readonly(true); - fs::set_permissions(&conversation_path, conversation_permissions) - .expect("make conversation read only"); + let conversation_failure_path = root.join(".agent/runtime/test-fail-next-conversation-append"); + fs::write(&conversation_failure_path, b"assistant\n") + .expect("arm one-shot assistant conversation failure"); release_sender .send(()) .expect("release background planning response"); @@ -24341,16 +24371,10 @@ async fn background_task_recovers_when_assistant_message_cannot_persist() { .expect("read assistant persistence runtime"); } - let mut conversation_permissions = fs::metadata(&conversation_path) - .expect("read final conversation permissions") - .permissions(); - conversation_permissions.set_readonly(false); - fs::set_permissions(&conversation_path, conversation_permissions) - .expect("restore conversation permissions"); - assert_eq!(result.state.status, "running"); assert_eq!(result.state.phase, "finalizing"); assert!(result.state.error.is_some()); + assert!(!conversation_failure_path.exists()); assert!(result.recent_tasks.iter().any(|task| { task.run_id == "assistant-conversation-write-failure-run" && task.status == "running" @@ -24413,7 +24437,14 @@ async fn background_task_recovers_when_assistant_message_cannot_persist() { assert!(request_receiver .recv_timeout(Duration::from_millis(250)) .is_err()); - let completed = wait_for_agent_runtime_idle(&root, "design-director"); + let completed = wait_for_agent_runtime_terminal_and_lane_release( + &root, + "design-director", + "assistant-conversation-write-failure-run", + "idle", + "completed", + ) + .state; assert_eq!(completed.phase, "completed"); assert_eq!(completed.run_id, "assistant-conversation-write-failure-run"); assert_eq!( @@ -31263,6 +31294,7 @@ async fn background_finalization_replans_same_run_after_cross_agent_revision_dri assert!(current_final_reply_request.contains(run_id)); let runtime = wait_for_agent_runtime_idle(&root, "design-director"); + wait_for_provider_handoff_terminal_cleanup(&root, "design-director", run_id); assert_eq!(runtime.status, "idle"); assert_eq!(runtime.phase, "completed"); assert_eq!(runtime.run_id, run_id); @@ -62694,19 +62726,14 @@ fn project_supervisor_corrupt_child_evidence_enters_parent_reconciliation() { .expect("delivery remains present"); assert_eq!(persisted.status, StaticDelegateDeliveryStatus::Dispatched); assert_eq!(persisted.structured_result, None); - let mut parent_runtime = None; - for _ in 0..100 { - let current = - read_game_creator_agent_runtime_at(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID) - .expect("read reconciled Supervisor runtime") - .state; - if current.phase == "needs-reconciliation" { - parent_runtime = Some(current); - break; - } - std::thread::sleep(Duration::from_millis(10)); - } - let parent_runtime = parent_runtime.expect("busy parent lane eventually reconciles"); + let parent_runtime = wait_for_agent_runtime_terminal_and_lane_release( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + parent_run_id, + "failed", + "needs-reconciliation", + ) + .state; assert_eq!(parent_runtime.run_id, parent_run_id); assert_eq!(parent_runtime.status, "failed"); assert_eq!(parent_runtime.phase, "needs-reconciliation"); diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index faaf77cd0..35e7ef74d 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -32,12 +32,20 @@ ## 异步 Runtime 测试不能把 child idle 当成终态结果已发布 -- 现象:isolated child 已显示 idle,单次 all-join reconcile 却偶发返回空列表,完整 Rust suite 里出现低概率失败,单独重跑通常通过。 -- 原因:child Runtime 释放执行 lane 与持久化终态 result、发布 join readiness 不是同一个原子观测点;测试只等待 idle,会在终态 result 发布前抢先 reconcile。 -- 处理:产品协议仍以 durable terminal result 和 join readiness 为准。测试在有界时限内重复调用幂等 reconcile,直到取得唯一 join 或超时;不得靠固定长 sleep,也不能因为第一次为空就把协议改成吞掉未完成 child。 -- 验证:`isolated_agents_with_same_template_run_independently_and_join_once` 最多执行 100 次、每次间隔 20ms 的 reconcile,并继续断言只有一个 all-join 和一次父唤醒。 +- 现象:isolated child 已显示 idle,单次 all-join reconcile 却偶发返回空列表;或者 Runtime 已显示 completed / failed,Goal、conversation、Agent DB 审计和 per-Agent lock 仍未完成,完整 Rust suite 里出现低概率失败,单独重跑通常通过。 +- 原因:Runtime state、Goal sidecar、终态 result、conversation、审计记录、handoff 清理和执行 lane 释放不是同一个原子观测点;测试只等待 idle / failed 会在同一后台 drain 的 durable 收尾前抢先断言。 +- 处理:产品协议仍以 durable terminal result 和 join readiness 为准。测试在有界时限内等待业务目标终态;需要断言同一 drain 的后续副作用时,同时以 per-Agent runtime task lock 释放为 fence,命中后重新读取投影。join 场景继续重复调用幂等 reconcile,直到取得唯一 join 或超时;不得靠固定长 sleep,也不能因为第一次为空就把协议改成吞掉未完成 child。 +- 验证:`isolated_agents_with_same_template_run_independently_and_join_once` 最多执行 100 次、每次间隔 20ms 的 reconcile,并继续断言只有一个 all-join 和一次父唤醒;Goal、loop-budget、finalization 与 Supervisor reconciliation 测试必须在目标 status / phase 与 Agent lane 同时收束后再读取最终副作用。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/tests.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent.rs`。 +## CI root 环境不能用文件只读权限注入写失败 + +- 现象:本地测试把 conversation 文件设为 readonly 后能稳定得到写入失败,Gitea Actions 中同一断言却发现写入成功并继续执行任务。 +- 原因:隔离 job 内测试进程可能以 root 运行;root 不受普通 owner write bit 的同等限制,`set_readonly(true)` 不是跨 runner 身份的确定性故障注入。 +- 处理:需要覆盖写失败恢复时使用仅在 `cfg(test)` 生效、一次性消费并限定写入阶段的 marker;生产路径仍走真实持久化函数。测试同时断言 marker 已消费、失败前数据未落盘和恢复后 exactly-once,不依赖 chmod、固定 sleep 或 runner 用户身份。 +- 验证:在普通本地用户和 root 容器中分别运行用户消息、assistant 最终回复持久化失败测试,均应进入相同 durable phase 并通过恢复断言。 +- 关联:`apps/ai-game-creator-shell/src-tauri/src/project.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent.rs`、`apps/ai-game-creator-shell/src-tauri/src/tests.rs`。 + ## PTY 测试不能假设输入回显与后续输出必然分行 - 现象:PTY 环境隔离用例偶发得到 `你好BRIDGE_ENV:`,而不是独立的 `你好` 与 `BRIDGE_ENV:` 两行;真实私有环境变量并未泄漏,但整行相等断言失败。 From 017e9567242e58c62063baa9df1fe523db42a75f Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 21 Jul 2026 23:15:10 +0800 Subject: [PATCH 10/14] =?UTF-8?q?=E8=A1=A5=E9=BD=90AI=E5=8E=9F=E7=94=9FCI?= =?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 通过 Google 官方签名 APT 源安装 headless Chrome 保持真实 DOM 与 Canvas smoke 不降级 同步 Gitea CI 运维要求和共享踩坑记录 --- .gitea/workflows/project-ci.yml | 15 +++++++++++++++ .../shared-memory/development-workflow.md | 4 ++-- docs/project-memory/shared-memory/pitfalls.md | 8 ++++++++ ...开发运维】本地开发验证与生产运维-2026-05-15.md | 4 ++-- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/project-ci.yml b/.gitea/workflows/project-ci.yml index b9c7d8486..ff94fe2f6 100644 --- a/.gitea/workflows/project-ci.yml +++ b/.gitea/workflows/project-ci.yml @@ -327,6 +327,21 @@ jobs: pkg-config \ ripgrep \ wget + google_key="$(mktemp)" + curl --retry 3 --retry-all-errors --retry-delay 2 \ + --proto '=https' --tlsv1.2 -fsSL \ + https://dl.google.com/linux/linux_signing_key.pub \ + -o "${google_key}" + "${sudo_command[@]}" install -d -m 0755 /etc/apt/keyrings + "${sudo_command[@]}" install -m 0644 "${google_key}" \ + /etc/apt/keyrings/google-chrome.asc + printf '%s\n' \ + 'deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.asc] https://dl.google.com/linux/chrome/deb/ stable main' \ + | "${sudo_command[@]}" tee /etc/apt/sources.list.d/google-chrome.list >/dev/null + "${sudo_command[@]}" apt-get update + "${sudo_command[@]}" env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + google-chrome-stable + google-chrome --version bwrap_args=( --die-with-parent --unshare-all diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index e46e7d940..470883f4a 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -454,10 +454,10 @@ npm run check:server-rs-ddd - 仓库 CI 入口是 `.gitea/workflows/project-ci.yml`,向 `master`、`codex/ai-game-creator-app` 推送和所有 PR 创建、更新时必须运行,也允许手工触发。 - CI 固定拆分为 `Repository checks`、`Frontend tests`、`Backend tests`、`Native shell tests` 四个 required job;对应 PR context 完整名称是 `Project CI / Repository checks (pull_request)`、`Project CI / Frontend tests (pull_request)`、`Project CI / Backend tests (pull_request)`、`Project CI / Native shell tests (pull_request)`,首次运行后仍须从 Gitea 最近一周 context 表复核。测试使用独立 job,不能只藏在综合检查 step 中;原生壳验收单独运行以便定位重型构建失败。 -- 四个 job 共同覆盖 `npm run check`,并追加 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --all-targets --manifest-path server-rs/Cargo.toml` 和 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。后端 runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。`codex/ai-game-creator-app` 分支必须用独立 lockfile 安装 AI 游戏创作壳依赖,其原生壳入口还必须覆盖 `npm run ai-game-creator-shell:check`、release build smoke,并检查 AI Tauri `Cargo.lock` 不漂移。原生壳 job 还要安装 `ripgrep`,把 `actions/setup-node` 的完整 Node.js 22 发行目录与 root-owned rustup proxy 映射到 `/usr/local`,供只接受受信任系统命令目录的 `command.exec` 沙箱测试使用,不能放宽生产命令目录白名单。Tauri 的 1132 项级别 suite 固定 `--test-threads=1`,避免共享 Agent Runtime 后台锁与异步终态在 libtest 并行调度下互相干扰。 +- 四个 job 共同覆盖 `npm run check`,并追加 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --all-targets --manifest-path server-rs/Cargo.toml` 和 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。后端 runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。`codex/ai-game-creator-app` 分支必须用独立 lockfile 安装 AI 游戏创作壳依赖,其原生壳入口还必须覆盖 `npm run ai-game-creator-shell:check`、release build smoke,并检查 AI Tauri `Cargo.lock` 不漂移。原生壳 job 还要通过 Google 官方签名 APT 源安装 `google-chrome-stable`,用于 headless preview 的真实 DOM / canvas smoke;同时安装 `ripgrep`,把 `actions/setup-node` 的完整 Node.js 22 发行目录与 root-owned rustup proxy 映射到 `/usr/local`,供只接受受信任系统命令目录的 `command.exec` 沙箱测试使用,不能放宽生产命令目录白名单。Tauri 的 1132 项级别 suite 固定 `--test-threads=1`,避免共享 Agent Runtime 后台锁与异步终态在 libtest 并行调度下互相干扰。 - checkout 必须使用完整历史。PR 将 base SHA 写入 `SPACETIME_SCHEMA_BASE_REF`,直接推送 `master` 使用 before SHA;事件基线不可解析时直接失败。Gitea 检查的是 PR head 而非预合并 commit,workflow 必须拒绝不包含最新 base commit 的过期 PR,分支保护同时保持“PR 过期禁止合并”。 - 普通 PR job 不读取业务 secret,不运行真实 API/SpacetimeDB/OSS/支付/生成/live smoke,也不执行会修改外部状态的维护、迁移、发布或备份命令。 -- Gitea 至少升级到 `1.26.4` 后才能注册执行 PR job 的 runner;`ubuntu-latest` 标签只映射到固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像,不使用浮动镜像 tag,不映射 host,不向 job 暴露 Docker socket、业务 secret 或不必要内网。runner 能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm、Rust 分发和 crates.io;workflow 的官方 action 固定完整 commit,若内网禁用 GitHub,先在当前 Gitea 镜像对应 commit 并改用绝对 URL。受控镜像优先预装 rustup。Gitea 1.26 的任务超时由 runner 全局配置控制;首次运行成功后,`master` 分支保护必须要求上述四个 job 全部成功。 +- Gitea 至少升级到 `1.26.4` 后才能注册执行 PR job 的 runner;`ubuntu-latest` 标签只映射到固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像,不使用浮动镜像 tag,不映射 host,不向 job 暴露 Docker socket、业务 secret 或不必要内网。runner 能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm、Rust 分发、crates.io 和 Google Chrome 的 `dl.google.com` 官方签名 APT 源;workflow 的官方 action 固定完整 commit,若内网禁用 GitHub,先在当前 Gitea 镜像对应 commit 并改用绝对 URL。受控镜像优先预装 rustup。Gitea 1.26 的任务超时由 runner 全局配置控制;首次运行成功后,`master` 分支保护必须要求上述四个 job 全部成功。 - `genarrative-station` 当前使用 Gitea `1.26.4` + 基于 Runner `2.0.0-dind-rootless` 的固定 digest 修补镜像:只修复 `systempaths=unconfined` 的空 slice 被 `mergo` 丢失,真实 job 必须保持 `MaskedPaths=[]`、`ReadonlyPaths=[]`;外层仍非 privileged、无 `CAP_SYS_ADMIN`,内部 Docker 只监听 Unix socket,`docker_host: "-"` 阻止 socket 进入 job。job 只在 `gitea-actions` internal network,通过 `/git` reverse gateway 访问 Gitea,通过拒绝私网、保留地址和 metadata 的 80/443 proxy 访问公共依赖;直连公网和 Gitea 数据网必须失败。内层 bwrap 所需 namespace/proc 选项只能用于该 rootless DinD,不能放宽宿主 rootful runner。系统依赖步骤在 root job 中不调用 sudo,非 root 时用 `sudo -E` 保留受控 proxy;Cargo 关闭 HTTP multiplexing 并设置 10 次网络重试,rustup bootstrap/toolchain 安装也按有界次数重试。AI 原生壳 job 把 Node 发行目录与 rustup proxy 安装到 `/usr/local` 的受信任只读路径,并在测试前执行完整 bwrap canary。宿主 compose helper 必须把 `/opt/gitea-stack` 挂到同名绝对路径,避免相对 volume 错误落到 `/stack` 空目录。 ## 后端相关默认验证 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 35e7ef74d..c35e017d5 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -46,6 +46,14 @@ - 验证:在普通本地用户和 root 容器中分别运行用户消息、assistant 最终回复持久化失败测试,均应进入相同 durable phase 并通过恢复断言。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/project.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent.rs`、`apps/ai-game-creator-shell/src-tauri/src/tests.rs`。 +## Ubuntu 容器不能把 chromium-browser 的 Snap 占位包当成 CI 浏览器 + +- 现象:AI 游戏创作壳的 1132 条 Rust 测试全部通过,尾部 `agent-run:smoke` 却以 `spawn google-chrome ENOENT` 失败;直接给 Ubuntu 24.04 job 安装 `chromium-browser` 仍拿不到可执行浏览器。 +- 原因:Ubuntu 24.04 仓库里的 `chromium-browser` 是 Snap 过渡包,普通 Docker job 没有 snapd 宿主能力;固定 job image 也不预装 Google Chrome。脚本回退到命令名 `google-chrome` 后只能在本机通过,在干净 Runner 中必然 ENOENT。 +- 处理:Native job 通过 Google 官方签名 APT 源安装 `google-chrome-stable`,安装后先执行 `google-chrome --version`;smoke 继续真实启动 headless 浏览器验证 DOM / canvas,不允许因 CI 缺浏览器而跳过或降级为静态 HTTP 检查。 +- 验证:固定 Ubuntu 24.04 job image 内先确认 `apt-cache policy chromium-browser` 仅为 Snap 占位,再安装官方签名包并运行 `google-chrome --version`;Gitea Native job 最终必须在 1132 passed / 5 ignored 后继续通过 `agent-run:smoke`。 +- 关联:`.gitea/workflows/project-ci.yml`、`apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs`。 + ## PTY 测试不能假设输入回显与后续输出必然分行 - 现象:PTY 环境隔离用例偶发得到 `你好BRIDGE_ENV:`,而不是独立的 `你好` 与 `BRIDGE_ENV:` 两行;真实私有环境变量并未泄漏,但整行相等断言失败。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 2d258cc0e..cc1e1feb2 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -205,13 +205,13 @@ npm run check - `Repository checks`:执行 `npm run lint`、主站与后台生产构建、内容数据检查和提交差异空白检查。 - `Frontend tests`:先按根 lockfile 与 `apps/ai-game-creator-shell/package-lock.json` 分别执行 `npm ci`,再独立执行根 `npm run test`,让 Vitest 文件数和测试数在 Gitea job 列表中明确可见。 - `Backend tests`:执行 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast`、`api-server --all-targets` 编译和 `spacetime-module` 编译;runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。依赖真实服务或密钥的测试必须显式 `ignored`,不能让普通 PR job访问现场环境。 -- `Native shell tests`:按根 lockfile 与 AI 游戏创作壳独立 lockfile 安装依赖后执行 `npm run check:native-shells`,覆盖微信壳、Expo 和 Tauri 的完整验收,并确认桌面壳与 AI 游戏创作壳的 `Cargo.lock` 都没有被构建过程改写,避免把重型原生壳或依赖锁漂移隐藏在基础检查末尾。`codex/ai-game-creator-app` 分支的同名脚本还会执行 `npm run ai-game-creator-shell:check` 和 AI 游戏创作壳 release build smoke。该 job 会把 `actions/setup-node` 安装的 Node.js 22 与 npm 映射到 `/usr/local/bin`,满足 `command.exec` 仅信任系统命令目录的安全边界,不允许为适配 CI 放宽生产白名单。 +- `Native shell tests`:按根 lockfile 与 AI 游戏创作壳独立 lockfile 安装依赖后执行 `npm run check:native-shells`,覆盖微信壳、Expo 和 Tauri 的完整验收,并确认桌面壳与 AI 游戏创作壳的 `Cargo.lock` 都没有被构建过程改写,避免把重型原生壳或依赖锁漂移隐藏在基础检查末尾。`codex/ai-game-creator-app` 分支的同名脚本还会执行 `npm run ai-game-creator-shell:check` 和 AI 游戏创作壳 release build smoke。该 job 会通过 Google 官方签名 APT 源安装 `google-chrome-stable`,用于 headless preview 的真实 DOM / canvas smoke;同时把 `actions/setup-node` 安装的 Node.js 22 与 npm 映射到 `/usr/local/bin`,满足 `command.exec` 仅信任系统命令目录的安全边界,不允许为适配 CI 放宽生产白名单。 四个 job 合起来覆盖根 `npm run check`,并补齐根检查没有包含的 server-rs DDD、正式 workspace Rust 测试与现役后端编译门禁。普通 PR CI 不注入业务密钥,不启动真实 API、SpacetimeDB、OSS、支付、图片生成或生产 live smoke;需要现场环境、可变外部状态、Docker 编排或发布凭据的 `check:*` 继续按对应专题和 Jenkins 发布流程执行,不能遍历所有同名前缀脚本冒充 PR 门禁。 PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 `SPACETIME_SCHEMA_BASE_REF`。`check:spacetime-schema` 依赖该基线识别已有表字段删除、改名、重排和改类型;事件给出的基线缺失或本地不可解析时必须直接失败,不能退化为空差异检查。Gitea 的 PR checkout 是 PR head,不是与目标分支的预合并 commit,因此 workflow 还会验证 PR head 包含事件中的最新 base commit;分支保护必须继续开启“PR 过期禁止合并”,过期分支先更新再重跑。向 `master` 直接推送时使用 push before SHA,手工触发时回退到 `origin/master`。 -启用或注册执行 PR job 的 runner 前,Gitea 服务端必须至少升级到 `1.26.4`;不得在 `1.26.2` 上执行不受信任 PR 代码。runner 必须提供 `ubuntu-latest` 标签,并将其映射到经验证且固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像;禁止使用浮动镜像 tag,禁止将该标签映射到 host 执行器,禁止向 job 暴露 Docker socket、业务环境变量、业务密钥或不必要的内网。workflow 会安装 Node 22、仓库 `rust-toolchain.toml` 固定的 Rust 1.96.0,以及 clang/lld 和 Tauri Linux 依赖;受控 runner 镜像应预装 rustup,fallback 下载只用于首次引导。runner 仍需能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm registry、Rust 分发和 crates.io。workflow 中的 `actions/checkout` / `actions/setup-node` 固定到完整 commit;内网 runner 不允许访问 GitHub 时,先把对应 commit 镜像到当前 Gitea 并把 workflow 改为绝对 action URL。首版不使用 Actions cache,避免未配置 runner cache 网络时把缓存恢复错误变成 PR 失败。当前 Runner 2.0.0 已支持 job 级 `timeout-minutes`,但 runner 全局 `3h` 仍是所有任务的硬上限;若 workflow 以后新增更短 timeout,不能删除全局兜底。 +启用或注册执行 PR job 的 runner 前,Gitea 服务端必须至少升级到 `1.26.4`;不得在 `1.26.2` 上执行不受信任 PR 代码。runner 必须提供 `ubuntu-latest` 标签,并将其映射到经验证且固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像;禁止使用浮动镜像 tag,禁止将该标签映射到 host 执行器,禁止向 job 暴露 Docker socket、业务环境变量、业务密钥或不必要的内网。workflow 会安装 Node 22、仓库 `rust-toolchain.toml` 固定的 Rust 1.96.0,以及 clang/lld、Tauri Linux 依赖和 headless preview 使用的 Google Chrome;受控 runner 镜像应预装 rustup,fallback 下载只用于首次引导。runner 仍需能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm registry、Rust 分发、crates.io,以及 Google Chrome 的 `dl.google.com` 官方签名 APT 源。workflow 中的 `actions/checkout` / `actions/setup-node` 固定到完整 commit;内网 runner 不允许访问 GitHub 时,先把对应 commit 镜像到当前 Gitea 并把 workflow 改为绝对 action URL。首版不使用 Actions cache,避免未配置 runner cache 网络时把缓存恢复错误变成 PR 失败。当前 Runner 2.0.0 已支持 job 级 `timeout-minutes`,但 runner 全局 `3h` 仍是所有任务的硬上限;若 workflow 以后新增更短 timeout,不能删除全局兜底。 当前 `genarrative-station` 已于 2026-07-21 升级到 Gitea `1.26.4`,并使用基于 Gitea Runner `2.0.0-dind-rootless` 的固定 digest 修补镜像与固定 digest 的 Ubuntu 24.04 job image。Runner 2.0.0 会先把 `systempaths=unconfined` 解析为空 `MaskedPaths` / `ReadonlyPaths`,再被 `mergo.WithOverride` 当成 empty value 丢失;站点修补只在 merge 后保留这两个显式空 slice,不改其它 runner 行为。真实 job inspect 必须看到 `MaskedPaths=[]`、`ReadonlyPaths=[]`、`SecurityOpt=[seccomp=unconfined]`、`Privileged=false`、无 CapAdd 且 `Binds=[]`。外层 runner 以 `rootless` 用户运行,`privileged=false`、不增加 `CAP_SYS_ADMIN`,只映射 `/dev/net/tun`,内部 Docker 只监听私有 Unix socket;runner 配置必须保持 `docker_host: "-"`、`valid_volumes: []` 和 `bind_workdir: false`,防止内部 Docker socket 或宿主 bind mount 进入 job。job 只连接 `gitea-actions` internal network:`genarrative-station` 由只转发 `/git` 到 Gitea 的内部 gateway 解析,公网依赖只经拒绝私网、保留地址和 metadata 的 80/443 egress proxy;绕过 proxy 的公网和 Postgres/Redis 数据网都必须不可达。完整 bwrap canary 需要 rootless DinD 外层的 rootlesskit AppArmor/userns 边界,以及内层 job 的 namespace/proc 挂载支持;相关 `seccomp/systempaths` 放宽只允许存在于这个无宿主 socket 的 rootless DinD 内层,禁止复制回控制宿主 rootful Docker 的 runner。 From 084543621b84b8b44e4455de3f070d24288a639c Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 22 Jul 2026 14:58:36 +0800 Subject: [PATCH 11/14] =?UTF-8?q?=E4=BC=98=E5=8C=96AI=E6=B8=B8=E6=88=8F?= =?UTF-8?q?=E5=88=9B=E4=BD=9C=E5=88=86=E6=94=AFCI=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E5=87=86=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 四个CI任务复用预构建工具链与下载缓存 直接从Gitea checkout并增加npm和Git网络重试 保留AI壳独立依赖安装测试与双Cargo锁门禁 --- .gitea/workflows/project-ci.yml | 366 +++----------------------- scripts/check-gitea-ci-job-image.sh | 108 ++++++++ scripts/check-gitea-ci-job-runtime.sh | 39 +++ 3 files changed, 187 insertions(+), 326 deletions(-) create mode 100644 scripts/check-gitea-ci-job-image.sh create mode 100644 scripts/check-gitea-ci-job-runtime.sh diff --git a/.gitea/workflows/project-ci.yml b/.gitea/workflows/project-ci.yml index ff94fe2f6..304fd7c47 100644 --- a/.gitea/workflows/project-ci.yml +++ b/.gitea/workflows/project-ci.yml @@ -17,46 +17,30 @@ env: CARGO_HTTP_MULTIPLEXING: 'false' CARGO_NET_RETRY: '10' CARGO_TERM_COLOR: always - RUSTUP_MAX_RETRIES: '10' + NPM_CONFIG_AUDIT: 'false' + NPM_CONFIG_FETCH_RETRIES: '10' + NPM_CONFIG_FETCH_RETRY_FACTOR: '2' + NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT: '60000' + NPM_CONFIG_FETCH_RETRY_MINTIMEOUT: '2000' + NPM_CONFIG_FUND: 'false' + NPM_CONFIG_PREFER_OFFLINE: 'true' + RUSTUP_AUTO_INSTALL: '0' RUSTC_WRAPPER: '' CARGO_BUILD_RUSTC_WRAPPER: '' jobs: repository-checks: name: Repository checks - runs-on: ubuntu-latest + runs-on: genarrative-ci steps: - - name: Checkout full history - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - with: - fetch-depth: 0 - persist-credentials: false + - name: Checkout full history from Gitea + env: + GENARRATIVE_GITEA_FETCH_DEPTH: '0' + GENARRATIVE_GITEA_TOKEN: ${{ github.token }} + run: genarrative-gitea-checkout - - name: Install base tools - shell: bash - run: | - set -euo pipefail - command -v apt-get >/dev/null 2>&1 || { - echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2 - exit 1 - } - sudo_command=() - if [[ "$(id -u)" -ne 0 ]]; then - command -v sudo >/dev/null 2>&1 || { - echo 'non-root runner user requires sudo for system dependencies.' >&2 - exit 1 - } - sudo_command=(sudo -E) - fi - "${sudo_command[@]}" apt-get update - "${sudo_command[@]}" env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - ca-certificates \ - curl - - - name: Set up Node.js 22 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: '22' + - name: Validate preinstalled CI job image and sandbox + run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh - name: Resolve comparison base shell: bash @@ -82,42 +66,6 @@ jobs: fi echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}" - - name: Set up repository Rust toolchain - shell: bash - run: | - set -euo pipefail - if ! command -v rustup >/dev/null 2>&1; then - for attempt in $(seq 1 10); do - if curl --retry 3 --retry-all-errors --retry-delay 2 \ - --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --profile minimal --default-toolchain none; then - break - fi - if [[ "${attempt}" -eq 10 ]]; then - echo 'rustup bootstrap failed after 10 attempts.' >&2 - exit 1 - fi - sleep $((attempt * 2)) - done - fi - echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}" - export PATH="${HOME}/.cargo/bin:${PATH}" - toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" - test -n "${toolchain}" - for attempt in $(seq 1 10); do - if rustup toolchain install "${toolchain}" --profile minimal --component rustfmt; then - break - fi - if [[ "${attempt}" -eq 10 ]]; then - echo 'Rust toolchain installation failed after 10 attempts.' >&2 - exit 1 - fi - sleep $((attempt * 2)) - done - rustc --version - cargo --version - rustfmt --version - - name: Install npm dependencies run: npm ci @@ -141,17 +89,16 @@ jobs: frontend-tests: name: Frontend tests - runs-on: ubuntu-latest + runs-on: genarrative-ci steps: - - name: Checkout source - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - with: - persist-credentials: false + - name: Checkout source from Gitea + env: + GENARRATIVE_GITEA_FETCH_DEPTH: '1' + GENARRATIVE_GITEA_TOKEN: ${{ github.token }} + run: genarrative-gitea-checkout - - name: Set up Node.js 22 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: '22' + - name: Validate preinstalled CI job image and sandbox + run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh - name: Install npm dependencies run: npm ci @@ -164,48 +111,16 @@ jobs: backend-tests: name: Backend tests - runs-on: ubuntu-latest + runs-on: genarrative-ci steps: - - name: Checkout full history - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - with: - fetch-depth: 0 - persist-credentials: false + - name: Checkout full history from Gitea + env: + GENARRATIVE_GITEA_FETCH_DEPTH: '0' + GENARRATIVE_GITEA_TOKEN: ${{ github.token }} + run: genarrative-gitea-checkout - - name: Install backend build dependencies - shell: bash - run: | - set -euo pipefail - command -v apt-get >/dev/null 2>&1 || { - echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2 - exit 1 - } - sudo_command=() - if [[ "$(id -u)" -ne 0 ]]; then - command -v sudo >/dev/null 2>&1 || { - echo 'non-root runner user requires sudo for system dependencies.' >&2 - exit 1 - } - sudo_command=(sudo -E) - fi - "${sudo_command[@]}" apt-get update - "${sudo_command[@]}" env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - build-essential \ - ca-certificates \ - clang \ - cmake \ - curl \ - ffmpeg \ - libclang-dev \ - libcurl4-openssl-dev \ - libssl-dev \ - lld \ - pkg-config - - - name: Set up Node.js 22 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: '22' + - name: Validate preinstalled CI job image and sandbox + run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh - name: Resolve comparison base shell: bash @@ -231,42 +146,6 @@ jobs: fi echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}" - - name: Set up repository Rust toolchain - shell: bash - run: | - set -euo pipefail - if ! command -v rustup >/dev/null 2>&1; then - for attempt in $(seq 1 10); do - if curl --retry 3 --retry-all-errors --retry-delay 2 \ - --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --profile minimal --default-toolchain none; then - break - fi - if [[ "${attempt}" -eq 10 ]]; then - echo 'rustup bootstrap failed after 10 attempts.' >&2 - exit 1 - fi - sleep $((attempt * 2)) - done - fi - echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}" - export PATH="${HOME}/.cargo/bin:${PATH}" - toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" - test -n "${toolchain}" - for attempt in $(seq 1 10); do - if rustup toolchain install "${toolchain}" --profile minimal --component rustfmt; then - break - fi - if [[ "${attempt}" -eq 10 ]]; then - echo 'Rust toolchain installation failed after 10 attempts.' >&2 - exit 1 - fi - sleep $((attempt * 2)) - done - rustc --version - cargo --version - rustfmt --version - - name: Install npm dependencies run: npm ci @@ -284,181 +163,16 @@ jobs: native-shell-tests: name: Native shell tests - runs-on: ubuntu-latest + runs-on: genarrative-ci steps: - - name: Checkout full history - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - with: - fetch-depth: 0 - persist-credentials: false + - name: Checkout full history from Gitea + env: + GENARRATIVE_GITEA_FETCH_DEPTH: '0' + GENARRATIVE_GITEA_TOKEN: ${{ github.token }} + run: genarrative-gitea-checkout - - name: Install native shell build dependencies - shell: bash - run: | - set -euo pipefail - command -v apt-get >/dev/null 2>&1 || { - echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2 - exit 1 - } - sudo_command=() - if [[ "$(id -u)" -ne 0 ]]; then - command -v sudo >/dev/null 2>&1 || { - echo 'non-root runner user requires sudo for system dependencies.' >&2 - exit 1 - } - sudo_command=(sudo -E) - fi - "${sudo_command[@]}" apt-get update - "${sudo_command[@]}" env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - build-essential \ - bubblewrap \ - ca-certificates \ - clang \ - cmake \ - curl \ - file \ - libayatana-appindicator3-dev \ - libssl-dev \ - libwebkit2gtk-4.1-dev \ - libxdo-dev \ - librsvg2-dev \ - lld \ - patchelf \ - pkg-config \ - ripgrep \ - wget - google_key="$(mktemp)" - curl --retry 3 --retry-all-errors --retry-delay 2 \ - --proto '=https' --tlsv1.2 -fsSL \ - https://dl.google.com/linux/linux_signing_key.pub \ - -o "${google_key}" - "${sudo_command[@]}" install -d -m 0755 /etc/apt/keyrings - "${sudo_command[@]}" install -m 0644 "${google_key}" \ - /etc/apt/keyrings/google-chrome.asc - printf '%s\n' \ - 'deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.asc] https://dl.google.com/linux/chrome/deb/ stable main' \ - | "${sudo_command[@]}" tee /etc/apt/sources.list.d/google-chrome.list >/dev/null - "${sudo_command[@]}" apt-get update - "${sudo_command[@]}" env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - google-chrome-stable - google-chrome --version - bwrap_args=( - --die-with-parent - --unshare-all - --unshare-user - --disable-userns - --assert-userns-disabled - --cap-drop ALL - --clearenv - --ro-bind /usr /usr - ) - for merged_path in /bin /sbin /lib /lib64; do - if [[ -L "${merged_path}" ]]; then - bwrap_args+=(--symlink "$(readlink "${merged_path}")" "${merged_path}") - fi - done - bwrap_args+=( - --proc /proc - --dev /dev - --tmpfs /tmp - -- - /usr/bin/true - ) - bwrap "${bwrap_args[@]}" - - - name: Set up Node.js 22 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: '22' - - - name: Expose trusted Node.js command paths - shell: bash - run: | - set -euo pipefail - node_path="$(command -v node)" - npm_path="$(command -v npm)" - test -x "${node_path}" - test -x "${npm_path}" - node_root="$(cd "$(dirname "${node_path}")/.." && pwd -P)" - test -d "${node_root}/lib/node_modules/npm" - sudo_command=() - if [[ "$(id -u)" -ne 0 ]]; then - command -v sudo >/dev/null 2>&1 || { - echo 'non-root runner user requires sudo to expose trusted Node.js paths.' >&2 - exit 1 - } - sudo_command=(sudo -E) - fi - trusted_node_root='/usr/local/lib/genarrative-node' - "${sudo_command[@]}" install -d -m 0755 "${trusted_node_root}" - "${sudo_command[@]}" cp -a "${node_root}/." "${trusted_node_root}/" - "${sudo_command[@]}" ln -sfn "${trusted_node_root}/bin/node" /usr/local/bin/node - "${sudo_command[@]}" ln -sfn "${trusted_node_root}/bin/npm" /usr/local/bin/npm - /usr/local/bin/node --version - /usr/local/bin/npm --version - - - name: Set up repository Rust toolchain - shell: bash - run: | - set -euo pipefail - if ! command -v rustup >/dev/null 2>&1; then - for attempt in $(seq 1 10); do - if curl --retry 3 --retry-all-errors --retry-delay 2 \ - --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --profile minimal --default-toolchain none; then - break - fi - if [[ "${attempt}" -eq 10 ]]; then - echo 'rustup bootstrap failed after 10 attempts.' >&2 - exit 1 - fi - sleep $((attempt * 2)) - done - fi - echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}" - export PATH="${HOME}/.cargo/bin:${PATH}" - toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" - test -n "${toolchain}" - for attempt in $(seq 1 10); do - if rustup toolchain install "${toolchain}" --profile minimal --component rustfmt; then - break - fi - if [[ "${attempt}" -eq 10 ]]; then - echo 'Rust toolchain installation failed after 10 attempts.' >&2 - exit 1 - fi - sleep $((attempt * 2)) - done - rustc --version - cargo --version - rustfmt --version - - - name: Expose trusted Rust command paths - shell: bash - run: | - set -euo pipefail - rustup_path="$(command -v rustup)" - test -x "${rustup_path}" - rustup_home="$(rustup show home)" - test -d "${rustup_home}" - sudo_command=() - if [[ "$(id -u)" -ne 0 ]]; then - command -v sudo >/dev/null 2>&1 || { - echo 'non-root runner user requires sudo to expose trusted Rust paths.' >&2 - exit 1 - } - sudo_command=(sudo -E) - fi - if [[ "$(readlink -f "${rustup_path}")" != '/usr/local/bin/rustup' ]]; then - "${sudo_command[@]}" install -m 0755 "${rustup_path}" /usr/local/bin/rustup - fi - for command_name in cargo rustc rustdoc rustfmt; do - "${sudo_command[@]}" ln -sfn rustup "/usr/local/bin/${command_name}" - done - echo "RUSTUP_HOME=${rustup_home}" >> "${GITHUB_ENV}" - /usr/local/bin/cargo --version - /usr/local/bin/rustc --version - /usr/local/bin/rustfmt --version + - name: Validate preinstalled CI job image and sandbox + run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh - name: Install npm dependencies run: npm ci diff --git a/scripts/check-gitea-ci-job-image.sh b/scripts/check-gitea-ci-job-image.sh new file mode 100644 index 000000000..88753b32d --- /dev/null +++ b/scripts/check-gitea-ci-job-image.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +expected_toolchain="$( + sed -n 's/^channel = "\([^"]*\)"/\1/p' "${repo_root}/rust-toolchain.toml" +)" + +test -n "${expected_toolchain}" +[[ "$(node --version)" == v22.* ]] +rustup toolchain list | rg -q "^${expected_toolchain}(-[^ ]+)?( |$)" +[[ "$(rustup run "${expected_toolchain}" rustc --version)" == "rustc ${expected_toolchain} "* ]] +test "$(readlink -f "$(command -v node)")" = "/usr/local/lib/genarrative-node/bin/node" +test "$(rustup show home)" = "/usr/local/rustup" +for trusted_command in cargo rustc rustdoc rustfmt rustup; do + test "$(command -v "${trusted_command}")" = "/usr/local/bin/${trusted_command}" +done +test "$(command -v genarrative-gitea-checkout)" = "/usr/local/bin/genarrative-gitea-checkout" +bash -n /usr/local/bin/genarrative-gitea-checkout +test -d /root/.npm/_cacache +test -d /usr/local/cargo/registry/cache + +verify_cache_lock() { + local cache_name="$1" + local expected_sha256="$2" + local lock_path="$3" + local actual_sha256 + + test -n "${expected_sha256}" + actual_sha256="$(sha256sum "${lock_path}")" + actual_sha256="${actual_sha256%% *}" + if [[ "${actual_sha256}" == "${expected_sha256}" ]]; then + printf '%s_cache_lock=hit\n' "${cache_name}" + return + fi + if [[ "${GENARRATIVE_GITEA_CI_CHECK_RUNTIME:-0}" == '1' ]]; then + printf '%s_cache_lock=partial\n' "${cache_name}" + return + fi + echo "${cache_name} cache lock does not match the verification checkout." >&2 + exit 1 +} + +npm_lock_path="${repo_root}/package-lock.json" +server_rust_lock_path="${repo_root}/server-rs/Cargo.lock" +desktop_rust_lock_path="${repo_root}/apps/desktop-shell/src-tauri/Cargo.lock" +if [[ ! -f "${npm_lock_path}" ]]; then + npm_lock_path='/usr/local/share/genarrative-ci/npm/package-lock.json' +fi +if [[ ! -f "${server_rust_lock_path}" ]]; then + server_rust_lock_path='/usr/local/share/genarrative-ci/locks/server-rs.Cargo.lock' +fi +if [[ ! -f "${desktop_rust_lock_path}" ]]; then + desktop_rust_lock_path='/usr/local/share/genarrative-ci/locks/desktop-shell.Cargo.lock' +fi + +verify_cache_lock \ + npm \ + "${GENARRATIVE_GITEA_CI_NPM_LOCK_SHA256:-}" \ + "${npm_lock_path}" +verify_cache_lock \ + server_rust \ + "${GENARRATIVE_GITEA_CI_SERVER_RUST_LOCK_SHA256:-}" \ + "${server_rust_lock_path}" +verify_cache_lock \ + desktop_rust \ + "${GENARRATIVE_GITEA_CI_DESKTOP_RUST_LOCK_SHA256:-}" \ + "${desktop_rust_lock_path}" + +for command_name in \ + bwrap \ + cargo \ + clang \ + cmake \ + curl \ + ffmpeg \ + file \ + google-chrome \ + lld \ + npm \ + patchelf \ + pkg-config \ + rg \ + rustfmt \ + rustup \ + wget; do + command -v "${command_name}" >/dev/null +done + +pkg-config --exists \ + ayatana-appindicator3-0.1 \ + libcurl \ + openssl \ + webkit2gtk-4.1 + +node --version +npm --version +rustup run "${expected_toolchain}" rustc --version +rustup run "${expected_toolchain}" cargo --version +rustup run "${expected_toolchain}" rustfmt --version +google-chrome --version +bwrap --version +ffmpeg -version | head -n 1 + +if [[ "${GENARRATIVE_GITEA_CI_CHECK_RUNTIME:-0}" == '1' ]]; then + bash "${repo_root}/scripts/check-gitea-ci-job-runtime.sh" +fi diff --git a/scripts/check-gitea-ci-job-runtime.sh b/scripts/check-gitea-ci-job-runtime.sh new file mode 100644 index 000000000..1a10d033b --- /dev/null +++ b/scripts/check-gitea-ci-job-runtime.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -euo pipefail + +bwrap_args=( + --die-with-parent + --unshare-all + --unshare-user + --disable-userns + --assert-userns-disabled + --cap-drop ALL + --clearenv + --ro-bind /usr /usr +) +for merged_path in /bin /sbin /lib /lib64; do + if [[ -L "${merged_path}" ]]; then + bwrap_args+=(--symlink "$(readlink "${merged_path}")" "${merged_path}") + fi +done +bwrap_args+=( + --proc /proc + --dev /dev + --tmpfs /tmp + -- + /usr/bin/true +) +bwrap "${bwrap_args[@]}" + +chrome_output="$( + timeout 30 google-chrome \ + --headless=new \ + --no-sandbox \ + --disable-dev-shm-usage \ + --disable-gpu \ + --dump-dom \ + 'data:text/html,genarrative-ci' \ + 2>/dev/null +)" +rg -q 'genarrative-ci' <<< "${chrome_output}" From f92c1a6f4317f06fbe13551ac8de0e884a7d26f9 Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 22 Jul 2026 16:01:12 +0800 Subject: [PATCH 12/14] =?UTF-8?q?=E7=A8=B3=E5=AE=9AAI=E5=88=86=E6=94=AFCar?= =?UTF-8?q?go=E4=BE=9D=E8=B5=96=E5=87=86=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在后端测试前有界重试server-rs依赖fetch 在原生门禁前分别准备桌面壳和AI壳Rust依赖 保持测试与编译命令单次执行并继续暴露真实失败 --- .gitea/workflows/project-ci.yml | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.gitea/workflows/project-ci.yml b/.gitea/workflows/project-ci.yml index 304fd7c47..714491747 100644 --- a/.gitea/workflows/project-ci.yml +++ b/.gitea/workflows/project-ci.yml @@ -152,6 +152,23 @@ jobs: - name: Check server-rs boundaries run: npm run check:server-rs-ddd + - name: Prepare server-rs Rust dependencies + shell: bash + run: | + set -euo pipefail + for attempt in $(seq 1 5); do + if cargo fetch --locked \ + --target x86_64-unknown-linux-gnu \ + --manifest-path server-rs/Cargo.toml; then + break + fi + if [[ "${attempt}" -eq 5 ]]; then + echo 'server-rs Cargo dependency fetch failed after 5 attempts.' >&2 + exit 1 + fi + sleep $((attempt * 2)) + done + - name: Run server-rs workspace tests run: cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml @@ -180,6 +197,27 @@ jobs: - name: Install AI game creator dependencies run: npm ci --prefix apps/ai-game-creator-shell + - name: Prepare native Rust dependencies + shell: bash + run: | + set -euo pipefail + for manifest_path in \ + apps/desktop-shell/src-tauri/Cargo.toml \ + apps/ai-game-creator-shell/src-tauri/Cargo.toml; do + for attempt in $(seq 1 5); do + if cargo fetch --locked \ + --target x86_64-unknown-linux-gnu \ + --manifest-path "${manifest_path}"; then + break + fi + if [[ "${attempt}" -eq 5 ]]; then + echo "Cargo dependency fetch failed after 5 attempts: ${manifest_path}" >&2 + exit 1 + fi + sleep $((attempt * 2)) + done + done + - name: Run native shell gates run: npm run check:native-shells From 27f66bf0b18ba4827382dd9b5b07bcc0863388cc Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 22 Jul 2026 16:42:20 +0800 Subject: [PATCH 13/14] =?UTF-8?q?=E7=A8=B3=E5=AE=9AAI=E5=8E=9F=E7=94=9F?= =?UTF-8?q?=E5=A3=B3=E7=BB=88=E6=80=81=E5=AE=A1=E8=AE=A1=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 让最终回复失败测试等待 Runtime 终态与 Agent lane 完整释放 让并行后台任务测试在读取对话和审计前等待两个 Agent lane 释放 补充异步终态测试的具体回归用例说明 --- .../src-tauri/src/tests.rs | 63 +++++++------------ docs/project-memory/shared-memory/pitfalls.md | 2 +- 2 files changed, 25 insertions(+), 40 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/tests.rs index 19e17c90d..0bfff5e40 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -17764,18 +17764,14 @@ async fn background_agent_runtime_marks_response_plan_step_failed_when_final_rep ) .expect("start background task"); - let mut runtime = read_game_creator_agent_runtime_at(&root, "design-director") - .expect("read runtime") - .state; - for _ in 0..750 { - if runtime.status == "failed" { - break; - } - std::thread::sleep(Duration::from_millis(20)); - runtime = read_game_creator_agent_runtime_at(&root, "design-director") - .expect("read runtime") - .state; - } + let failed_result = wait_for_agent_runtime_terminal_and_lane_release( + &root, + "design-director", + "design-response-fail-run", + "failed", + "failed", + ); + let runtime = &failed_result.state; assert_eq!(runtime.status, "failed"); assert_eq!(runtime.phase, "failed"); @@ -17787,8 +17783,6 @@ async fn background_agent_runtime_marks_response_plan_step_failed_when_final_rep .detail .as_deref() .is_some_and(|detail| detail.contains("后台 Agent 最终回复调用 LLM 失败"))); - let failed_result = - read_game_creator_agent_runtime_at(&root, "design-director").expect("read failed events"); let event_types = failed_result .recent_events .iter() @@ -46074,24 +46068,22 @@ async fn background_agent_runtime_tasks_can_run_in_parallel_and_persist_replies( assert!(combined_requests.contains("后台准备主角规范图")); assert!(combined_requests.contains("后台整理玩法循环")); - let mut art_runtime = read_game_creator_agent_runtime_at(&root, "art-director") - .expect("read art runtime") - .state; - let mut design_runtime = read_game_creator_agent_runtime_at(&root, "design-director") - .expect("read design runtime") - .state; - for _ in 0..250 { - if art_runtime.status == "idle" && design_runtime.status == "idle" { - break; - } - std::thread::sleep(Duration::from_millis(20)); - art_runtime = read_game_creator_agent_runtime_at(&root, "art-director") - .expect("read art runtime") - .state; - design_runtime = read_game_creator_agent_runtime_at(&root, "design-director") - .expect("read design runtime") - .state; - } + let art_runtime_result = wait_for_agent_runtime_terminal_and_lane_release( + &root, + "art-director", + "art-background-run", + "idle", + "completed", + ); + let design_runtime_result = wait_for_agent_runtime_terminal_and_lane_release( + &root, + "design-director", + "design-background-run", + "idle", + "completed", + ); + let art_runtime = &art_runtime_result.state; + let design_runtime = &design_runtime_result.state; assert_eq!(art_runtime.status, "idle"); assert_eq!(art_runtime.phase, "completed"); @@ -46105,10 +46097,6 @@ async fn background_agent_runtime_tasks_can_run_in_parallel_and_persist_replies( design_runtime.last_response.as_deref(), Some("策划后台任务完成:先收敛核心循环。") ); - let art_runtime_result = - read_game_creator_agent_runtime_at(&root, "art-director").expect("art runtime result"); - let design_runtime_result = read_game_creator_agent_runtime_at(&root, "design-director") - .expect("design runtime result"); assert!(art_runtime_result .recent_tasks .iter() @@ -46171,9 +46159,6 @@ async fn background_agent_runtime_tasks_can_run_in_parallel_and_persist_replies( "assistant conversation must persist before Runtime completes for {agent_id}" ); } - drop(wait_to_acquire_agent_runtime_lock(&root, "art-director")); - drop(wait_to_acquire_agent_runtime_lock(&root, "design-director")); - fs::remove_dir_all(root).ok(); } diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index c35e017d5..28286a2bd 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -35,7 +35,7 @@ - 现象:isolated child 已显示 idle,单次 all-join reconcile 却偶发返回空列表;或者 Runtime 已显示 completed / failed,Goal、conversation、Agent DB 审计和 per-Agent lock 仍未完成,完整 Rust suite 里出现低概率失败,单独重跑通常通过。 - 原因:Runtime state、Goal sidecar、终态 result、conversation、审计记录、handoff 清理和执行 lane 释放不是同一个原子观测点;测试只等待 idle / failed 会在同一后台 drain 的 durable 收尾前抢先断言。 - 处理:产品协议仍以 durable terminal result 和 join readiness 为准。测试在有界时限内等待业务目标终态;需要断言同一 drain 的后续副作用时,同时以 per-Agent runtime task lock 释放为 fence,命中后重新读取投影。join 场景继续重复调用幂等 reconcile,直到取得唯一 join 或超时;不得靠固定长 sleep,也不能因为第一次为空就把协议改成吞掉未完成 child。 -- 验证:`isolated_agents_with_same_template_run_independently_and_join_once` 最多执行 100 次、每次间隔 20ms 的 reconcile,并继续断言只有一个 all-join 和一次父唤醒;Goal、loop-budget、finalization 与 Supervisor reconciliation 测试必须在目标 status / phase 与 Agent lane 同时收束后再读取最终副作用。 +- 验证:`isolated_agents_with_same_template_run_independently_and_join_once` 最多执行 100 次、每次间隔 20ms 的 reconcile,并继续断言只有一个 all-join 和一次父唤醒;Goal、loop-budget、finalization 与 Supervisor reconciliation 测试必须在目标 status / phase 与 Agent lane 同时收束后再读取最终副作用。`background_agent_runtime_marks_response_plan_step_failed_when_final_reply_fails` 和 `background_agent_runtime_tasks_can_run_in_parallel_and_persist_replies` 同样必须经过该 fence 后再断言 `turn.failed` 或 `agent.runtime.completed` 审计。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/tests.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent.rs`。 ## CI root 环境不能用文件只读权限注入写失败 From ff84b5a308f33dab45369248ad80188fed9b06dc Mon Sep 17 00:00:00 2001 From: menghao Date: Thu, 23 Jul 2026 10:50:02 +0800 Subject: [PATCH 14/14] =?UTF-8?q?=E4=BF=AE=E5=A4=8DAI=E6=B8=B8=E6=88=8F?= =?UTF-8?q?=E5=88=9B=E4=BD=9C=E5=A3=B3=E8=B7=A8=E5=B9=B3=E5=8F=B0=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=20(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 macOS 下 Unix 文件身份比较和临时目录测试兼容 隔离 AI 游戏创作本地数据库与发布身份并阻止旧 schema 降级启动 完善 Tauri 开发栈错误传播和 POSIX 子进程树清理 跳过 macOS 不支持的进程指标回调以消除周期告警 补充开发调度测试、技术方案和团队排障记忆 Reviewed-on: http://genarrative-station/git/GenarrativeAI/Genarrative/pulls/107 Co-authored-by: menghao Co-committed-by: menghao --- .../scripts/start-dev-stack.mjs | 321 +++++++++++++----- .../src-tauri/src/agent.rs | 11 + .../src-tauri/src/collaboration.rs | 2 +- .../src-tauri/src/command_exec.rs | 16 +- .../src-tauri/src/process_session.rs | 51 ++- .../src-tauri/src/project.rs | 8 +- .../src-tauri/src/runner.rs | 4 +- .../src-tauri/src/tests.rs | 119 +++++-- .../src-tauri/src/tool_plan_handoff.rs | 4 +- .../tests/start-dev-stack.test.ts | 178 ++++++++++ docs/project-memory/shared-memory/pitfalls.md | 26 +- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 4 + scripts/dev.mjs | 233 +++++++++---- scripts/dev.test.ts | 142 ++++++-- .../crates/api-server/src/process_metrics.rs | 6 + 15 files changed, 909 insertions(+), 216 deletions(-) create mode 100644 apps/ai-game-creator-shell/tests/start-dev-stack.test.ts diff --git a/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs b/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs index 60de90438..91bf6d1b2 100644 --- a/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs +++ b/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs @@ -11,9 +11,15 @@ const viteHost = '127.0.0.1'; const vitePort = 3080; const viteUrl = `http://${viteHost}:${vitePort}/`; const viteMarkerUrl = `${viteUrl}__agc_dev_server.json`; -const defaultApiTarget = process.env.RUST_SERVER_TARGET || 'http://127.0.0.1:8082'; +const defaultApiTarget = + process.env.RUST_SERVER_TARGET || 'http://127.0.0.1:8082'; const backendDatabase = 'genarrative-game-creator-dev'; +const backendSpacetimeDataDir = resolve( + repoRoot, + 'server-rs/.spacetimedb/ai-game-creator/data', +); const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; +const childLifecycles = new WeakMap(); function readJson(path) { if (!existsSync(path)) { @@ -53,44 +59,70 @@ function httpGetText(url, timeout = 1000) { async function isHttpReady(url) { const response = await httpGetText(url); - return Boolean(response && response.statusCode >= 200 && response.statusCode < 300); + return Boolean( + response && response.statusCode >= 200 && response.statusCode < 300, + ); } -function readBackendTargets({ requireAgcDatabase = false } = {}) { - const state = readJson(devStackStatePath); +function resolveBackendTargetsFromState( + state, + { + requireAgcBackend = false, + expectedDatabase = backendDatabase, + expectedSpacetimeDataDir = backendSpacetimeDataDir, + fallbackApiTarget = defaultApiTarget, + } = {}, +) { const apiServer = state?.services?.['api-server']; const spacetime = state?.services?.spacetime; const isActive = (service) => service && ['running', 'reused', 'starting'].includes(service.status ?? ''); const database = typeof state?.database === 'string' ? state.database : ''; - const hasMatchingDatabase = database === backendDatabase; - const canReuseState = !requireAgcDatabase || hasMatchingDatabase; + const spacetimeDataDir = + typeof state?.spacetimeDataDir === 'string' + ? resolve(state.spacetimeDataDir) + : ''; + const hasMatchingDatabase = database === expectedDatabase; + const hasMatchingDataDir = + Boolean(spacetimeDataDir) && + spacetimeDataDir === resolve(expectedSpacetimeDataDir); + const hasMatchingBackend = hasMatchingDatabase && hasMatchingDataDir; + const canReuseState = !requireAgcBackend || hasMatchingBackend; const apiUrl = canReuseState && isActive(apiServer) && apiServer.url ? apiServer.url - : requireAgcDatabase + : requireAgcBackend ? '' - : defaultApiTarget; + : fallbackApiTarget; const spacetimeUrl = canReuseState && isActive(spacetime) && spacetime.url ? spacetime.url - : requireAgcDatabase + : requireAgcBackend ? '' : 'http://127.0.0.1:3101'; return { apiUrl, spacetimeUrl, database, + spacetimeDataDir, hasMatchingDatabase, + hasMatchingDataDir, + hasMatchingBackend, }; } +function readBackendTargets({ requireAgcBackend = false } = {}) { + return resolveBackendTargetsFromState(readJson(devStackStatePath), { + requireAgcBackend, + }); +} + async function isBackendReady() { - const { apiUrl, spacetimeUrl, hasMatchingDatabase } = readBackendTargets({ - requireAgcDatabase: true, + const { apiUrl, spacetimeUrl, hasMatchingBackend } = readBackendTargets({ + requireAgcBackend: true, }); return ( - hasMatchingDatabase && + hasMatchingBackend && Boolean(apiUrl) && Boolean(spacetimeUrl) && (await isHttpReady(`${apiUrl}/healthz`)) && @@ -145,16 +177,90 @@ async function isExistingVitePairedWithBackend(apiTarget) { ); } -function spawnChild(command, args, options) { - return spawn(command, args, { +function spawnChild(command, args, options, spawnImpl = spawn) { + const useShell = process.platform === 'win32'; + const child = spawnImpl(command, args, { ...options, - shell: true, + shell: useShell, + // POSIX 下让每个长驻服务拥有独立进程组,退出时可以连同 npm、 + // Node、Cargo 及其子进程一起清理,避免残留订阅任务持续刷日志。 + detached: !useShell, stdio: 'inherit', }); + const lifecycle = { + failure: null, + promise: null, + // detached 子进程在 POSIX 下以自身 PID 作为 PGID。leader 退出后 + // child.pid 仍是清理其后代的唯一稳定句柄,必须随生命周期保留。 + processGroupId: !useShell && Number.isInteger(child.pid) ? child.pid : null, + }; + lifecycle.promise = new Promise((resolveLifecycle) => { + child.once('error', (error) => { + lifecycle.failure = { type: 'error', error }; + resolveLifecycle(lifecycle.failure); + }); + child.once('exit', (code, signal) => { + if (!lifecycle.failure) { + lifecycle.failure = { type: 'exit', code, signal }; + } + resolveLifecycle(lifecycle.failure); + }); + }); + childLifecycles.set(child, lifecycle); + return child; +} + +function readChildFailure(child) { + return childLifecycles.get(child)?.failure ?? null; +} + +function waitForChildTermination(child) { + const lifecycle = childLifecycles.get(child); + if (!lifecycle) { + return Promise.resolve({ + type: 'error', + error: new Error('子进程未注册生命周期监听'), + }); + } + return lifecycle.promise; +} + +function formatChildFailure(failure) { + if (failure?.type === 'error') { + return failure.error instanceof Error + ? failure.error.message + : String(failure.error); + } + return failure?.signal + ? `signal=${failure.signal}` + : `code=${failure?.code ?? 0}`; } function stopChild(child, signal = 'SIGTERM') { - if (!child || child.exitCode != null || child.signalCode != null) { + if (!child) { + return; + } + + if (process.platform !== 'win32') { + const processGroupId = childLifecycles.get(child)?.processGroupId; + if (Number.isInteger(processGroupId)) { + try { + process.kill(-processGroupId, signal); + return; + } catch (error) { + if (error?.code === 'ESRCH') { + return; + } + // leader 尚存活时保留 direct child fallback;leader 已退出则仍以 + // 负 PGID kill 的失败为准,不能误以为 descendants 已清理。 + if (child.exitCode != null || child.signalCode != null) { + return; + } + } + } + } + + if (child.exitCode != null || child.signalCode != null) { return; } try { @@ -166,47 +272,62 @@ function stopChild(child, signal = 'SIGTERM') { async function waitForBackendReady(backendChild, timeoutMs = 600_000) { const startedAt = Date.now(); - let backendExit = null; - backendChild?.on('exit', (code, signal) => { - backendExit = signal ? `signal=${signal}` : `code=${code ?? 0}`; - }); while (Date.now() - startedAt < timeoutMs) { if (await isBackendReady()) { return readBackendTargets(); } - if (backendExit) { - throw new Error(`配套后端启动失败: ${backendExit}`); + const failure = readChildFailure(backendChild); + if (failure) { + throw new Error(`配套后端启动失败: ${formatChildFailure(failure)}`); } - await new Promise((resolveWait) => setTimeout(resolveWait, 1000)); + await Promise.race([ + new Promise((resolveWait) => setTimeout(resolveWait, 1000)), + waitForChildTermination(backendChild), + ]); } throw new Error('等待配套后端和数据库启动超时'); } -async function ensureBackend() { - if (await isBackendReady()) { - const targets = readBackendTargets(); +async function ensureBackend({ + onBackendChild = () => {}, + checkBackendReady = isBackendReady, + resolveTargets = readBackendTargets, + spawnBackend = () => + spawnChild( + npm, + [ + '--prefix', + '../..', + 'run', + 'agc:backend', + '--', + '--database', + backendDatabase, + '--spacetime-data-dir', + backendSpacetimeDataDir, + '--no-interactive', + ], + { cwd: appRoot }, + ), + waitUntilReady = waitForBackendReady, +} = {}) { + if (await checkBackendReady()) { + const targets = resolveTargets(); console.log(`[ai-game-creator-shell] reuse backend ${targets.apiUrl}`); return { backendChild: null, targets }; } console.log('[ai-game-creator-shell] starting backend stack'); - const backendChild = spawnChild( - npm, - [ - '--prefix', - '../..', - 'run', - 'agc:backend', - '--', - '--database', - backendDatabase, - '--no-interactive', - ], - { cwd: appRoot }, - ); - const targets = await waitForBackendReady(backendChild); - console.log(`[ai-game-creator-shell] backend ready ${targets.apiUrl}`); - return { backendChild, targets }; + const backendChild = spawnBackend(); + try { + onBackendChild(backendChild); + const targets = await waitUntilReady(backendChild); + console.log(`[ai-game-creator-shell] backend ready ${targets.apiUrl}`); + return { backendChild, targets }; + } catch (error) { + stopChild(backendChild); + throw error; + } } async function startVite(apiTarget) { @@ -224,7 +345,9 @@ async function startVite(apiTarget) { (await isExistingVitePairedWithBackend(apiTarget)) && (await isExistingViteProxyReady()) ) { - console.log(`[ai-game-creator-shell] reuse existing Vite dev server ${viteUrl}`); + console.log( + `[ai-game-creator-shell] reuse existing Vite dev server ${viteUrl}`, + ); return null; } if (isAiGameCreatorServer(existing)) { @@ -244,40 +367,86 @@ async function startVite(apiTarget) { ); } -let backendChild = null; -let viteChild = null; +async function main() { + let backendChild = null; + let viteChild = null; + let shutdownSignal = ''; + const signalHandlers = new Map(); -for (const signal of ['SIGINT', 'SIGTERM']) { - process.on(signal, () => { - stopChild(viteChild, signal); - stopChild(backendChild, signal); - }); -} - -try { - const backend = await ensureBackend(); - backendChild = backend.backendChild; - viteChild = await startVite(backend.targets.apiUrl); - - const children = [backendChild, viteChild].filter(Boolean); - if (children.length === 0) { - process.exit(0); + for (const signal of ['SIGINT', 'SIGTERM']) { + const handler = () => { + shutdownSignal = signal; + stopChild(viteChild, signal); + stopChild(backendChild, signal); + }; + signalHandlers.set(signal, handler); + process.on(signal, handler); } - await new Promise((resolveExit) => { - for (const child of children) { - child.on('exit', (code, signal) => { - stopChild(viteChild); - stopChild(backendChild); - resolveExit(signal ? 1 : code ?? 0); - }); + try { + const backend = await ensureBackend({ + onBackendChild(child) { + backendChild = child; + if (shutdownSignal) { + stopChild(child, shutdownSignal); + } + }, + }); + backendChild = backend.backendChild; + if (shutdownSignal) { + throw new Error(`启动期收到 ${shutdownSignal},已停止配套后端`); } - }).then((code) => process.exit(code)); -} catch (error) { - stopChild(viteChild); - stopChild(backendChild); - console.error( - `[ai-game-creator-shell] ${error instanceof Error ? error.message : String(error)}`, - ); - process.exit(1); + + viteChild = await startVite(backend.targets.apiUrl); + if (shutdownSignal) { + stopChild(viteChild, shutdownSignal); + throw new Error(`启动期收到 ${shutdownSignal},已停止前端服务`); + } + + const children = [backendChild, viteChild].filter(Boolean); + if (children.length === 0) { + return 0; + } + + const failure = await Promise.race( + children.map((child) => waitForChildTermination(child)), + ); + stopChild(viteChild); + stopChild(backendChild); + return failure.type === 'error' || failure.signal ? 1 : (failure.code ?? 0); + } catch (error) { + stopChild(viteChild); + stopChild(backendChild); + console.error( + `[ai-game-creator-shell] ${error instanceof Error ? error.message : String(error)}`, + ); + return 1; + } finally { + for (const [signal, handler] of signalHandlers) { + process.off(signal, handler); + } + } +} + +function isDirectModuleExecution() { + return Boolean( + process.argv[1] && + resolve(process.argv[1]) === fileURLToPath(import.meta.url), + ); +} + +export { + ensureBackend, + formatChildFailure, + isDirectModuleExecution, + readChildFailure, + resolveBackendTargetsFromState, + spawnChild, + stopChild, + waitForBackendReady, + waitForChildTermination, +}; + +if (isDirectModuleExecution()) { + process.exitCode = await main(); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index 6d3bb9b6d..aa8b447b8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -42605,6 +42605,9 @@ pub(crate) struct AgentRuntimeTaskLock { file: Option, } +#[cfg(unix)] +static AGENT_RUNTIME_LOCK_OPEN_GUARD: OnceLock> = OnceLock::new(); + impl Drop for AgentRuntimeTaskLock { fn drop(&mut self) { self.file.take(); @@ -42768,6 +42771,14 @@ fn try_open_game_creator_agent_runtime_task_lock_file( use std::os::fd::{AsRawFd, FromRawFd}; use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; + // macOS 上两个线程首次并发创建同一套 mkdirat/openat 锁目录时,loser + // 可能在最终 O_CREAT 前短暂观察到 ENOENT。进程内只串行化安全打开阶段; + // 返回后的 flock 仍负责真实的跨线程、跨进程互斥。 + let _open_guard = AGENT_RUNTIME_LOCK_OPEN_GUARD + .get_or_init(|| Mutex::new(())) + .lock() + .map_err(|_| "Agent Runtime 锁安全打开门禁已损坏".to_string())?; + validate_project_root(root)?; let relative_path = normalize_relative_path(relative_path)?; let path = root.join(&relative_path); diff --git a/apps/ai-game-creator-shell/src-tauri/src/collaboration.rs b/apps/ai-game-creator-shell/src-tauri/src/collaboration.rs index d01d9626b..b8c68d893 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/collaboration.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/collaboration.rs @@ -568,7 +568,7 @@ pub(crate) fn bind_supervisor_collaboration_policy_snapshot_at( &lock_id, "collaboration-policy-snapshot", )? - .ok_or_else(|| "Project Supervisor 协作策略快照正被其他进程绑定".to_string())?; + .ok_or_else(|| "Project Supervisor 协作策略快照并发绑定冲突:正被其他进程绑定".to_string())?; let existing_binding = read_supervisor_collaboration_policy_snapshot_binding_at( root, parent_agent_id, diff --git a/apps/ai-game-creator-shell/src-tauri/src/command_exec.rs b/apps/ai-game-creator-shell/src-tauri/src/command_exec.rs index a34566216..936d1befb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/command_exec.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/command_exec.rs @@ -832,9 +832,10 @@ fn validate_npm_arguments(arguments: &[String]) -> Result<(), String> { } if subcommand == "run" && arguments - .get(1) + .iter() + .skip(1) .map(String::as_str) - .filter(|value| !value.starts_with('-')) + .find(|value| !value.starts_with('-')) .is_none() { return Err("command.exec npm run 缺少脚本名".to_string()); @@ -2608,6 +2609,17 @@ raise SystemExit(code)' validate_npm_arguments(&command_args(&["run", "test:unit", "--", "sample.test",])) .is_ok() ); + assert!(validate_npm_arguments(&command_args(&[ + "run", + "--silent", + "--ignore-scripts", + "test:unit", + ])) + .is_ok()); + assert!( + validate_npm_arguments(&command_args(&["run", "--silent", "--ignore-scripts",])) + .is_err() + ); } #[tokio::test] diff --git a/apps/ai-game-creator-shell/src-tauri/src/process_session.rs b/apps/ai-game-creator-shell/src-tauri/src/process_session.rs index 726f8f9be..213bb76a0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/process_session.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/process_session.rs @@ -2125,6 +2125,12 @@ fn drain_process_session_output( } } if output_limit { + if let Ok(mut output) = live.output.lock() { + output.output_limit_exceeded = true; + output.status = "output-limit-exceeded".to_string(); + output.stdin_open = false; + live.output_changed.notify_all(); + } let _ = live.control.send(ProcessControl::OutputLimit); break; } @@ -3115,7 +3121,7 @@ mod tests { PROCESS_SESSION_TEST_LOCK .get_or_init(|| Mutex::new(())) .lock() - .expect("process session test lock") + .unwrap_or_else(std::sync::PoisonError::into_inner) } fn process_identity(project_id: &str) -> ProcessSessionIdentity { @@ -3130,6 +3136,22 @@ mod tests { } } + fn process_test_command_spec(root: &Path) -> ProjectCommandSpec { + fs::write( + root.join("package.json"), + r#"{"scripts":{"dev":"node fixture.js"}}"#, + ) + .expect("write process test package.json"); + resolve_project_command_spec_at( + root, + "npm", + &["run".to_string(), "dev".to_string()], + ".", + 30, + ) + .expect("resolve process test command") + } + #[test] fn process_session_cursor_preserves_unicode_boundaries() { let process_id = "proc-0123456789abcdef0123456789abcdef"; @@ -3239,8 +3261,7 @@ mod tests { &identity, &process_id, &format!("cmd-legacy-active-{index}"), - &resolve_project_command_spec_at(root, "bash", &["--version".to_string()], ".", 30) - .expect("resolve command"), + &process_test_command_spec(root), None, &"a".repeat(64), "running", @@ -3285,8 +3306,7 @@ mod tests { &identity, &process_id, &format!("cmd-legacy-terminal-{index}"), - &resolve_project_command_spec_at(root, "bash", &["--version".to_string()], ".", 30) - .expect("resolve command"), + &process_test_command_spec(root), None, &"b".repeat(64), "exited", @@ -3354,9 +3374,7 @@ mod tests { .expect("initialize project"); let identity = process_identity("v3-validation-project"); let process_id = "proc-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; - let spec = - resolve_project_command_spec_at(root, "bash", &["--version".to_string()], ".", 30) - .expect("resolve command"); + let spec = process_test_command_spec(root); let mut record = initial_process_session_record( &identity, process_id, @@ -4468,13 +4486,20 @@ process.stdin.resume(); let root = directory.path(); init_local_game_project_at(root, "stdin-race-project", "Stdin Race Project") .expect("initialize project"); + fs::write( + root.join("package.json"), + r#"{"scripts":{"dev":"node fixture.js"}}"#, + ) + .expect("write package.json"); + fs::write( + root.join("fixture.js"), + "process.stdout.write('READY\\n'); setInterval(() => {}, 1000);\n", + ) + .expect("write fixture"); let spec = resolve_project_command_spec_at( root, - "bash", - &[ - "-lc".to_string(), - "printf 'READY\\n'; while :; do sleep 1; done".to_string(), - ], + "npm", + &["run".to_string(), "dev".to_string()], ".", 30, ) diff --git a/apps/ai-game-creator-shell/src-tauri/src/project.rs b/apps/ai-game-creator-shell/src-tauri/src/project.rs index 8deee436d..e8c8f34c5 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project.rs @@ -336,8 +336,8 @@ fn verify_unix_agent_db_root(root: &Path, opened: &File) -> Result<(), String> { let metadata = opened .metadata() .map_err(|error| format!("复核 Agent DB 项目目录句柄失败:{error}"))?; - if stat.st_dev != metadata.dev() - || stat.st_ino != metadata.ino() + if stat.st_dev as u64 != metadata.dev() + || stat.st_ino as u64 != metadata.ino() || stat.st_mode & libc::S_IFMT != libc::S_IFDIR { return Err("Agent DB 项目目录在安全打开期间发生替换或不是普通目录".to_string()); @@ -380,8 +380,8 @@ fn verify_unix_agent_db_entry( } else { libc::S_IFREG }; - if stat.st_dev != metadata.dev() - || stat.st_ino != metadata.ino() + if stat.st_dev as u64 != metadata.dev() + || stat.st_ino as u64 != metadata.ino() || stat.st_mode & libc::S_IFMT != expected_type { return Err(format!("{label}在安全打开期间发生替换")); diff --git a/apps/ai-game-creator-shell/src-tauri/src/runner.rs b/apps/ai-game-creator-shell/src-tauri/src/runner.rs index 0b16b09be..6dd33fb4b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/runner.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/runner.rs @@ -1200,8 +1200,8 @@ fn verify_unix_project_owner_entry( } else { libc::S_IFREG }; - if stat.st_dev != opened_metadata.dev() - || stat.st_ino != opened_metadata.ino() + if stat.st_dev as u64 != opened_metadata.dev() + || stat.st_ino as u64 != opened_metadata.ino() || stat.st_mode & libc::S_IFMT != expected_type { return Err(format!("{label} 在安全打开期间发生替换")); diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/tests.rs index 0bfff5e40..a7658bf37 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -820,9 +820,13 @@ async fn agent_goal_paused_edit_replans_old_confirmation_in_same_run() { .send(final_tool_plan_response("已按新目标收束,未执行旧动作。")) .expect("complete edited Goal"); - let completed = wait_for_agent_runtime_idle(&root, "code-prototype"); - assert_eq!(completed.phase, "completed"); - assert_eq!(completed.run_id, run_id); + wait_for_agent_runtime_terminal_and_lane_release( + &root, + "code-prototype", + run_id, + "idle", + "completed", + ); assert!(!root.join("game/paused-edit-stale.txt").exists()); assert_eq!( read_game_creator_agent_goal_at(&root, "code-prototype", &session_id) @@ -1286,7 +1290,10 @@ fn unique_project_path() -> PathBuf { .expect("system clock should be after epoch") .as_millis(); let counter = TEST_PROJECT_COUNTER.fetch_add(1, Ordering::Relaxed); - std::env::temp_dir().join(format!( + let temp_root = std::env::temp_dir() + .canonicalize() + .unwrap_or_else(|_| std::env::temp_dir()); + temp_root.join(format!( "genarrative-ai-game-creator-test-{}-{millis}-{counter}", std::process::id() )) @@ -1322,6 +1329,25 @@ fn wait_for_agent_runtime_idle(root: &Path, agent_id: &str) -> AgentRuntimeState runtime } +async fn wait_for_captured_mock_request( + receiver: &mpsc::Receiver, + description: &str, +) -> String { + let deadline = std::time::Instant::now() + Duration::from_secs(10); + loop { + match receiver.try_recv() { + Ok(request) => return request, + Err(mpsc::TryRecvError::Empty) if std::time::Instant::now() < deadline => { + tokio::time::sleep(Duration::from_millis(20)).await; + } + Err(mpsc::TryRecvError::Empty) => panic!("{description}: Timeout"), + Err(mpsc::TryRecvError::Disconnected) => { + panic!("{description}: capture channel disconnected") + } + } + } +} + fn wait_for_agent_runtime_terminal_and_lane_release( root: &Path, agent_id: &str, @@ -12783,10 +12809,13 @@ fn supervisor_collaboration_policy_snapshot_concurrent_conflict_has_one_winner() .collect::>(); assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1); assert_eq!(results.iter().filter(|result| result.is_err()).count(), 1); - assert!(results - .iter() - .filter_map(|result| result.as_ref().err()) - .all(|error| error.contains("冲突"))); + assert!( + results + .iter() + .filter_map(|result| result.as_ref().err()) + .all(|error| error.contains("冲突")), + "unexpected concurrent binding results: {results:?}" + ); let snapshot = read_supervisor_collaboration_policy_snapshot_for_test(&root, run_id); assert!(policies.contains(&snapshot.policy)); let (primary, previous) = @@ -17334,7 +17363,14 @@ async fn response_stream_disabled_keeps_direct_planning_reply_to_one_request() { run_id, ) .expect("start direct planning response task"); - let completed = wait_for_agent_runtime_idle(&root, "design-director"); + let completed = wait_for_agent_runtime_terminal_and_lane_release( + &root, + "design-director", + run_id, + "idle", + "completed", + ) + .state; assert_eq!(completed.status, "idle"); assert_eq!(completed.phase, "completed"); assert_eq!(completed.last_response.as_deref(), Some(direct_response)); @@ -20350,26 +20386,29 @@ async fn background_agent_runtime_reports_and_truncates_excess_tool_actions() { "design-tool-budget-run", ) .expect("start background task"); - receiver - .recv_timeout(Duration::from_secs(2)) - .expect("first planning request"); - let second_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("second planning request"); + wait_for_captured_mock_request(&receiver, "first planning request").await; + let second_request = wait_for_captured_mock_request(&receiver, "second planning request").await; assert!(second_request.contains("runtime.tool_budget")); assert!(second_request.contains("本轮请求了 4 个工具动作,只执行前 3 个")); assert!(second_request.contains("project.index")); assert!(second_request.contains("task.list")); assert!(second_request.contains("asset.list")); - let completed = wait_for_agent_runtime_idle(&root, "design-director"); - assert_eq!(completed.phase, "completed"); + let completed = wait_for_agent_runtime_terminal_and_lane_release( + &root, + "design-director", + "design-tool-budget-run", + "idle", + "completed", + ); assert!(completed + .state .observations .iter() .any(|item| item.contains("本轮请求了 4 个工具动作,只执行前 3 个"))); - assert_eq!(completed.recent_tool_calls.len(), 3); + assert_eq!(completed.state.recent_tool_calls.len(), 3); assert!(completed + .state .recent_tool_calls .iter() .all(|action| action.tool != "memory.read")); @@ -36908,10 +36947,16 @@ async fn provider_retry_waiting_tool_plan_resumes_only_after_due_and_cleans_side assert_eq!(waiting.status, "running"); assert_eq!(waiting.run_id, run_id); assert_eq!(waiting.session_id, started.state.session_id); - assert!( - game_creator_agent_runtime_task_lock_is_available(&root, "design-director") - .expect("probe released Agent lane") - ); + let mut lane_released = false; + for _ in 0..250 { + lane_released = game_creator_agent_runtime_task_lock_is_available(&root, "design-director") + .expect("probe released Agent lane"); + if lane_released { + break; + } + std::thread::sleep(Duration::from_millis(20)); + } + assert!(lane_released, "Provider retry 等待投影后 Agent lane 应释放"); let retry = crate::provider_retry::read_for_run_at(&root, "design-director", run_id) .expect("read persisted Provider retry") .expect("persisted Provider retry exists"); @@ -62320,13 +62365,18 @@ async fn project_supervisor_parent_wake_is_singleflight_and_projects_structural_ .error .as_deref() .is_some_and(|error| error.contains("委派屏障"))); - let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("read agent db"); - assert_eq!( - agent_db + let deadline = std::time::Instant::now() + Duration::from_secs(3); + let audit_count = loop { + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("read agent db"); + let count = agent_db .matches("agent.runtime.agent.delegate_parent_wake.needs_reconciliation") - .count(), - 1 - ); + .count(); + if count > 0 || std::time::Instant::now() >= deadline { + break count; + } + tokio::time::sleep(Duration::from_millis(20)).await; + }; + assert_eq!(audit_count, 1); fs::remove_dir_all(root).ok(); } @@ -63039,7 +63089,9 @@ async fn project_supervisor_resume_replays_executing_run_status_observation() { "apiKey": "project-supervisor-resume-key", "baseUrl": {base_url:?}, "model": "project-supervisor-resume-model", - "apiKind": "openai_responses" + "apiKind": "openai_responses", + "maxRetries": 1, + "retryBackoffMs": 100 }} }} }}"# @@ -63164,7 +63216,14 @@ async fn project_supervisor_resume_replays_executing_run_status_observation() { "ok", ); - let completed = wait_for_agent_runtime_idle(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID); + let completed = wait_for_agent_runtime_terminal_and_lane_release( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + parent_run_id, + "idle", + "completed", + ) + .state; assert_eq!(completed.phase, "completed"); assert_eq!( completed.last_response.as_deref(), diff --git a/apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff.rs b/apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff.rs index 866c761fa..2540b4de3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff.rs @@ -1673,8 +1673,8 @@ fn verify_unix_tool_plan_entry( } else { libc::S_IFREG }; - if stat.st_dev != opened_metadata.dev() - || stat.st_ino != opened_metadata.ino() + if stat.st_dev as u64 != opened_metadata.dev() + || stat.st_ino as u64 != opened_metadata.ino() || stat.st_mode & libc::S_IFMT != expected_type { return Err(format!("{label} 在安全扫描期间发生替换")); diff --git a/apps/ai-game-creator-shell/tests/start-dev-stack.test.ts b/apps/ai-game-creator-shell/tests/start-dev-stack.test.ts new file mode 100644 index 000000000..0a6b36918 --- /dev/null +++ b/apps/ai-game-creator-shell/tests/start-dev-stack.test.ts @@ -0,0 +1,178 @@ +import { EventEmitter } from 'node:events'; +import { existsSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; + +import { describe, expect, test, vi } from 'vitest'; + +import { + ensureBackend, + resolveBackendTargetsFromState, + spawnChild, + stopChild, + waitForChildTermination, +} from '../scripts/start-dev-stack.mjs'; + +const expectedDatabase = 'genarrative-game-creator-dev'; +const expectedDataDir = resolve('server-rs/.spacetimedb/ai-game-creator/data'); + +function backendState(spacetimeDataDir?: string) { + return { + schemaVersion: spacetimeDataDir ? 2 : 1, + database: expectedDatabase, + ...(spacetimeDataDir ? { spacetimeDataDir } : {}), + services: { + 'api-server': { + status: 'running', + url: 'http://127.0.0.1:8082', + }, + spacetime: { + status: 'running', + url: 'http://127.0.0.1:3101', + }, + }, + }; +} + +async function waitForFile(path: string, timeoutMs = 5000) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (existsSync(path)) { + return; + } + await new Promise((resolveWait) => setTimeout(resolveWait, 25)); + } + throw new Error(`等待测试进程标记超时: ${path}`); +} + +describe('AI 游戏创作配套后端复用门禁', () => { + test('旧状态缺少专用 data dir 时拒绝复用同名健康后端', () => { + const targets = resolveBackendTargetsFromState(backendState(), { + requireAgcBackend: true, + expectedDatabase, + expectedSpacetimeDataDir: expectedDataDir, + }); + + expect(targets.hasMatchingDatabase).toBe(true); + expect(targets.hasMatchingDataDir).toBe(false); + expect(targets.hasMatchingBackend).toBe(false); + expect(targets.apiUrl).toBe(''); + expect(targets.spacetimeUrl).toBe(''); + }); + + test('只有数据库名和专用 data dir 都匹配时才允许复用', () => { + const wrongDir = resolveBackendTargetsFromState( + backendState(resolve('server-rs/.spacetimedb/local/data')), + { + requireAgcBackend: true, + expectedDatabase, + expectedSpacetimeDataDir: expectedDataDir, + }, + ); + const matching = resolveBackendTargetsFromState( + backendState(expectedDataDir), + { + requireAgcBackend: true, + expectedDatabase, + expectedSpacetimeDataDir: expectedDataDir, + }, + ); + + expect(wrongDir.hasMatchingBackend).toBe(false); + expect(matching.hasMatchingBackend).toBe(true); + expect(matching.apiUrl).toBe('http://127.0.0.1:8082'); + expect(matching.spacetimeUrl).toBe('http://127.0.0.1:3101'); + }); +}); + +describe('AI 游戏创作启动子进程生命周期', () => { + const posixTest = process.platform === 'win32' ? test.skip : test; + + posixTest('npm 不可解析时进入受控 error 结果而不是未处理事件', async () => { + const child = spawnChild('genarrative-command-that-does-not-exist', [], { + cwd: process.cwd(), + }); + + const failure = await waitForChildTermination(child); + + expect(failure.type).toBe('error'); + expect(failure.error).toMatchObject({ code: 'ENOENT' }); + }); + + posixTest('leader 退出后仍按保留的 PGID 清理后代进程', async () => { + const tempDir = mkdtempSync(join(tmpdir(), 'agc-process-group-')); + const readyPath = join(tempDir, 'descendant-ready'); + const stoppedPath = join(tempDir, 'descendant-stopped'); + const descendantSource = ` + const { writeFileSync } = require('node:fs'); + const [readyPath, stoppedPath] = process.argv.slice(1); + process.on('SIGTERM', () => { + writeFileSync(stoppedPath, 'stopped'); + process.exit(0); + }); + writeFileSync(readyPath, 'ready'); + setInterval(() => {}, 1000); + `; + const leaderSource = ` + const { spawn } = require('node:child_process'); + const [readyPath, stoppedPath, descendantSource] = process.argv.slice(1); + const descendant = spawn( + process.execPath, + ['-e', descendantSource, readyPath, stoppedPath], + { stdio: 'ignore' }, + ); + descendant.unref(); + process.exit(42); + `; + let child; + try { + child = spawnChild( + process.execPath, + ['-e', leaderSource, readyPath, stoppedPath, descendantSource], + { cwd: process.cwd() }, + ); + + const failure = await waitForChildTermination(child); + expect(failure).toMatchObject({ type: 'exit', code: 42 }); + await waitForFile(readyPath); + + stopChild(child); + + await waitForFile(stoppedPath); + } finally { + if (Number.isInteger(child?.pid)) { + try { + process.kill(-child.pid, 'SIGKILL'); + } catch { + // 测试后代已经退出。 + } + } + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test('后端句柄在 ready 等待前交给外层且异常时立即清理', async () => { + const child = Object.assign(new EventEmitter(), { + exitCode: null, + signalCode: null, + kill: vi.fn(), + }); + const onBackendChild = vi.fn(); + const waitUntilReady = vi.fn(async (receivedChild) => { + expect(receivedChild).toBe(child); + expect(onBackendChild).toHaveBeenCalledWith(child); + throw new Error('等待配套后端和数据库启动超时'); + }); + + await expect( + ensureBackend({ + checkBackendReady: async () => false, + spawnBackend: () => child, + onBackendChild, + waitUntilReady, + }), + ).rejects.toThrow('等待配套后端和数据库启动超时'); + + expect(child.kill).toHaveBeenCalledWith('SIGTERM'); + }); +}); diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 28286a2bd..a912da410 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -3267,8 +3267,8 @@ - 现象:target 注册了 SIGTERM 清理逻辑,但 `command.terminate` 只偶尔出现 stopped marker;耗时 300-500ms 的清理经常被提前截断。 - 原因:如果先向 wrapper/bwrap/trampoline/target 共用的外层进程组发送 SIGTERM,wrapper 会先退出,bwrap 的 die-with-parent 随即收走 namespace;名义上的 800ms 宽限并没有真正留给 target。 -- 处理:process-session target 在 child pre-exec 内暂时屏蔽 SIGTTOU,完成 setpgid + PTY slave tcsetpgrp 并恢复信号掩码后才 exec;不能先 spawn 到后台组再由 parent 设前台,否则 target 可能已经因 immediate read 收到 SIGTTIN。Runtime 通过两级私有控制通道请求 trampoline 只向 target group 发 SIGTERM。direct leader 退出后 trampoline 继续检查同组后代,外层 wrapper/bwrap 在最多 800ms 宽限期保持存活,超时才强杀 containment group。 -- 验证:使用直接 bash target 启动同组后台子进程;leader 在输出 READY 后自然退出,仍存活的子进程收到 TERM 后由 trap 延迟 400ms 写 marker 并退出,terminate 返回前 marker 必须存在。另跑 immediate stdin/EOF、Runner owner SIGKILL 和后代隔离用例,确认前台切组没有破坏交互或 fail-closed 回收。 +- 处理:process-session target 在 child pre-exec 内暂时屏蔽 SIGTTOU,完成 setpgid + PTY slave tcsetpgrp 并恢复信号掩码后才 exec;不能先 spawn 到后台组再由 parent 设前台,否则 target 可能已经因 immediate read 收到 SIGTTIN。Runtime 通过两级私有控制通道请求 trampoline 只向 target group 发 SIGTERM。direct leader 退出后 trampoline 继续检查同组后代,外层 wrapper/bwrap 在最多 800ms 宽限期保持存活,超时才强杀 containment group。reader 发现未换行输出超过上限时必须先原子投影 `output-limit-exceeded` 并唤醒 poll,再异步发送终止控制,不能让高负载下的 supervisor 调度延迟把已越界进程继续暴露为 `running`。 +- 验证:使用直接 bash target 启动同组后台子进程;leader 在输出 READY 后自然退出,仍存活的子进程收到 TERM 后由 trap 延迟 400ms 写 marker 并退出,terminate 返回前 marker 必须存在。正式 `command.exec` 测试夹具仍必须走允许的 `npm run` 等程序,不能为了构造 stdin race 绕过白名单直接解析 `bash -lc`。另跑 immediate stdin/EOF、Runner owner SIGKILL 和后代隔离用例,确认前台切组没有破坏交互或 fail-closed 回收;测试互斥锁在前序 panic 后应恢复 guard 继续报告后续独立结果,不能用 `PoisonError` 掩盖真实失败范围。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/process_session.rs`、`process_session_bridge.rs`、`command_sandbox_trampoline.rs`。 ## 启动记录必须封闭状态组合,child 不能自行猜 durable commit 超时 @@ -3311,14 +3311,14 @@ - 处理:正式主聊天只路由到 `project-supervisor` active Session,活跃期输入继续 same-run steer;同一父 run 最多同时保留 3 个 `dispatched / ready` 静态专业委派,已预留的同 action delivery 恢复复用原 target Session/run,不另占名额。同一工具计划完成委派后,Runtime 在下一次 Provider planning 前直接持久化 `waiting-for-delegate-receipts` 并释放 lane,不让模型轮询等待。delivery 单向推进 `dispatched -> ready -> claimed-by-parent / suppressed`,claim 单向推进 `Prepared -> Committed -> Observed`;先持有 claim 锁,再对 delegationId 排序去重并按序取齐 delivery 锁,任一锁不可得时零状态推进。delivery / claim journal 与 pending observation 是事实源;Agent DB append 只能 best-effort,失败不得推翻已持久化结果。入队在 Session lane 内完成,Runner 通知在 lane 外发送;`agent.run_status` 保留 claim 身份校验但不绑定全局 project revision/fingerprint。 - 恢复门禁:只有 `project-supervisor` 的 executing `agent.delegate / agent.run_status` 可在项目锁内重验 durable pending、Session/run/action fingerprint、delivery/claim/child 身份和当前 policy 后补交;只有 delivery 预留且无 child 时,拒绝动作必须把该预留 CAS 为 suppressed。其他 executing 动作或副作用身份不明必须进入 `needs-reconciliation`。parent-wake 以 project/Agent/run 做 coalescing singleflight,新信号不能在已有 worker 退出窗口丢失;有界重试接受 lane 竞争、暂时连接、连接中止、broken pipe、unexpected EOF、资源暂不可用和超时类错误。损坏 journal、身份冲突及重启扫描中的损坏 barrier 直接投影 reconciliation。External Runner wake 用项目根、method、Agent、runId 和 loop iteration 派生稳定 requestId,目标未观察到、仍 waiting 或 lane 忙时返回不缓存的可重试错误。 - 身份与收束:子终态发布前同时核对 parent Agent/Session/run/action、delegationId 派生、target Agent/Session/run、child source 和 child 反向 parent/delegation 链接。错配 child 保持原 delivery 不变并记录冲突;父任务先进入 completed / failed / cancelled / budget-exhausted 时,终态写入路径 suppress 尚未认领的匹配 delivery,合法迟到 child 不能重新写 ready。父 run 在 waiting、ready-unclaimed 或 unobserved claim 任一非零时都不得 final;全部清零后仍由原 Supervisor Session/run 的 finalization journal 幂等写入唯一 assistant,不创建新 receipt run。 -- 验证:Rust 定向回归使用 `project_supervisor_` 前缀,覆盖 delivery/claim 状态机、同 action 幂等、第 4 个新委派拒绝与已预留委派复用/拒绝 suppression、后续 delivery 锁忙时零部分认领、Agent DB 故障后回执仍可重放、未 Observed 阻断 final、Provider planning 前 durable 等待、parent-wake coalescing/结构性错误、重启损坏 barrier、错配和迟到 child、executing `run_status` 续接与 delegate policy 重验;`agent_background_enqueue_notifies_only_after_session_lane_release` 覆盖入队锁序,Runner 内部测试覆盖定向 wake 与不缓存重试。真实 Provider 必须同时证明专业 Agent 时间区间重叠、父 run 仅一次 waiting、同一 Observed claim 认领全部回执、唯一 assistant、第二轮历史引用不新增委派和项目范围密钥扫描为 0。 +- 验证:Rust 定向回归使用 `project_supervisor_` 前缀,覆盖 delivery/claim 状态机、同 action 幂等、第 4 个新委派拒绝与已预留委派复用/拒绝 suppression、后续 delivery 锁忙时零部分认领、Agent DB 故障后回执仍可重放、未 Observed 阻断 final、Provider planning 前 durable 等待、parent-wake coalescing/结构性错误、重启损坏 barrier、错配和迟到 child、executing `run_status` 续接与 delegate policy 重验;本地 mock Provider 长套件应允许一次短间隔 connectivity 重试,并在断言前同时等待终态投影和 Agent lane 释放,避免端口瞬时波动或后台收尾窗口制造假失败。`agent_background_enqueue_notifies_only_after_session_lane_release` 覆盖入队锁序,Runner 内部测试覆盖定向 wake 与不缓存重试。真实 Provider 必须同时证明专业 Agent 时间区间重叠、父 run 仅一次 waiting、同一 Observed claim 认领全部回执、唯一 assistant、第二轮历史引用不新增委派和项目范围密钥扫描为 0。 - 关联:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`、`apps/ai-game-creator-shell/src-tauri/src/delegation.rs`、`agent.rs`、`runner.rs`、`tests.rs`。 ## 父 run 协作策略不能在绑定后继续按全局 live policy 重验 - 现象:同一 Supervisor 父 run 已经持久化合法 collaboration batch,管理员随后修改或损坏 `.agent/collaboration-policy.json`,后续 spawn、claim、mutation、MCP 或 finalization 却突然改用新策略、进入 reconciliation;或者 snapshot 被删除后,Runtime 又按 live policy 把已有 run 当成未绑定 run。另一类症状是 contractless/v1 batch 被跳过、两个不安全 run ID 经字符替换落到同一 snapshot/锁 key,或旧 `Prepared / Committed` claim 因 snapshot/binding 不可读而不能重放 observation。 - 原因:把项目级 policy 当成每个动作的 live 执行事实,没有为父 run 设置明确线性化点、不可变策略快照和独立“曾绑定”记录;或者在 v2 batch 完整验真前就用 `contract.policy` 播种 snapshot。只对 run ID 做 lossy 规范化、让锁复用该路径片段,或用通用原子 replace 代替同一身份锁内 CAS,也会制造路径碰撞、并发覆盖和伪合同漂移。 -- 处理:V1.38 固定顺序为 `v2 batch -> snapshot -> binding sidecar -> action side effects`。snapshot 位于 `.agent/runtime/collaboration-policy-snapshots//.json`,其完整字段必须统一为 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policy / policyFingerprint / snapshotFingerprint / boundAt`;snapshot fingerprint 覆盖除 `snapshotFingerprint / boundAt` 外的全部稳定字段。独立 binding 位于 `.agent/runtime/collaboration-policy-snapshot-bindings//.json`,固定包含 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policyFingerprint / snapshotFingerprint / boundAt`,与 snapshot 逐字段交叉验证并持久证明“该 run 曾绑定”。 +- 处理:V1.38 固定顺序为 `v2 batch -> snapshot -> binding sidecar -> action side effects`。snapshot 位于 `.agent/runtime/collaboration-policy-snapshots//.json`,其完整字段必须统一为 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policy / policyFingerprint / snapshotFingerprint / boundAt`;snapshot fingerprint 覆盖除 `snapshotFingerprint / boundAt` 外的全部稳定字段。独立 binding 位于 `.agent/runtime/collaboration-policy-snapshot-bindings//.json`,固定包含 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policyFingerprint / snapshotFingerprint / boundAt`,与 snapshot 逐字段交叉验证并持久证明“该 run 曾绑定”。同一 run 并发绑定时,无论 loser 是读取到不同快照还是在 winner 持锁期间耗尽有界等待,都必须返回稳定的“并发绑定冲突”错误分类。Unix 同进程首次并发初始化安全锁路径时,需要短暂串行化 `mkdirat/openat` 打开阶段,规避 macOS loser 在最终 `O_CREAT` 前观察到瞬时 `ENOENT`;返回后的 `flock` 仍承担跨线程、跨进程互斥。 - 路径与恢复:不安全或规范化后变化的 Agent/run ID 使用有界安全前缀加原始 ID 稳定 SHA-256,锁 key 对完整 `parentAgentId + NUL + parentRunId` 计算稳定 SHA-256,不能只做字符替换。恢复顺序为 existing valid snapshot > 完整验真的 v2 batch contract > 符合严格状态门禁的 legacy 当前有效 policy;snapshot 缺 binding 可从 snapshot 补写,binding 存在但 snapshot 丢失只能按可信 v2 contract 和首次绑定身份恢复,无可信 v2 时禁止 live policy 重绑。contractless/v1 collaboration batch 必须先失败关闭。`legacy-current-project-policy` 只允许无 snapshot/binding、无可信 v2 contract,且不存在上述旧 batch,并由可信身份和状态明确证明属于 `pending / running / waiting-for-confirmation / waiting-for-user-input` 的旧父 run;terminal、`needs-reconciliation` 或身份/状态未知 run 的状态读取不得新建 snapshot。 - 漂移与 Claim:绑定后 global policy 的 `matched / drifted / unreadable` 只报告状态,不能改变后续动作或完成门禁;新 policy 只用于后续新父 run。旧 durable claim、未观察 claim 和 legacy claimed delivery 先按原 action/group 身份恢复且不得取得新 delivery;新的 claim 必须先成功解析 effective snapshot 并核对 binding,再执行 V1.35-V1.37 的全锁、预算、完整 observation 和 group 数量门禁。 - 真实 E2E 现场:正在运行的正式客户端可能在验收期间启动或重启正式 Runner,导致 source endpoint 身份真实变化。不得关闭 `sourceRunnerEndpointUnchanged` 门禁,也不得杀掉不属于验收器的进程;应把同一配置内容复制到仓库外的大容量磁盘私有目录,目录/文件权限分别为 `0700/0600`,不复制 endpoint、锁、会话或数据库,验收后删除。功能完整但 source endpoint 被外部改变的报告与后续干净清理报告不得拼接。 @@ -3500,3 +3500,21 @@ - 运维陷阱:从容器内运行 Compose 时,宿主 `/opt/gitea-stack` 必须挂到容器同名绝对路径;挂成 `/stack` 会让相对 bind source 被 daemon解析为宿主 `/stack/...`,表现为 Gitea进入空安装页、gateway 脚本“缺失”。发现后不要迁移空库,立即用同路径 mount 重建并核对原数据大小、installed 日志、仓库数和 API。切换前保留冷数据 tar、pg_dumpall 和原 compose/env/runner 配置,备份与 token 不提交 Git。 - 验证:同时检查 Gitea 版本、runner declare、外层 `Privileged=false`/无 CapAdd/无宿主 socket、inner job `Binds=[]`、`MaskedPaths=[]`、`ReadonlyPaths=[]`、固定 image digest、`/var/run/docker.sock` 不存在、公共 proxy 可用、直连公网/Postgres/metadata 失败,以及完整 bwrap canary。AI 原生壳的共享 Agent Runtime 后台锁 suite 固定单线程执行;并行全量出现锁或异步终态失败、逐项单线程全部通过时,修正 suite 调度口径,不放宽断言。最后重跑四个 CI job;checkout 成功但 apt/rustup/npm 同时失败时,先排 proxy/env,而不是改测试。 - 关联:`.gitea/workflows/project-ci.yml`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`、`docs/project-memory/shared-memory/development-workflow.md`。 + +## Unix 文件身份复核不能假定 Linux 的 `dev_t` 类型 + +- 现象:AI 游戏创作 Tauri 壳在 Linux CI 编译通过,但 macOS 上会在 Agent DB、External Runner owner 和 tool-plan handoff 的 `fstatat` 身份复核中报 `i32 == u64` 类型错误;Tauri 失败后配套后端收束,终端还可能短暂出现 SpacetimeDB 订阅连接失败的连锁日志。 +- 原因:`libc::stat.st_dev` 跟随平台 `dev_t`,macOS 为有符号整数,而 `std::os::unix::fs::MetadataExt::dev()` 统一返回 `u64`;直接比较会把 Linux 的类型偶合误当成 Unix 通用契约。 +- 处理:与 Rust 标准库的 Unix `MetadataExt` 实现保持一致,先把 `st_dev / st_ino` 规范为 `u64`,再与 `metadata.dev() / metadata.ino()` 比较;设备号、inode 和文件类型三重检查均必须保留。 +- macOS 测试夹具:`std::env::temp_dir()` 可能返回 `/var/folders/...`,而 `/var` 是系统兼容符号链接。需要真实项目根的 Runtime 测试应先 canonicalize 已存在的临时根目录,再创建唯一子目录;不得为了让夹具通过而放宽生产 Runtime 的项目根及祖先符号链接拒绝规则。 +- 验证:macOS 本机运行 `cargo check --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`,并复跑 Agent DB、project owner 和 tool-plan handoff 的 Unix 相对句柄替换检测;Linux CI 继续覆盖原有安全回归。 +- 关联:`apps/ai-game-creator-shell/src-tauri/src/project.rs`、`runner.rs`、`tool_plan_handoff.rs`。 + +## AI 游戏创作壳不能用全局或匿名身份发布本地模块 + +- 现象:`npm run agc` 在发布模块时先访问 `auth.spacetimedb.com` 并以 401 失败;改成 `--anonymous` 后首次可能成功,但再次启动会因匿名 identity 变化而 403。若把 403 当成可忽略警告继续启动,api-server 会连接旧 schema,随后持续输出 `external_generation_job`、`profile_recharge_order_expiration_timer` 等缺表订阅失败,Tauri 也可能在后端就绪前退出或迟迟不弹窗。 +- 原因:本地 publish 默认继承开发者全局 SpacetimeDB 云端登录,离线时 standalone 无法校验 issuer;`--anonymous` 不是可跨进程持久复用的 owner identity;AI 游戏创作壳若再复用主站历史数据目录,还会继承旧数据库归属和旧 schema。 +- 处理:AI 游戏创作壳固定使用 gitignored 的独立数据目录;standalone 就绪后先从 `/v1/identity` 获取并按 data dir 而非监听端口持久化同一 API identity,再用数据目录内权限为 `0600` 的独立 `cli.toml` 执行 `spacetime login --token` 和 publish。旧端口作用域记录在同一 data dir 下身份唯一时迁移,存在多个不同身份时失败关闭,不能猜 owner。远程 server 继续使用正常登录配置;本地 publish 403 必须阻断 API/Vite,不得带旧 schema 降级启动。`.app/dev-stack.json` 记录规范化 data dir,独立壳复用后端时必须同时匹配数据库名、专用目录和健康状态;缺少目录字段的旧状态不得复用。POSIX 启动器在 `spawn` 后立即监听 `error / exit`、保存 detached leader 的 PGID、向外层登记句柄并用独立进程组收束 npm、Node、Cargo 和子进程;direct leader 先退出后仍向负 PGID 发信号清理后代,ready 前中断、超时或 ENOENT 也走统一清理,退出后确认 3080、8082、3101 均释放。 +- macOS 日志:api-server 进程指标当前只实现 Windows API 和 Linux `/proc`,macOS 必须跳过 observable callback 注册;不能每轮采集为每个指标重复打印“不支持平台”。Rust/Tauri 既有 `dead_code` warning 与一次性配置缺失提示不属于长驻重试日志。非 Linux `project.verify` 校验 `npm run` 参数时必须越过 `--silent`、`--ignore-scripts` 等前置选项定位真实脚本名,不能固定读取 `run` 后第一个参数,否则会在 macOS 将合法验证误报为“缺少脚本名”并引发 Runtime 测试级联失败。 +- 验证:定向测试覆盖同一 data dir 跨端口复用 identity、不同 data dir 隔离、旧 state/data dir 不匹配拒绝复用、spawn ENOENT 受控失败、direct leader 以 42 退出后同组 descendant 仍收到 TERM,以及后端 ready 前句柄已登记且超时清理。连续运行两次 `npm run agc`,两次都必须真实完成 module publish、`/v1/ping`、`/healthz`、Vite 3080 和 Tauri `Running`;稳定观察期间不得出现缺表订阅失败或进程指标平台告警,Ctrl-C 后三个端口和主 Tauri 进程均应释放。 +- 关联:`scripts/dev.mjs`、`apps/ai-game-creator-shell/scripts/start-dev-stack.mjs`、`server-rs/crates/api-server/src/process_metrics.rs`。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 1b98ce07e..0b99b701c 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -449,6 +449,7 @@ game-project/ - `apps/ai-game-creator-shell` 是独立 Tauri App,不复用 `apps/desktop-shell`。 - 独立客户端启动时先进入平台登录检查;未登录页默认展示手机号验证码登录,并保留密码登录切换。验证码登录调用平台后端 `/api/auth/phone/send-code` 与 `/api/auth/phone/login`,密码登录继续调用 `/api/auth/entry`;Tauri dev 下 `/api` 走固定 3080 Vite 代理,发布版静态窗口下登录请求默认直连本机配套 `http://127.0.0.1:8082` API,网络层失败时展示登录服务不可达提示,不裸露 WebView 的 `Load failed`。 +- `npm run agc` 的本地 SpacetimeDB owner identity 以独立 `spacetimeDataDir` 为作用域,不绑定可能漂移的监听端口;旧端口作用域记录仅在同一 data dir 下身份唯一时自动迁移,出现多个不同旧身份时失败关闭。`.app/dev-stack.json` 必须记录规范化 `spacetimeDataDir`,独立壳只复用数据库名和该目录同时匹配且健康的后端,旧 schema 状态或共享目录状态缺少此字段时不得复用。POSIX 子进程在 `spawn` 返回时立即登记 `error / exit` 生命周期、保存 detached leader 的 PGID 并把句柄交给外层;即使 direct leader 已先退出,也必须继续向负 PGID 发信号清理同组后代。后端 ready 前的 SIGINT、SIGTERM、超时或 ENOENT 都必须走同一进程组清理链路,不能遗留 npm、Cargo 或 SpacetimeDB。非 Linux Runtime 执行 `project.verify` 时,`npm run` 参数校验必须允许受控的 `--silent`、`--ignore-scripts` 位于脚本名前,并继续拒绝缺少真实脚本名的调用。 - Tauri Rust 入口保持薄壳:`src-tauri/src/main.rs` 只保留共享类型 / 常量、模块声明、CLI preflight、`tauri::Builder`、运行时配置初始化和 `invoke_handler` 清单;命令行入口放在 `cli.rs`,Tauri command 包装放在 `commands.rs`,运行时配置与 LLM 配置检查放在 `config.rs`,Agent loop 与生成编排放在 `agent.rs`,上传 / 画板 / 平台美术生成接入放在 `assets.rs`,本地项目文件、记忆、对话、权限、checkpoint、manifest 和通用路径工具放在 `project.rs`,本地 HTTP 预览与 preview 命令放在 `preview.rs`,旧窗口兼容命令放在 `windows.rs`,Rust 单测放在 `tests.rs`。后续继续拆分时保持 Tauri command 名、JSON 字段、`.agent/*` 路径和错误语义不变。 - 本地项目初始化会创建 `game/`、`assets/`、`memory/`、`memory/agents/`、`exports/`、`.agent/logs/`,写入 `.agent/manifest.json`,生成 append-only JSONL 本地项目索引 `.agent/agent.db`,并生成默认 `game/index.html`。 - v1 conversation 记录使用 append-only JSONL,每行带 `schemaVersion`、`role`、`content`、`agentId` 和 `updatedAt`,作为聊天历史和单 agent 对话历史的事实源;目录在首次写入时创建。 @@ -474,6 +475,9 @@ game-project/ - 终端可用 `npm run ai-game-creator-shell:agent-run -- /绝对项目路径 "游戏创作需求"` 跑一次真实 LLM 生成、落盘、`game.static_smoke` 和本地 HTTP 预览;发布 App 读取 Tauri 应用配置目录中的 `game-creator.config.json`,开发 CLI 无 AppHandle 时才读取仓库旁边的配置模板和 gitignored 本机覆盖文件,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。默认 API kind 为 `openai_responses`;旧 Chat Completions 兼容网关设置 `llm.apiKind` 为 `openai_chat`,Anthropic Messages 网关设置 `llm.apiKind` 为 `anthropic`。真实 OpenAI-compatible 网关建议设置 `llm.stream` 为 `true` 跑 Planner 和 Generator,避免长请求非流式空闲断连。 - 终端可用 `npm run ai-game-creator-shell:agent-run:smoke` 跑一次无密钥本地端到端 smoke:脚本启动本机 OpenAI-compatible SSE 流式测试 provider,预置一个本地上传图片和一个本地上传音频,复用真实 `--agent-run`、Planner / Orchestrator / 角色 agent / Generator / Evaluator loop、本地落盘、`game.static_smoke` 和本地 HTTP 预览,并断言每次 provider 请求都使用 `stream: true`、Planner 与 Generator 分别命中自己的 `agentLlm` provider 配置、provider prompt 收到图片与音频资产上下文以及最近对话上下文、生成 HTML 引用这些资产、预览服务能用 `GET` 读取 `/assets/...`、用 `HEAD` 返回真实资源长度和对应 MIME、headless Chrome 打开预览后至少执行一帧游戏 JS,且通过确定性亮色探针采样证明 canvas 不是空白画布、`.agent/run.latest.json` 的 step group 覆盖 design / balance / art / audio / code / publishing 六组、第二轮会重跑 Evaluator 命中任务及其下游影响任务,未受影响角色 carry-over;随后脚本自动给 CLI 发送回车停止预览。该脚本只用于开发验证,不进入产品生成路径。 - `npm run ai-game-creator-shell:dev` 的 Tauri `devUrl` 固定为 `http://127.0.0.1:3080/`,Vite 必须 `strictPort` 对齐;`beforeDevCommand` 先复用已经跑在 3080 且页面标题为 `AI 游戏创作` 的本 app Vite server,否则才启动新的 Vite,若端口被其它服务占用则直接失败并提示释放端口。 +- AI 游戏创作 App 的本地后端使用 gitignored 的 `server-rs/.spacetimedb/ai-game-creator/data`,不复用主站旧 standalone 数据目录。启动器从本地 `/v1/identity` 获取并持久化 API identity,再通过数据目录内 `0600` 的独立 `dev-cli/cli.toml` 发布模块;不得读取或覆盖开发者全局 SpacetimeDB 登录,也不得回退到每次变化的 `--anonymous` 身份。发布失败时 API 和 Vite 不得继续启动旧 schema,避免 `external_generation_job` 等缺表订阅进入持续重试。 +- `start-dev-stack.mjs` 在 POSIX 下以独立进程组托管后端和 Vite,关闭 Tauri 或任一子进程失败时必须收束整组;macOS 不注册仅支持 Windows/Linux 的 api-server 进程指标 observable callback,避免每轮指标采集重复输出平台不支持告警。 +- Unix 下 Agent DB、External Runner owner 和 tool-plan handoff 的相对句柄复核必须同时比较设备号、inode 和文件类型;`libc::stat` 的 `st_dev / st_ino` 先按 Rust `MetadataExt` 的 Unix 口径规范为 `u64` 再比较,保持 Linux 和 macOS 的同一安全语义,不得为了通过 macOS 编译而删除路径替换检测。 - AI 游戏创作 App 的 Vite root 保持在 `apps/ai-game-creator-shell`,但开发服务器必须通过 `server.fs.allow: [repoRoot]` 允许加载 `packages/shared/src` 共享契约;配置自检同时守住该规则,避免 typecheck 通过后真实 Tauri WebView 因共享源码 403 变成白屏。Tauri 事件 capability 只向 `client`、`developer`、`main`、`launcher` 窗口开放 `core:event:allow-listen` 和 `core:event:allow-unlisten`;Runtime 事件仍由 Rust 发出,前端不获得 `emit` 权限。 - `.agent/manifest.json` 会保存 6 个专业组下 16 个组内角色任务状态,当前覆盖 `Director`、`Gameplay`、`Difficulty`、`Asset`、`Polish`、`SFX`、`Code`、`Review`、`Preview`、`Playtest`、`Publish`;程序组内显式包含 `quality-review` 质量评审 gate,由 Evaluator trace 标记完成;开发窗口的专业组面板读取 manifest,而不是前端硬编码。 - 主窗口的 agent 状态列表以 manifest 角色任务为底表,再合并最近 run trace 中 `taskGraph.tasks` 的任务状态、同 taskId / group / role 的最新 step 状态、输入输出路径、错误摘要、lifecycleStatus 和 `activeTaskIds` / `carriedTaskIds` / `readyTaskIds` 编排标记;如果 trace 缺失或过期,只展示 manifest 的静态任务状态和“暂无最近运行证据”。 diff --git a/scripts/dev.mjs b/scripts/dev.mjs index c1004be94..49ba11217 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -383,10 +383,11 @@ function buildDevStackSnapshot(runner, updatedAt = new Date().toISOString()) { } return { - schemaVersion: 1, + schemaVersion: 2, command: runner.command ?? 'all', repoRoot, database: runner.options.database, + spacetimeDataDir: resolve(runner.options.spacetimeDataDir), watch: Boolean(runner.options.watch), updatedAt, services, @@ -924,7 +925,7 @@ function readLinuxApiServerProcessSnapshot(pid) { if ( error?.code === 'ENOENT' || error?.code === 'EACCES' || - error?.code === 'EPERM' || + error?.code === 'EPERM' || error?.code === 'ESRCH' ) { return null; @@ -1124,7 +1125,11 @@ class DevRunner { this.command = command; ensureRequiredFiles(command); requireCommand('node'); - if (command === 'api-server' || command === 'all' || command === 'backend') { + if ( + command === 'api-server' || + command === 'all' || + command === 'backend' + ) { requireCommand('cargo'); } if ( @@ -1319,7 +1324,11 @@ class DevRunner { } } - if (command === 'all' || command === 'backend' || command === 'api-server') { + if ( + command === 'all' || + command === 'backend' || + command === 'api-server' + ) { portConfig.api = { host: options.apiHost, preferredPort: options.apiPort, @@ -1513,12 +1522,11 @@ class DevRunner { await this.publishSpacetimeModule(); } catch (error) { if (isSpacetimePublishPermissionError(error)) { - console.warn( - `[dev:spacetime] 本地发布被当前 identity 拒绝,保留已启动的 standalone: ${error.message}`, + throw new Error( + `本地数据库不属于当前隔离 identity,已停止启动以避免 API 使用旧 schema 后持续重试订阅。请改用独立本地数据目录,或在确认无需保留旧开发数据后重建该目录。详情: ${error.message}`, ); - } else { - throw error; } + throw error; } } } @@ -1645,8 +1653,10 @@ class DevRunner { async publishSpacetimeModule() { const env = buildLocalRustProcessEnv(this.baseEnv); this.prepareMigrationBootstrapSecret(env); + const cliConfigPath = await this.prepareLocalSpacetimeCliIdentity(env); const args = buildSpacetimePublishArgs({ + cliConfigPath, database: this.options.database, preserveDatabase: this.options.preserveDatabase, server: this.state.spacetimeServer, @@ -1660,6 +1670,48 @@ class DevRunner { }); } + async prepareLocalSpacetimeCliIdentity(env) { + if (!isLoopbackSpacetimeServer(this.state.spacetimeServer)) { + return ''; + } + + await this.ensureApiServerSpacetimeToken(); + const cliConfigPath = resolve( + this.options.spacetimeDataDir, + 'dev-cli', + 'cli.toml', + ); + ensureParentDir(cliConfigPath); + if ( + existsSync(cliConfigPath) && + resolveCurrentSpacetimeCliToken(cliConfigPath) === this.spacetimeApiToken + ) { + chmodSync(cliConfigPath, 0o600); + console.log('[dev:spacetime] 已复用隔离的本地发布 identity'); + return cliConfigPath; + } + await runForeground( + 'spacetime', + [ + '--config-path', + cliConfigPath, + 'login', + '--token', + this.spacetimeApiToken, + ], + { + cwd: serverRsDir, + env, + label: 'spacetime-login', + }, + ); + if (existsSync(cliConfigPath)) { + chmodSync(cliConfigPath, 0o600); + } + console.log('[dev:spacetime] 已配置隔离的本地发布 identity'); + return cliConfigPath; + } + prepareMigrationBootstrapSecret(env) { let runtimeServiceBootstrapSecret = ''; switch (this.options.migrationBootstrapSecretMode) { @@ -2447,10 +2499,85 @@ function normalizeSpacetimeServerForIdentity(serverUrl) { return url.href.replace(/\/$/u, ''); } -function resolveLocalSpacetimeApiIdentityPath(dataDir, serverUrl) { - const normalizedServer = normalizeSpacetimeServerForIdentity(serverUrl); - const serverKey = createHash('sha256').update(normalizedServer).digest('hex'); - return resolve(dataDir, 'dev-api-identities', `${serverKey}.json`); +function resolveLocalSpacetimeApiIdentityPath(dataDir) { + return resolve(dataDir, 'dev-api-identities', 'local-node.json'); +} + +function readLocalSpacetimeApiIdentityRecord(identityPath, expected = {}) { + const stat = lstatSync(identityPath); + if (!stat.isFile() || stat.isSymbolicLink()) { + throw new Error('记录不是普通文件'); + } + chmodSync(identityPath, 0o600); + const payload = JSON.parse(readFileSync(identityPath, 'utf8')); + const identity = + typeof payload.identity === 'string' ? payload.identity.trim() : ''; + const token = typeof payload.token === 'string' ? payload.token.trim() : ''; + if (!identity || !token) { + throw new Error('记录缺少 identity 或 token'); + } + + if (payload.schemaVersion === 2 && payload.scope === 'local-data-dir') { + return { identity, token }; + } + if ( + expected.allowLegacy && + payload.schemaVersion === 1 && + typeof payload.server === 'string' && + isLoopbackSpacetimeServer(payload.server) + ) { + return { identity, token }; + } + throw new Error('记录格式或 data dir 作用域不匹配'); +} + +function migrateLegacyLocalSpacetimeApiIdentity(dataDir) { + const identityDir = resolve(dataDir, 'dev-api-identities'); + if (!existsSync(identityDir)) { + return null; + } + + const candidates = []; + for (const entry of readdirSync(identityDir, { withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith('.json')) { + continue; + } + const candidatePath = resolve(identityDir, entry.name); + if (candidatePath === resolveLocalSpacetimeApiIdentityPath(dataDir)) { + continue; + } + try { + candidates.push( + readLocalSpacetimeApiIdentityRecord(candidatePath, { + allowLegacy: true, + }), + ); + } catch { + // 无效或非本地旧记录不参与迁移。 + } + } + + const uniqueCandidates = new Map( + candidates.map((candidate) => [ + `${candidate.identity}\n${candidate.token}`, + candidate, + ]), + ); + if (uniqueCandidates.size === 0) { + return null; + } + if (uniqueCandidates.size > 1) { + throw new Error( + '同一 SpacetimeDB data dir 下发现多个旧 API identity,无法安全判断数据库 owner;请保留正确 owner 记录后重试', + ); + } + + const [identity] = uniqueCandidates.values(); + writeLocalSpacetimeApiIdentity({ dataDir, ...identity }); + console.log( + '[dev:spacetime] 已将旧端口作用域 API identity 迁移到 data dir 作用域', + ); + return identity; } function resolveLocalSpacetimeRuntimeServiceBootstrapSecretPath( @@ -2600,37 +2727,13 @@ function readLocalSpacetimeApiIdentity({ dataDir, serverUrl }) { return null; } - const normalizedServer = normalizeSpacetimeServerForIdentity(serverUrl); - const identityPath = resolveLocalSpacetimeApiIdentityPath( - dataDir, - normalizedServer, - ); + const identityPath = resolveLocalSpacetimeApiIdentityPath(dataDir); if (!existsSync(identityPath)) { - return null; + return migrateLegacyLocalSpacetimeApiIdentity(dataDir); } try { - const stat = lstatSync(identityPath); - if (!stat.isFile() || stat.isSymbolicLink()) { - throw new Error('记录不是普通文件'); - } - chmodSync(identityPath, 0o600); - const payload = JSON.parse(readFileSync(identityPath, 'utf8')); - if ( - payload.schemaVersion !== 1 || - payload.server !== normalizedServer || - typeof payload.identity !== 'string' || - !payload.identity.trim() || - typeof payload.token !== 'string' || - !payload.token.trim() - ) { - throw new Error('记录格式或 server 绑定不匹配'); - } - - return { - identity: payload.identity.trim(), - token: payload.token.trim(), - }; + return readLocalSpacetimeApiIdentityRecord(identityPath); } catch (error) { console.warn( `[dev:spacetime] 本地 API identity 记录不可用,将重新创建: ${error.message}`, @@ -2639,17 +2742,8 @@ function readLocalSpacetimeApiIdentity({ dataDir, serverUrl }) { } } -function writeLocalSpacetimeApiIdentity({ - dataDir, - serverUrl, - identity, - token, -}) { - const normalizedServer = normalizeSpacetimeServerForIdentity(serverUrl); - const identityPath = resolveLocalSpacetimeApiIdentityPath( - dataDir, - normalizedServer, - ); +function writeLocalSpacetimeApiIdentity({ dataDir, identity, token }) { + const identityPath = resolveLocalSpacetimeApiIdentityPath(dataDir); const tempPath = `${identityPath}.${process.pid}.${randomHex(8)}.tmp`; ensureParentDir(identityPath); @@ -2657,8 +2751,8 @@ function writeLocalSpacetimeApiIdentity({ writeFileSync( tempPath, `${JSON.stringify({ - schemaVersion: 1, - server: normalizedServer, + schemaVersion: 2, + scope: 'local-data-dir', identity, token, })}\n`, @@ -2814,8 +2908,14 @@ function isLoopbackSpacetimeServer(serverUrl) { } } -function resolveCurrentSpacetimeCliToken() { - const result = spawnSync('spacetime', ['login', 'show', '--token'], { +function resolveCurrentSpacetimeCliToken(cliConfigPath = '') { + const args = [ + ...(cliConfigPath ? ['--config-path', cliConfigPath] : []), + 'login', + 'show', + '--token', + ]; + const result = spawnSync('spacetime', args, { cwd: repoRoot, encoding: 'utf8', shell: process.platform === 'win32', @@ -2839,13 +2939,21 @@ function trimPreview(text, maxLength = 300) { function runForeground(command, args, { cwd, env, label }) { return new Promise((resolveRun, rejectRun) => { + let capturedOutput = ''; + const capture = (chunk, target) => { + target.write(chunk); + capturedOutput = `${capturedOutput}${String(chunk)}`.slice(-32_768); + }; const child = spawn(command, args, { cwd, env, - stdio: 'inherit', + stdio: ['inherit', 'pipe', 'pipe'], shell: process.platform === 'win32', }); + child.stdout?.on('data', (chunk) => capture(chunk, process.stdout)); + child.stderr?.on('data', (chunk) => capture(chunk, process.stderr)); + child.on('error', rejectRun); child.on('exit', (code, signal) => { if (signal) { @@ -2854,7 +2962,12 @@ function runForeground(command, args, { cwd, env, label }) { } if (code !== 0) { - rejectRun(new Error(`[dev:${label}] 退出码: ${code}`)); + const detail = trimPreview(capturedOutput, 2_000); + rejectRun( + new Error( + `[dev:${label}] 退出码: ${code}${detail ? `: ${detail}` : ''}`, + ), + ); return; } @@ -2914,8 +3027,14 @@ function isDirectModuleExecution(argv1, moduleUrl, resolvePath = safeRealpath) { } } -function buildSpacetimePublishArgs({ database, server, preserveDatabase }) { +function buildSpacetimePublishArgs({ + cliConfigPath = '', + database, + server, + preserveDatabase, +}) { const args = [ + ...(cliConfigPath ? ['--config-path', cliConfigPath] : []), 'publish', database, '--server', diff --git a/scripts/dev.test.ts b/scripts/dev.test.ts index b6ebbac90..f6e09fd93 100644 --- a/scripts/dev.test.ts +++ b/scripts/dev.test.ts @@ -11,7 +11,7 @@ import { writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; -import { dirname, join } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; import { afterEach, describe, expect, test, vi } from 'vitest'; @@ -510,9 +510,12 @@ describe('dev scheduler stack state file', () => { const snapshot = buildDevStackSnapshot(runner, updatedAt); - expect(snapshot.schemaVersion).toBe(1); + expect(snapshot.schemaVersion).toBe(2); expect(snapshot.command).toBe('web'); expect(snapshot.database).toBe('genarrative-test'); + expect(snapshot.spacetimeDataDir).toBe( + resolve('server-rs/.spacetimedb/local/data'), + ); expect(snapshot.services.web).toMatchObject({ status: 'running', pid: 4321, @@ -761,16 +764,20 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.0; ).toBe(false); }); - test('发布 spacetime-module 时忽略 spacetime.json 以免覆盖显式数据库', () => { + test('发布 spacetime-module 时使用隔离身份配置并忽略 spacetime.json', () => { const args = buildSpacetimePublishArgs({ + cliConfigPath: '/tmp/genarrative-cli.toml', database: 'xushi-p4wfr', preserveDatabase: false, server: 'http://127.0.0.1:3101', }); expect(args).toContain('--no-config'); + expect(args).not.toContain('--anonymous'); expect(args).toEqual( expect.arrayContaining([ + '--config-path', + '/tmp/genarrative-cli.toml', 'publish', 'xushi-p4wfr', '--server', @@ -780,6 +787,17 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.0; ); }); + test('远程 SpacetimeDB 发布继续使用默认登录身份', () => { + const args = buildSpacetimePublishArgs({ + database: 'xushi-p4wfr', + preserveDatabase: true, + server: 'https://spacetime.example.com', + }); + + expect(args).not.toContain('--anonymous'); + expect(args).not.toContain('--config-path'); + }); + test('手动刷新 spacetime 只重新发布模块,不重启 standalone 进程', async () => { const { explicitOptions, options } = parseArgs([], {}); const runner = new DevRunner(options, {}, explicitOptions); @@ -812,26 +830,18 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.0; expect(runner.publishSpacetimeModule).not.toHaveBeenCalled(); }); - test('本地 API identity 路径同时绑定 data dir 和规范化 server', () => { + test('本地 API identity 路径只绑定 data dir', () => { const first = resolveLocalSpacetimeApiIdentityPath( '/tmp/genarrative-data-a', - 'http://127.0.0.1:3101', ); - const normalizedEquivalent = resolveLocalSpacetimeApiIdentityPath( + const sameDataDir = resolveLocalSpacetimeApiIdentityPath( '/tmp/genarrative-data-a', - 'http://127.0.0.1:3101/', - ); - const otherServer = resolveLocalSpacetimeApiIdentityPath( - '/tmp/genarrative-data-a', - 'http://127.0.0.1:3102', ); const otherDataDir = resolveLocalSpacetimeApiIdentityPath( '/tmp/genarrative-data-b', - 'http://127.0.0.1:3101', ); - expect(normalizedEquivalent).toBe(first); - expect(otherServer).not.toBe(first); + expect(sameDataDir).toBe(first); expect(otherDataDir).not.toBe(first); expect(first).toContain(join('genarrative-data-a', 'dev-api-identities')); }); @@ -858,10 +868,7 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.0; await firstRunner.ensureApiServerSpacetimeToken(); - const identityPath = resolveLocalSpacetimeApiIdentityPath( - tempDir, - firstRunner.state.spacetimeServer, - ); + const identityPath = resolveLocalSpacetimeApiIdentityPath(tempDir); expect(firstRunner.spacetimeApiToken).toBe('local-api-token'); expect(firstRunner.baseEnv.GENARRATIVE_SPACETIME_TOKEN).toBeUndefined(); expect(globalThis.fetch).toHaveBeenCalledWith( @@ -869,8 +876,8 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.0; expect.objectContaining({ method: 'POST' }), ); expect(JSON.parse(readFileSync(identityPath, 'utf8'))).toMatchObject({ - schemaVersion: 1, - server: 'http://127.0.0.1:3101', + schemaVersion: 2, + scope: 'local-data-dir', identity: 'c200localidentity', token: 'local-api-token', }); @@ -880,7 +887,7 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.0; } const secondRunner = new DevRunner(options, {}, explicitOptions); - secondRunner.state.spacetimeServer = 'http://127.0.0.1:3101'; + secondRunner.state.spacetimeServer = 'http://127.0.0.1:3199'; globalThis.fetch = vi.fn(); await secondRunner.ensureApiServerSpacetimeToken(); @@ -899,6 +906,94 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.0; } }); + test('旧端口作用域 API identity 会迁移为 data dir 作用域', async () => { + const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-identity-')); + try { + const legacyServer = 'http://127.0.0.1:3101'; + const legacyKey = createHash('sha256').update(legacyServer).digest('hex'); + const legacyPath = join( + tempDir, + 'dev-api-identities', + `${legacyKey}.json`, + ); + mkdirSync(dirname(legacyPath), { recursive: true }); + writeFileSync( + legacyPath, + `${JSON.stringify({ + schemaVersion: 1, + server: legacyServer, + identity: 'legacy-owner-identity', + token: 'legacy-owner-token', + })}\n`, + { mode: 0o600 }, + ); + const { explicitOptions, options } = parseArgs( + ['--spacetime-data-dir', tempDir], + {}, + ); + const runner = new DevRunner(options, {}, explicitOptions); + runner.state.spacetimeServer = 'http://127.0.0.1:3199'; + globalThis.fetch = vi.fn(); + + await runner.ensureApiServerSpacetimeToken(); + + expect(runner.spacetimeApiToken).toBe('legacy-owner-token'); + expect(globalThis.fetch).not.toHaveBeenCalled(); + expect( + JSON.parse( + readFileSync(resolveLocalSpacetimeApiIdentityPath(tempDir), 'utf8'), + ), + ).toMatchObject({ + schemaVersion: 2, + scope: 'local-data-dir', + identity: 'legacy-owner-identity', + }); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test('同一 data dir 存在多个旧 identity 时失败关闭', async () => { + const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-identity-')); + try { + for (const [port, identity] of [ + [3101, 'legacy-owner-a'], + [3199, 'legacy-owner-b'], + ] as const) { + const server = `http://127.0.0.1:${port}`; + const legacyPath = join( + tempDir, + 'dev-api-identities', + `${createHash('sha256').update(server).digest('hex')}.json`, + ); + mkdirSync(dirname(legacyPath), { recursive: true }); + writeFileSync( + legacyPath, + `${JSON.stringify({ + schemaVersion: 1, + server, + identity, + token: `${identity}-token`, + })}\n`, + { mode: 0o600 }, + ); + } + const { explicitOptions, options } = parseArgs( + ['--spacetime-data-dir', tempDir], + {}, + ); + const runner = new DevRunner(options, {}, explicitOptions); + globalThis.fetch = vi.fn(); + + await expect(runner.ensureApiServerSpacetimeToken()).rejects.toThrow( + '无法安全判断数据库 owner', + ); + expect(globalThis.fetch).not.toHaveBeenCalled(); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + test('外部显式 token 优先于已持久化的本地 API identity', async () => { const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-identity-')); const originalToken = process.env.GENARRATIVE_SPACETIME_TOKEN; @@ -954,10 +1049,7 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.0; ); const runner = new DevRunner(options, {}, explicitOptions); runner.state.spacetimeServer = 'http://127.0.0.1:3101'; - const identityPath = resolveLocalSpacetimeApiIdentityPath( - tempDir, - runner.state.spacetimeServer, - ); + const identityPath = resolveLocalSpacetimeApiIdentityPath(tempDir); mkdirSync(dirname(identityPath), { recursive: true }); const linkedRecordPath = join(tempDir, 'linked-api-identity.json'); writeFileSync( diff --git a/server-rs/crates/api-server/src/process_metrics.rs b/server-rs/crates/api-server/src/process_metrics.rs index d61a7b15f..1ab2d5b90 100644 --- a/server-rs/crates/api-server/src/process_metrics.rs +++ b/server-rs/crates/api-server/src/process_metrics.rs @@ -8,6 +8,12 @@ use tracing::warn; // 进程指标只描述 api-server 自身,不携带请求、用户或作品维度,避免 OTLP 指标高基数膨胀。 pub(crate) fn register_process_metrics() { + // 当前采集实现依赖 Windows API 或 Linux /proc。macOS 等平台不注册 + // observable callbacks,避免每次 OTLP reader 采集时为每个指标重复告警。 + if !cfg!(any(windows, target_os = "linux")) { + return; + } + static REGISTERED: OnceLock<()> = OnceLock::new(); REGISTERED.get_or_init(register_process_metrics_once); }