diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000..9bc7f369 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT=$(git rev-parse --show-toplevel) +cd "$ROOT" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +STAGED=$(git diff --cached --name-only --diff-filter=ACMRTUXB || true) + +if [ -z "$STAGED" ]; then + exit 0 +fi + +echo -e "${YELLOW}🔍 运行 pre-commit 检查...${NC}" + +if ! command -v uv >/dev/null 2>&1; then + echo -e "${RED}❌ 未找到 uv,请先安装 uv${NC}" + exit 1 +fi + +if [ ! -f "pyproject.toml" ]; then + echo -e "${RED}❌ 未找到 pyproject.toml${NC}" + exit 1 +fi + +echo -e "${YELLOW}📝 Python: ruff format --check${NC}" +uv run ruff format --check --exclude "code/" --force-exclude + +echo -e "${YELLOW}📝 Python: ruff check${NC}" +uv run ruff check . --exclude "code/" --force-exclude + +echo -e "${YELLOW}📝 Python: mypy${NC}" +uv run mypy . --exclude "code/" + +if printf '%s\n' "$STAGED" | grep -Eq '^(apps/undefined-console/|src/Undefined/webui/static/js/|biome\.json$|\.github/workflows/(ci|release)\.yml$)'; then + echo -e "${YELLOW}📝 检测到 JS/Tauri 相关改动,运行前端与 Tauri 检查...${NC}" + + if ! command -v npm >/dev/null 2>&1; then + echo -e "${RED}❌ 未找到 npm,请先安装 Node.js${NC}" + exit 1 + fi + + if [ ! -x "apps/undefined-console/node_modules/.bin/biome" ]; then + echo -e "${RED}❌ 缺少 apps/undefined-console/node_modules,请先运行 cd apps/undefined-console && npm install${NC}" + exit 1 + fi + + npm --prefix apps/undefined-console run check +fi + +echo -e "${GREEN}✅ pre-commit 检查通过${NC}" diff --git a/.githooks/pre-tag b/.githooks/pre-tag new file mode 100755 index 00000000..398cdab7 --- /dev/null +++ b/.githooks/pre-tag @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +TAG_NAME="${1:-}" +TAG_VERSION="${TAG_NAME#v}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +cd "$(git rev-parse --show-toplevel)" + +get_pyproject_version() { + grep -E '^version = ' pyproject.toml | sed -E 's/version = "([^"]+)"/\1/' +} + +get_init_version() { + grep -E '^__version__' src/Undefined/__init__.py | sed -E "s/__version__ = '([^']+)'/\1/" | sed -E 's/__version__ = "([^"]+)"/\1/' +} + +PYPROJECT_VERSION=$(get_pyproject_version) +INIT_VERSION=$(get_init_version) + +if [ "$TAG_VERSION" != "$PYPROJECT_VERSION" ] || [ "$TAG_VERSION" != "$INIT_VERSION" ]; then + echo -e "${RED}❌ Tag 版本与仓库版本号不一致${NC}" + echo -e "tag: ${YELLOW}$TAG_VERSION${NC}" + echo -e "pyproject.toml: ${YELLOW}$PYPROJECT_VERSION${NC}" + echo -e "src/Undefined/__init__.py: ${YELLOW}$INIT_VERSION${NC}" + exit 1 +fi + +echo -e "${GREEN}✅ 版本检查通过: $TAG_VERSION${NC}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 440fc88b..d06a7f69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,3 +63,49 @@ jobs: - name: Verify wheel contains resources run: | uv run python -c "import glob, zipfile; whl=glob.glob('dist/*.whl')[0]; z=zipfile.ZipFile(whl); names=set(z.namelist()); required={'config.toml.example','res/prompts/undefined.xml','img/xlwy.jpg'}; missing=sorted([p for p in required if p not in names]); assert not missing, f'missing in wheel: {missing}'" + + console-quality-check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Cache npm and node_modules + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-undefined-console-npm-${{ hashFiles('apps/undefined-console/package-lock.json', 'apps/undefined-console/package.json', 'apps/undefined-console/biome.json') }} + restore-keys: | + ${{ runner.os }}-undefined-console-npm- + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry and target + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + apps/undefined-console/src-tauri -> target + + - name: Install Linux system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf + + - name: Install app dependencies + working-directory: apps/undefined-console + run: npm ci + + - name: Run console checks + working-directory: apps/undefined-console + run: npm run check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 069fb973..1fc77a76 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,9 +9,14 @@ permissions: contents: write id-token: write +env: + PYTHON_VERSION: "3.12" + NODE_VERSION: "22" + APP_DIR: apps/undefined-console + jobs: - build-and-release: - name: Build and Release + verify-python: + name: Verify Python package runs-on: ubuntu-latest environment: release steps: @@ -20,17 +25,14 @@ jobs: with: fetch-depth: 0 - - name: Fetch tags - run: git fetch --tags --force - - name: Install uv uses: astral-sh/setup-uv@v5 with: enable-cache: true - cache-dependency-glob: "uv.lock" + cache-dependency-glob: uv.lock - - name: Install dependencies - run: uv sync --group ci -p 3.12 + - name: Install Python dependencies + run: uv sync --group ci -p ${{ env.PYTHON_VERSION }} - name: Cache Ruff uses: actions/cache@v4 @@ -40,6 +42,11 @@ jobs: restore-keys: | ${{ runner.os }}-ruff- + - name: Run Ruff + run: | + uv run ruff check . + uv run ruff format --check . + - name: Cache Mypy uses: actions/cache@v4 with: @@ -48,11 +55,6 @@ jobs: restore-keys: | ${{ runner.os }}-mypy- - - name: Run Ruff - run: | - uv run ruff check . - uv run ruff format --check . - - name: Run Mypy run: uv run mypy . @@ -64,72 +66,398 @@ jobs: restore-keys: | ${{ runner.os }}-pytest- - - name: Run Tests + - name: Run tests run: uv run pytest tests/ - - name: Build project + - name: Build Python distributions run: uv build - - name: Verify dist contains packaged resources + - name: Verify wheel contains packaged resources run: | uv run python -c "import glob, zipfile; wheels=glob.glob('dist/*.whl'); assert wheels, 'no wheel built'; z=zipfile.ZipFile(wheels[0]); names=set(z.namelist()); required={'config.toml.example','res/prompts/undefined.xml','img/xlwy.jpg'}; missing=sorted([p for p in required if p not in names]); assert not missing, f'missing in wheel: {missing}'" - - name: Create Release + - name: Upload Python distributions + uses: actions/upload-artifact@v4 + with: + name: python-dist + path: dist/* + if-no-files-found: error + + verify-console: + name: Verify console app + runs-on: ubuntu-latest + environment: release + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Cache npm and node_modules + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-undefined-console-npm-${{ hashFiles('apps/undefined-console/package-lock.json', 'apps/undefined-console/package.json', 'apps/undefined-console/biome.json') }} + restore-keys: | + ${{ runner.os }}-undefined-console-npm- + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry and target + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + apps/undefined-console/src-tauri -> target + + - name: Install Linux system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf + + - name: Install app dependencies + working-directory: ${{ env.APP_DIR }} + run: npm ci + + - name: Run console checks + working-directory: ${{ env.APP_DIR }} + run: npm run check + + build-tauri-desktop: + name: Build Tauri desktop (${{ matrix.label }}) + runs-on: ${{ matrix.os }} + environment: release + needs: + - verify-python + - verify-console + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + label: linux-x64 + bundles: appimage,deb + - os: windows-latest + label: windows-x64 + bundles: nsis,msi + - os: macos-13 + label: macos-x64 + bundles: dmg + - os: macos-14 + label: macos-arm64 + bundles: dmg + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Cache npm and node_modules + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-undefined-console-npm-${{ hashFiles('apps/undefined-console/package-lock.json', 'apps/undefined-console/package.json', 'apps/undefined-console/biome.json') }} + restore-keys: | + ${{ runner.os }}-undefined-console-npm- + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry and target + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + apps/undefined-console/src-tauri -> target + + - name: Install Linux system dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf + + - name: Install app dependencies + working-directory: ${{ env.APP_DIR }} + run: npm ci + + - name: Build Tauri bundles + working-directory: ${{ env.APP_DIR }} + shell: bash + run: | + if [ "${{ runner.os }}" = "Linux" ]; then + NO_STRIP=true npm run tauri:build -- --ci --bundles ${{ matrix.bundles }} + else + npm run tauri:build -- --ci --bundles ${{ matrix.bundles }} + fi + + - name: Collect release artifacts + shell: bash env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + LABEL: ${{ matrix.label }} + run: | + copy_single_match() { + local search_root="$1" + local pattern="$2" + local destination="$3" + mapfile -t matches < <(find "$search_root" -type f -name "$pattern" | sort) + if [ "${#matches[@]}" -ne 1 ]; then + echo "Expected exactly one match for $pattern under $search_root, found ${#matches[@]}" >&2 + printf 'Matches:\n%s\n' "${matches[*]}" >&2 + exit 1 + fi + cp "${matches[0]}" "$destination" + } + + mkdir -p release-artifacts + case "$LABEL" in + linux-x64) + copy_single_match "$APP_DIR/src-tauri/target/release/bundle" '*.AppImage' "release-artifacts/Undefined-Console-${TAG}-${LABEL}.AppImage" + copy_single_match "$APP_DIR/src-tauri/target/release/bundle" '*.deb' "release-artifacts/Undefined-Console-${TAG}-${LABEL}.deb" + ;; + windows-x64) + copy_single_match "$APP_DIR/src-tauri/target/release/bundle" '*.exe' "release-artifacts/Undefined-Console-${TAG}-${LABEL}-setup.exe" + copy_single_match "$APP_DIR/src-tauri/target/release/bundle" '*.msi' "release-artifacts/Undefined-Console-${TAG}-${LABEL}.msi" + ;; + macos-x64|macos-arm64) + copy_single_match "$APP_DIR/src-tauri/target/release/bundle" '*.dmg' "release-artifacts/Undefined-Console-${TAG}-${LABEL}.dmg" + ;; + esac + + - name: Upload desktop artifacts + uses: actions/upload-artifact@v4 + with: + name: tauri-${{ matrix.label }} + path: release-artifacts/* + if-no-files-found: error + + build-tauri-android: + name: Build Tauri Android + runs-on: ubuntu-latest + environment: release + needs: + - verify-python + - verify-console + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Cache npm and node_modules + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-undefined-console-npm-${{ hashFiles('apps/undefined-console/package-lock.json', 'apps/undefined-console/package.json', 'apps/undefined-console/biome.json') }} + restore-keys: | + ${{ runner.os }}-undefined-console-npm- + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry and target + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + apps/undefined-console/src-tauri -> target + + - name: Add Android Rust targets + run: | + rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android + + - name: Setup Java 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Cache Android SDK packages + uses: actions/cache@v4 + with: + path: /usr/local/lib/android/sdk + key: android-sdk-34-ndk-27.0.12077973 + id: android-sdk-cache + + - name: Install Android platform packages + if: steps.android-sdk-cache.outputs.cache-hit != 'true' run: | - # 1. 检查是否为注解标签 (Annotated Tag) - TAG_TYPE=$(git cat-file -t ${{ github.ref_name }}) + sdkmanager "platform-tools" "platforms;android-34" "build-tools;34.0.0" "ndk;27.0.12077973" + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Install app dependencies + working-directory: ${{ env.APP_DIR }} + run: npm ci + + - name: Initialize Android project for CI + working-directory: ${{ env.APP_DIR }} + run: npm run tauri:android:init + + - name: Build Android APK + # If signing secrets are wired into the generated Android project, replace the + # debug fallback with a release build. The scaffold keeps the expectation explicit + # while still publishing an installable APK on every non-iOS release. + working-directory: ${{ env.APP_DIR }} + run: npm run tauri:android:debug -- --ci --apk + + - name: Collect Android artifact + shell: bash + env: + TAG: ${{ github.ref_name }} + run: | + copy_single_match() { + local search_root="$1" + local pattern="$2" + local destination="$3" + mapfile -t matches < <(find "$search_root" -type f -name "$pattern" | sort) + if [ "${#matches[@]}" -ne 1 ]; then + echo "Expected exactly one match for $pattern under $search_root, found ${#matches[@]}" >&2 + printf 'Matches:\n%s\n' "${matches[*]}" >&2 + exit 1 + fi + cp "${matches[0]}" "$destination" + } + + mkdir -p release-artifacts + copy_single_match "$APP_DIR/src-tauri" '*.apk' "release-artifacts/Undefined-Console-${TAG}-android-universal.apk" + + - name: Upload Android artifact + uses: actions/upload-artifact@v4 + with: + name: tauri-android + path: release-artifacts/* + if-no-files-found: error + + publish-release: + name: Publish release assets + runs-on: ubuntu-latest + needs: + - verify-python + - build-tauri-desktop + - build-tauri-android + environment: release + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch tags + run: git fetch --tags --force + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + path: release-downloads + merge-multiple: true + + - name: Build release notes + shell: bash + run: | + TAG_NAME="${{ github.ref_name }}" + TAG_TYPE=$(git cat-file -t "$TAG_NAME") TAG_CONTENT="" - if [ "$TAG_TYPE" = "tag" ]; then - # 使用 %(contents) 获取完整的 Tag Message - TAG_CONTENT=$(git tag -l --format='%(contents)' ${{ github.ref_name }}) - # 移除 PGP 签名(如果存在) + TAG_CONTENT=$(git tag -l --format='%(contents)' "$TAG_NAME") TAG_CONTENT=$(echo "$TAG_CONTENT" | sed '/-----BEGIN PGP SIGNATURE-----/,/-----END PGP SIGNATURE-----/d') fi - - # 2. 获取上一个 Tag - PREV_TAG=$(git describe --tags --abbrev=0 ${{ github.ref_name }}^ 2>/dev/null || git rev-list --max-parents=0 HEAD) - - # 3. 初始化 tag_message.txt + PREV_TAG=$(git describe --tags --abbrev=0 "${TAG_NAME}^" 2>/dev/null || git rev-list --max-parents=0 HEAD) if [ -n "$TAG_CONTENT" ]; then echo "$TAG_CONTENT" > tag_message.txt - echo -e "\n---\n" >> tag_message.txt + printf '\n---\n\n' >> tag_message.txt else - touch tag_message.txt + : > tag_message.txt fi + cat >> tag_message.txt <<'NOTES' + ## 📦 Bundles + + - Python: wheel + sdist + - Desktop: Windows (`.exe`, `.msi`), Linux (`.AppImage`, `.deb`), macOS Intel/Apple Silicon (`.dmg`) + - Android: universal `.apk` - # 4. 追加"Detailed Changes"标题 - echo -e "## 📝 Detailed Changes\n" >> tag_message.txt + ## 🔐 Signing expectations - # 5. 自动生成基于 Commit 的详细列表 (Angular 规范分类) - # 仅在有对应 commits 时才显示标题 - FEAT_COMMITS=$(git log ${PREV_TAG}..${{ github.ref_name }} --grep="^feat" --pretty=format:"* %s (%h)" 2>/dev/null || true) + - Windows/Linux artifacts are built directly from CI. + - macOS signing/notarization can be added later via Apple secrets. + - Android currently ships an installable APK on every release; wire signing secrets into the generated Android project to promote it to a signed release build. + + ## 📝 Detailed Changes + NOTES + FEAT_COMMITS=$(git log ${PREV_TAG}..${TAG_NAME} --grep='^feat' --pretty=format:'* %s (%h)' 2>/dev/null || true) if [ -n "$FEAT_COMMITS" ]; then - echo -e "### 🚀 Features" >> tag_message.txt + echo -e "\n### 🚀 Features" >> tag_message.txt echo "$FEAT_COMMITS" >> tag_message.txt - echo -e "\n" >> tag_message.txt fi - - FIX_COMMITS=$(git log ${PREV_TAG}..${{ github.ref_name }} --grep="^fix" --pretty=format:"* %s (%h)" 2>/dev/null || true) + FIX_COMMITS=$(git log ${PREV_TAG}..${TAG_NAME} --grep='^fix' --pretty=format:'* %s (%h)' 2>/dev/null || true) if [ -n "$FIX_COMMITS" ]; then - echo -e "### 🐛 Bug Fixes" >> tag_message.txt + echo -e "\n### 🐛 Bug Fixes" >> tag_message.txt echo "$FIX_COMMITS" >> tag_message.txt - echo -e "\n" >> tag_message.txt fi - - OTHER_COMMITS=$(git log ${PREV_TAG}..${{ github.ref_name }} --grep="^feat\|^fix" --invert-grep --pretty=format:"* %s (%h)" 2>/dev/null || true) + OTHER_COMMITS=$(git log ${PREV_TAG}..${TAG_NAME} --grep='^feat\|^fix' --invert-grep --pretty=format:'* %s (%h)' 2>/dev/null || true) if [ -n "$OTHER_COMMITS" ]; then - echo -e "### 🛠 Maintenance & Others" >> tag_message.txt + echo -e "\n### 🛠 Maintenance & Others" >> tag_message.txt echo "$OTHER_COMMITS" >> tag_message.txt fi - - # 6. 创建 Release - gh release create ${{ github.ref_name }} dist/* \ + + - name: Create GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create ${{ github.ref_name }} release-downloads/* \ --title "Undefined ${{ github.ref_name }}" \ - --notes-file tag_message.txt \ - --generate-notes + --notes-file tag_message.txt + + publish-pypi: + name: Publish Python package to PyPI + runs-on: ubuntu-latest + needs: + - verify-python + - publish-release + environment: release + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: uv.lock + + - name: Download Python dist + uses: actions/download-artifact@v4 + with: + name: python-dist + path: dist - name: Publish to PyPI - run: uv publish + run: uv publish dist/* diff --git a/.gitignore b/.gitignore index 0341f93c..7173eef9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__/ .venv/ venv/ node_modules/ +apps/**/node_modules/ # Logs and temp files *.log @@ -50,7 +51,11 @@ config.local.json .vscode .ruff_cache dist/ +apps/**/dist/ .cache/ +apps/**/src-tauri/target/ +apps/**/src-tauri/gen/ +apps/**/src-tauri/.cargo/ # MCP 配置 config/mcp.json diff --git a/AGENTS.md b/AGENTS.md index cba03cf5..648dbd93 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,28 +1,26 @@ # Repository Guidelines ## Project Structure & Module Organization -Primary code lives in `src/Undefined/`. Keep changes in the matching domain package: `ai/` for model orchestration, `cognitive/` for memory pipelines, `services/` for runtime services, `skills/` for tools/agents/commands, `webui/` for the management UI, and `utils/` for shared helpers. Tests live in `tests/`. Packaged assets and defaults live in `res/`, `img/`, and `config/`. Scripts are in `scripts/`; docs are in `docs/`. +`src/Undefined/` contains the main Python package. Core areas include `ai/`, `cognitive/`, `services/`, `skills/`, and `webui/`. `tests/` holds the pytest suite; add new tests close to the behavior they cover with `test_*.py` names. `apps/undefined-console/` is the Tauri + Vite management client. `code/NagaAgent/` is a Git submodule, so keep upstream syncs and local patches as clearly separated changes. Packaged assets live in `res/`, `img/`, and `config/`; generated outputs like `dist/` and runtime data under `logs/` are not primary edit targets. ## Build, Test, and Development Commands -- `uv sync --group dev -p 3.12`: install the project with contributor tooling. -- `uv run playwright install`: install the browser runtime used by rendering and web-driven features. -- `cp config.toml.example config.toml`: create a local config before running the app. -- `uv run Undefined`: start the bot process directly. -- `uv run Undefined-webui`: start the WebUI manager. Do not run this alongside `Undefined`. -- `uv run ruff format .`: apply formatting. -- `uv run ruff check .`: run lint checks. -- `uv run mypy .`: run strict type checking. -- `uv run pytest tests/`: run the full test suite. -- `uv build --wheel`: build the distribution and verify packaged resources. +Use `uv` for the root project: + +- `uv sync` installs Python dependencies. +- `uv run playwright install` installs browser runtimes used by screenshot features. +- `uv run Undefined-webui` starts the recommended local management entrypoint. +- `uv run pytest tests/` runs the backend test suite. +- `uv run ruff check .` and `uv run ruff format --check .` enforce Python linting and formatting. +- `uv run mypy .` runs strict type checks. +- `uv build --wheel` validates packaging and resource inclusion. + +For the desktop console app, run `cd apps/undefined-console && npm ci && npm run check`. Use `npm run tauri:dev` for local desktop development. ## Coding Style & Naming Conventions -Use 4-space indentation, Python type hints, and `async`/`await` for I/O paths. Let Ruff drive formatting instead of hand-formatting around it. Use `snake_case` for modules, functions, and variables; `PascalCase` for classes; `UPPER_SNAKE_CASE` for constants. Keep modules narrow in scope, and place new Skills content under the correct subtree such as `skills/tools/`, `skills/toolsets/`, or `skills/agents/`. +Use 4-space indentation. Python code should be type-annotated and Ruff-formatted; follow `snake_case` for modules and functions, `PascalCase` for classes, and keep modules focused. WebUI JavaScript in `src/Undefined/webui/static/js/` is formatted with Biome using 4-space indents. `code/NagaAgent/frontend/` follows Vue/TypeScript conventions enforced by ESLint. ## Testing Guidelines -The project uses `pytest` with `pytest-asyncio` (`asyncio_mode = auto`). Name files `tests/test_*.py` and test functions `test_*`. Prefer focused runs while iterating, for example `uv run pytest tests/test_parse_command.py -q`, then finish with the full suite. Add regression coverage for behavior changes in handlers, config loading, Skills discovery, and WebUI routes. +Write tests as `tests/test_.py`. Async tests are supported through `pytest-asyncio`. Add or update tests for behavior changes in APIs, config loading, cognitive memory, and WebUI routes. CI runs the full Python suite plus console checks on pushes and pull requests; no explicit coverage threshold is configured, so use judgment and cover touched paths well. ## Commit & Pull Request Guidelines -Follow the commit style already used in history: `feat: ...`, `fix(scope): ...`, `chore(version): ...`. Keep subjects short and imperative. PRs should include a clear summary, linked issue when applicable, the commands you ran (`ruff`, `mypy`, `pytest`), and screenshots for WebUI changes. If you modify `res/`, `img/`, or `config.toml.example`, note that wheel packaging was checked with `uv build --wheel`. - -## Security & Configuration Tips -Treat `config.toml` as runtime state and avoid committing secrets. Prefer `config.toml.example` for documented defaults. Outputs under `data/` and `logs/` should stay out of feature commits unless the change explicitly targets fixtures or diagnostics. +Recent history follows Conventional Commits with optional scopes, for example `fix(webui): refine launcher return flow` and `docs(build): document linux no-strip workaround`. Keep subjects imperative and concise. For pull requests, include a short impact summary, linked issues, and the commands you ran. Attach screenshots for WebUI or Tauri UI changes. If you change versioned release files, keep `pyproject.toml` and `src/Undefined/__init__.py` in sync. To mirror local checks, enable repo hooks with `git config core.hooksPath .githooks`. diff --git a/CLAUDE.md b/CLAUDE.md index 06e13033..65513104 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,119 +4,101 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## 项目概述 -Undefined 是一个基于 Python 异步架构的高性能 QQ 机器人平台,采用自研 Skills 架构,通过 OneBot V11 协议(NapCat/Lagrange.Core)与 QQ 通信,支持多个智能 Agent 协作处理复杂任务。 +Undefined 是基于 Python asyncio 的高性能 QQ 机器人平台,通过 OneBot V11 协议(NapCat/Lagrange.Core)与 QQ 通信,搭载认知记忆架构和自研 Skills 系统。 -- Python 版本:3.11~3.13 -- 包管理:uv + hatchling -- 主包路径:`src/Undefined/` -- 入口点:`Undefined.main:run`(机器人)、`Undefined.webui:run`(WebUI) - -## 常用命令 +## 开发命令 ```bash -# 安装依赖 -uv sync - -# 启动机器人(二选一,不可同时运行) -uv run Undefined -uv run Undefined-webui - -# 代码检查(ruff 使用默认规则,无额外 ruff.toml) -uv run ruff format . -uv run ruff check . -uv run mypy . # strict 模式,ignore_missing_imports=true - -# 运行测试(pytest-asyncio,asyncio_mode="auto") -uv run pytest tests/ -uv run pytest tests/test_xxx.py # 单个测试文件 -uv run pytest tests/test_xxx.py::test_fn # 单个测试函数 - -# Playwright 浏览器安装(网页浏览功能需要) -uv run playwright install +# 启动 +uv run Undefined-webui # 启动 WebUI 管理控制台(推荐入口) +uv run Undefined # 直接启动 Bot + +# 代码质量(提交前必须全部通过) +uv run ruff format . # 格式化 +uv run ruff check . # Lint +uv run mypy . # 严格类型检查(strict=true) +uv run pytest tests/ # 运行全部测试 +uv run pytest tests/test_xxx.py # 运行单个测试文件 +uv run pytest tests/test_xxx.py::test_func -v # 运行单个测试函数 + +# 前端(仅改动 apps/undefined-console/ 或 webui/static/js/ 时需要) +cd apps/undefined-console && npm install && npm run check + +# Git hooks 安装 +bash scripts/install_git_hooks.sh +# pre-commit 自动执行:ruff format --check + ruff check + mypy +# 含 JS/Tauri 改动时额外执行:Biome + TypeScript + cargo fmt/check ``` -## 核心架构(8 层) - -消息流:用户 → OneBot协议端 → `OneBotClient`(onebot.py) → `MessageHandler`(handlers.py) → 安全检测/命令分发/AI协调 → `QueueManager` → `AIClient` → LLM API → 工具执行 → 回复 - -关键模块: - -| 层 | 核心文件 | 职责 | -|---|---------|------| -| 入口 | `main.py`, `config/manager.py`, `onebot.py`, `context.py` | 启动、配置管理、WS客户端、请求上下文(contextvars) | -| 消息处理 | `handlers.py`, `services/security.py`, `services/command.py`, `services/ai_coordinator.py`, `services/queue_manager.py` | 安全检测、命令分发、AI协调、队列调度 | -| AI核心 | `ai/client.py`, `ai/prompts.py`, `ai/llm.py`, `ai/tooling.py`, `ai/multimodal.py`, `ai/summaries.py` | AI客户端、提示词构建、模型请求、工具管理、多模态分析、总结 | -| Skills | `skills/tools/`, `skills/toolsets/`, `skills/agents/`, `skills/anthropic_skills/` | 基础工具、工具集、智能体、Anthropic Skills | -| 存储 | `memory.py`, `utils/history.py`, `end_summary_storage.py`, `faq.py`, `scheduled_task_storage.py`, `token_usage_storage.py` | 记忆、历史、总结、FAQ、定时任务、Token统计 | -| IO | `utils/io.py` | 异步安全IO(文件锁+原子写入) | - -## Skills 技能系统 - -Skills 是核心扩展机制,分四类,全部通过 `config.json`(OpenAI function calling 格式)+ `handler.py` 自动发现注册: - -### 基础工具 (`skills/tools/{tool_name}/`) -- 原子操作,直接暴露给主 AI -- 必须实现 `async def execute(args: dict, context: dict) -> str` +## 代码规范 -### 工具集 (`skills/toolsets/{category}/{tool_name}/`) -- 按类别分组,注册名为 `{category}.{tool_name}`(如 `render.render_html`) -- 8 大类:group, messages, memory, notices, render, scheduler, mcp, cognitive +- **类型注释**:所有 Python 代码必须有完整类型注释,mypy strict 模式 +- **异步 IO**:磁盘读写必须走 `utils/io.py`(asyncio.to_thread + 跨平台文件锁 + 原子写入),禁止在事件循环中直接阻塞 IO +- **Python 版本**:>=3.11, <3.14(推荐 3.12) +- **测试**:pytest + pytest-asyncio,asyncio_mode = "auto" +- **JS 格式化**:`biome.json` 管理 `webui/static/js/` 目录 -### 智能体 (`skills/agents/{agent_name}/`) -- 每个 Agent 目录包含:`config.json`, `handler.py`, `prompt.md`, `intro.md`, `tools/`(子工具) -- 可选:`mcp.json`(Agent 私有 MCP)、`anthropic_skills/`(Agent 私有 Skills) -- Agent 的 config.json 统一使用 `prompt` 参数接收用户需求 -- Agent handler 应使用 `skills/agents/runner.py` 的 `run_agent_with_tools()` 统一执行入口,它处理 prompt 加载、LLM 迭代、tool call 并发执行、结果回填 -- Agent 通过 `context["ai_client"].request_model()` 调用模型,确保 Token 统计一致 -- 6 个内置 Agent:info_agent, web_agent, file_analysis_agent, naga_code_analysis_agent, entertainment_agent, code_delivery_agent +## 架构分层 -### Anthropic Skills (`skills/anthropic_skills/{skill_name}/SKILL.md`) -- 遵循 agentskills.io 标准,YAML frontmatter(name + description)+ Markdown 正文 -- 注册为 `skills-_-` function tool,渐进式披露 +源码在 `src/Undefined/`,8 层架构: -### handler.py 中可用的 `context` 字典关键 key -- `ai_client`:AIClient 实例,用于 `request_model()` 调用模型 -- `onebot_client`:OneBotClient 实例,用于发送消息/调用 OneBot API -- `config`:全局 Config 对象 -- `end_summaries`:deque,短期总结列表 -- `end_summary_storage`:EndSummaryStorage 实例 -- `conversation_ended`:设为 `True` 通知调用方对话结束 -- `agent_history`:Agent 调用时的上下文历史消息列表 - -## 关键设计模式 - -### 请求上下文 -基于 `contextvars` 的 `RequestContext`(`context.py`),每个请求自动 UUID 追踪,通过 `get_group_id()`, `get_user_id()`, `get_request_id()` 获取。 - -### 队列模型(车站-列车) -`QueueManager` 按模型隔离队列,四级优先级(超管 > 私聊 > @群聊 > 普通群聊),可配置发车间隔,非阻塞调度。 - -### 异步 IO -所有磁盘操作通过 `utils/io.py`,使用 `asyncio.to_thread` + 跨平台文件锁(flock/msvcrt)+ 原子写入(`os.replace`)。**不要**在 handler.py 中直接使用阻塞式 `open()` 读写。 - -### 资源加载 -`utils/resources.py` 实现可覆盖资源加载:运行目录 `./res/...` > 安装包自带资源 > 仓库结构兜底。 - -### 配置系统 -- 主配置:`config.toml`(TOML 格式,支持热更新) -- 配置模型:`config/models.py`,四类模型配置(chat/vision/agent/security) -- 运行时动态数据:`config.local.json`(自动生成,勿提交) -- 需重启的配置项:`log_level`, `logging.file_path/max_size_mb/backup_count/tty_enabled`, `onebot.ws_url/token`, `webui.*` - -## 开发注意事项 - -- Skills 的 handler.py 中**避免引用** `skills/` 外部的本地模块,外部依赖通过 `context` 注入 -- 提示词文件在 `res/prompts/`,主系统提示词为 `undefined.xml`(NagaAgent 模式用 `undefined_nagaagent.xml`) -- 工具名中的 `.` 在发送给模型前会映射为 `config.tools.dot_delimiter`(默认 `-_-`) -- XML 注入防护:结构化 Prompt 使用 `utils/xml.py` 做转义 -- NagaAgent 子模块在 `code/NagaAgent/`,通过 `git submodule update --init --recursive` 初始化 -- 热重载默认开启(`skills.hot_reload = true`),扫描 `skills/` 目录变更自动重载 -- Agent intro 自动生成:按代码 hash 检测变更,生成 `intro.generated.md`,不要手动编辑 -- 数据持久化在 `data/` 目录:`history/`, `faq/`, `token_usage_archives/`, `memory.json`, `end_summaries.json`, `scheduled_tasks.json` - -## 不应提交的文件 +### 消息处理流程 +``` +OneBot WebSocket → onebot.py → handlers.py → SecurityService(注入检测) + → CommandDispatcher(斜杠指令) 或 AICoordinator(AI回复) + → QueueManager(车站-列车模型,4级优先级) → AIClient → LLM API +``` -- `config.toml`(含 API key 等敏感信息) -- `config.local.json`(运行时自动生成的动态数据) -- `data/`(运行时数据目录) -- `intro.generated.md`(Agent 自动生成的介绍文件) +### 关键模块 + +| 目录 | 职责 | +|------|------| +| `ai/` | AI 核心:client.py(主入口)、llm.py(模型请求)、prompts.py(Prompt构建)、tooling.py(工具管理)、multimodal.py(多模态)、model_selector.py(模型选择) | +| `services/` | 运行服务:ai_coordinator.py(协调器+队列投递)、queue_manager.py(车站-列车队列)、command.py(命令分发)、model_pool.py(多模型池)、security.py(安全防护) | +| `skills/` | 热重载技能系统:tools/(原子工具)、toolsets/(9类工具集)、agents/(6个智能体)、commands/(斜杠指令)、anthropic_skills/(SKILL.md知识注入) | +| `cognitive/` | 认知记忆:service.py(入口)、vector_store.py(ChromaDB)、historian.py(后台史官异步改写+侧写合并)、job_queue.py、profile_storage.py | +| `config/` | 配置系统:loader.py(TOML解析+类型化)、models.py(数据模型)、hot_reload.py(热更新) | +| `webui/` | aiohttp Web 管理控制台 | +| `api/` | Management API + Runtime API | +| `utils/` | io.py(异步IO)、history.py(消息历史)、paths.py、logging.py、sender.py 等 | + +### 多模型池分工 +- `ai/model_selector.py` — 纯选择逻辑(策略/偏好/compare状态),无 IO 副作用 +- `services/model_pool.py` — 私聊交互服务,持有 ai/config/sender +- `services/ai_coordinator.py` — 持有 `ModelPoolService`(`self.model_pool`),私聊队列投递时通过它选模型 +- `handlers.py` — 私聊消息调用 `ai_coordinator.model_pool.handle_private_message()` +- 默认关闭:`models.pool_enabled = false`;群聊不参与多模型,始终走主模型 + +### Skills 系统 +- **热重载**:自动扫描 `skills/` 下 `config.json`/`handler.py` 变更并重载 +- **Skills handler 不引用 `skills/` 外的本地模块**,依赖通过 context 注入 +- **Agent 标准结构**:`config.json`(工具定义) + `handler.py`(执行逻辑) + `prompt.md`(系统提示) + `intro.md` + `mcp.json`(可选私有MCP) +- **Agent 直接调用** `ai_client.model_selector.select_agent_config(...)`,无 hasattr + +### 队列模型 +车站-列车模型(QueueManager):按模型隔离队列组,4 级优先级(超管 > 私聊 > @提及 > 普通群聊),普通队列自动修剪保留最新 2 条,非阻塞按节奏发车(默认 1Hz)。 + +### 存储与数据 +- `data/history/` — 消息历史(group_*.json / private_*.json,10000 条限制) +- `data/cognitive/` — ChromaDB 向量库 + profiles/ 侧写 + queues/ 任务队列 +- `data/memory.json` — 置顶备忘录(500 条上限) +- `data/faq/` — FAQ 存储 +- `data/token_usage.jsonl` — Token 统计(自动 gzip 归档) +- `res/prompts/` — 系统提示词模板 + +## 配置系统 + +- 主配置:`config.toml`(从 `config.toml.example` 复制) +- 配置热更新:`config.reload()` 触发回调 +- MCP 配置:`config/mcp.json`(全局)或 `agents//mcp.json`(Agent 私有) +- 脚本 `scripts/sync_config_template.py` 可同步新配置项到已有 config.toml + +## 运维脚本 + +- `scripts/sync_config_template.py` — 同步配置模板新增项(支持 `--dry-run`) +- `scripts/reembed_cognitive.py` — 更换嵌入模型后重建向量库(支持 `--events-only`/`--profiles-only`/`--batch-size`/`--dry-run`) +- `scripts/install_git_hooks.sh` — 安装 Git hooks + +## 跨平台控制台 + +`apps/undefined-console/` — Tauri + Vue3 + TypeScript,支持 Windows/macOS/Linux/Android,连接同一 Management API。 diff --git a/README.md b/README.md index 8d638b3b..27193c49 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,14 @@ Python uv License + PyPI Ask DeepWiki

大鹏一日同风起,扶摇直上九万里。

项目简介

- Undefined 是一个基于 Python 异步架构的高性能 QQ 机器人平台,搭载认知记忆架构,采用自研 Skills 系统,内置多个智能 Agent,支持代码分析、网络搜索、娱乐互动等多模态能力,并提供 WebUI 在线管理。 + Undefined 是一个基于 Python 异步架构的高性能 QQ 机器人平台,搭载认知记忆架构,采用自研 Skills 系统,内置多个智能 Agent,支持代码分析、网络搜索、娱乐互动等多模态能力,并提供 Management-first WebUI 在线管理,以及可连接同一管理服务的 Desktop / Android App

@@ -31,6 +32,8 @@ [点击添加官方实例QQ](https://qm.qq.com/q/cvjJoNysGA) +> **推荐上手方式**:优先运行 `uv run Undefined-webui`。它会先拉起管理控制台,你可以在浏览器里直接补配置、校验配置、查看日志、启动 Bot;后续也可以用桌面端或 Android App 连接这同一个管理入口,实现远程管理。 + ## ⚡ 核心特性 - **Skills 架构**:全新设计的技能系统,将基础工具(Tools)与智能代理(Agents)分层管理,支持自动发现与注册。 @@ -40,8 +43,9 @@ - **认知记忆**(`end.observations` + `cognitive.*`):核心层,AI 在每轮对话中主动观察并提取用户/群聊事实及有价值的自身行为,经后台史官异步改写后存入向量数据库;支持语义检索、时间衰减加权排序、MMR 多样性去重、跨群记忆联动与用户/群聊自动侧写(合并时注入历史事件防止特征丢失),前台零延迟 - **置顶备忘录**(`memory.*`):AI 自身的置顶提醒(自我约束、待办事项),每轮固定注入,支持增删改查 详见 [认知记忆文档](docs/cognitive-memory.md)。 -- **配置热更新 + WebUI**:使用 `config.toml` 配置,支持热更新;提供 WebUI 在线编辑与校验。 -- **Runtime API + OpenAPI**:主进程可暴露本地 Runtime API(探针、记忆只读查询、认知侧写检索、WebUI AI Chat),并提供 OpenAPI 文档。详见 [docs/openapi.md](docs/openapi.md)。 +- **Management-first WebUI**:继续保留 `uv run Undefined-webui` 一键入口;即使 `config.toml` 缺失或未配完,也能先进入管理态补配置、看日志、校验并启动 Bot。 +- **远程管理 + 多端客户端**:浏览器版 WebUI 与新的跨平台控制台共享同一管理面,支持远程管理,并覆盖 `Windows / macOS / Linux / Android` 发布链路。 +- **Management API + Runtime API 分层**:配置、日志、Bot 启停和管理探针由 Management API 提供;主进程 Runtime API 则专注探针、记忆只读查询、认知侧写检索和 WebUI AI Chat。详见 [docs/management-api.md](docs/management-api.md) 与 [docs/openapi.md](docs/openapi.md)。 - **多模型池**:支持配置多个 AI 模型,可轮询、随机选择或用户指定;支持多模型并发比较,选择最佳结果继续对话。详见 [多模型功能文档](docs/multi-model.md)。 - **本地知识库**:将纯文本文件向量化存入 ChromaDB,AI 可通过关键词搜索或语义搜索查询领域知识;支持增量嵌入与自动扫描。详见 [知识库文档](docs/knowledge.md)。 - **访问控制(群/私聊)**:支持 `access.mode` 三种模式(`off` / `blacklist` / `allowlist`)和群/私聊黑白名单;可按策略限制收发范围,避免误触发与误投递。详见 [docs/access-control.md](docs/access-control.md)。 @@ -70,6 +74,7 @@ Undefined 的功能极为丰富,为了让本页面不过于臃肿,我们将各个模块的深入解析与高阶玩法整理成了专题游览图。这里是开启探索的钥匙: - ⚙️ **[安装与部署指南](docs/deployment.md)**:不管你是需要 `pip` 无脑一键安装,还是源码二次开发,这里的排坑指南应有尽有。 +- 🧭 **[Management API 与远程管理](docs/management-api.md)**:WebUI / App 共用的管理接口、认证、配置/日志/Bot 控制与引导探针说明。 - 🛠️ **[配置与热更新说明](docs/configuration.md)**:从模型切换到 MCP 库挂载,全方位掌握 `config.toml` 的高阶配置。 - 💡 **[交互与使用手册](docs/usage.md)**:包含实用的对话示例、多模态解析用法,以及群管家必备的管理员`/指令`。 - 🛡️ **[访问控制说明](docs/access-control.md)**:教你如何精准配置黑白名单,让机器人的使用范围分毫不差。 @@ -77,7 +82,8 @@ Undefined 的功能极为丰富,为了让本页面不过于臃肿,我们将 - 📚 **[本地知识库接入方案](docs/knowledge.md)**:为 AI 挂载本地文本资产,轻松拥抱企业/个人专属 QA。 - 🔄 **[多模型并发竞技](docs/multi-model.md)**:配置多个异构模型,让它们并行运算、同台 PK,从中择优响应。 - ⌨️ **[命令系统与斜杠指令](docs/slash-commands.md)**:查阅所有斜杠指令(`/*`)的详细用法,并学习如何轻松扩展你自己的指令系统。 -- 🌐 **[Runtime API 与 OpenAPI](docs/openapi.md)**:主进程 API、鉴权、探针、记忆/侧写查询和 WebUI 代理调用说明。 +- 🌐 **[Runtime API 与 OpenAPI](docs/openapi.md)**:主进程 Runtime API、鉴权、探针、记忆/侧写查询和运行态集成说明。 +- 🏗️ **[构建指南](docs/build.md)**:Python 包、WebUI、跨平台 App、Android 与 Release 工作流的构建说明。 - 🔧 **[运维脚本](scripts/README.md)**:嵌入模型更换后的向量库重嵌入等维护工具。 - 👨‍💻 **[开发者与拓展中心](docs/development.md)**:代码结构剖析和开发新 Agent 的流程参考及自检命令。 - **[核心技能系统 (Skills) 解析](src/Undefined/skills/README.md)**:全景式掌握什么是 Skills 架构、怎样定制原子工具与子智能体。 @@ -101,13 +107,16 @@ pip install uv # 若未安装 uv uv sync # uv 将自动为你处理兼容的 Python 解释器并安装包 uv run playwright install # 安装浏览器内核(用于页面截图等能力) -# 3. 准备配置文件(请参照 example 按需修改 API URL 和 Key) -cp config.toml.example config.toml - -# 4. 启动可视化控制台(首次启动需使用 webui 修改默认密码和参数) +# 3. 启动管理控制台(推荐入口) uv run Undefined-webui + +# 4. 在 WebUI 中补齐/校验配置,然后直接点击启动 Bot +# 如需先手动准备配置,也可以再执行: +# cp config.toml.example config.toml ``` +> 浏览器是默认入口;如果你下载了 Release 中的桌面端或 Android 安装包,也可以在完成首轮密码设置后,连接到同一个 Management API 地址进行远程管理。 + --- ## 风险提示与免责声明 diff --git a/apps/undefined-console/README.md b/apps/undefined-console/README.md new file mode 100644 index 00000000..771a3a19 --- /dev/null +++ b/apps/undefined-console/README.md @@ -0,0 +1,47 @@ +# Undefined Console + +A `Tauri v2` management client for Undefined that targets the remote Management API and Runtime API. + +## Goals + +- Desktop builds for `Windows`, `macOS`, and `Linux` +- Android build support from the same codebase +- Mobile-friendly shell for connecting to remote instances +- Separate connection profiles for `Management` and `Runtime-only` modes + +## Scripts + +```bash +npm install +npm run dev # Vite web shell +npm run build # Shared frontend bundle +npm run tauri:dev # Desktop development shell +npm run tauri:build # Desktop production bundles +npm run tauri:build:no-strip # Linux 本地打包失败时的 workaround +npm run tauri:android -- --apk +``` + +## Linux local build note + +On some newer Linux distributions, local AppImage packaging may fail during the `linuxdeploy` stage due to `strip` compatibility. If that happens, use: + +```bash +NO_STRIP=true npm run tauri:build +``` + +or: + +```bash +npm run tauri:build:no-strip +``` + +## Connection model + +- **Management mode**: connect with `management_url` and a password/token flow. +- **Runtime-only mode**: connect with `runtime_url` and `X-Undefined-API-Key` compatible credentials. + +This scaffold intentionally keeps the frontend shell lightweight. It is suitable for embedding into both browser-hosted WebUI and Tauri clients. + +## Android notes + +The release workflow expects the Android SDK and Java 17. If signing secrets are configured, the workflow should be upgraded to emit a signed release APK/AAB. The current scaffold always emits an installable APK artifact and makes signing expectations explicit in the CI comments. diff --git a/apps/undefined-console/biome.json b/apps/undefined-console/biome.json new file mode 100644 index 00000000..3ae703df --- /dev/null +++ b/apps/undefined-console/biome.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "files": { + "include": [ + "src/**/*.ts", + "package.json", + "tsconfig.json", + "vite.config.ts", + "src-tauri/tauri.conf.json", + "src-tauri/capabilities/**/*.json" + ] + }, + "linter": { + "rules": { + "correctness": { + "noUnusedVariables": "warn" + } + } + } +} diff --git a/apps/undefined-console/index.html b/apps/undefined-console/index.html new file mode 100644 index 00000000..a9237ee0 --- /dev/null +++ b/apps/undefined-console/index.html @@ -0,0 +1,13 @@ + + + + + + + Undefined Console + + + +
+ + diff --git a/apps/undefined-console/package-lock.json b/apps/undefined-console/package-lock.json new file mode 100644 index 00000000..92491754 --- /dev/null +++ b/apps/undefined-console/package-lock.json @@ -0,0 +1,1591 @@ +{ + "name": "undefined-console", + "version": "3.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "undefined-console", + "version": "3.2.0", + "dependencies": { + "@tauri-apps/api": "^2.3.0", + "@tauri-apps/plugin-http": "^2.3.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@tauri-apps/cli": "^2.3.1", + "typescript": "^5.7.3", + "vite": "^6.2.1" + } + }, + "node_modules/@biomejs/biome": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/api": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", + "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", + "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", + "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.10.1", + "@tauri-apps/cli-darwin-x64": "2.10.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", + "@tauri-apps/cli-linux-arm64-musl": "2.10.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-musl": "2.10.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", + "@tauri-apps/cli-win32-x64-msvc": "2.10.1" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", + "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", + "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.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", + "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", + "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.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", + "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", + "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.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", + "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", + "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", + "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", + "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", + "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", + "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", + "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.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", + "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", + "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.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", + "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-http": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.5.7.tgz", + "integrity": "sha512-+F2lEH/c9b0zSsOXKq+5hZNcd9F4IIKCK1T17RqMwpCmVnx2aoqY8yIBccCd25HTYUb3j6NPVbRax/m00hKG8A==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "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/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "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==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "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 + } + } + } + } +} diff --git a/apps/undefined-console/package.json b/apps/undefined-console/package.json new file mode 100644 index 00000000..94e0c13e --- /dev/null +++ b/apps/undefined-console/package.json @@ -0,0 +1,33 @@ +{ + "name": "undefined-console", + "private": true, + "version": "3.2.0", + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0 --port 1420", + "lint": "biome check src package.json tsconfig.json vite.config.ts src-tauri/tauri.conf.json src-tauri/capabilities/default.json", + "lint:webui": "biome check --config-path ../../biome.json ../../src/Undefined/webui/static/js", + "typecheck": "tsc --noEmit", + "tauri:fmt:check": "cargo fmt --manifest-path src-tauri/Cargo.toml --all --check", + "tauri:check": "cargo check --manifest-path src-tauri/Cargo.toml", + "check": "npm run lint && npm run lint:webui && npm run typecheck && npm run tauri:fmt:check && npm run tauri:check", + "build": "npm run typecheck && vite build", + "preview": "vite preview --host 0.0.0.0 --port 4173", + "tauri:dev": "tauri dev", + "tauri:build": "tauri build", + "tauri:android:init": "tauri android init --ci", + "tauri:android": "tauri android build", + "tauri:android:debug": "tauri android build --debug", + "tauri:build:no-strip": "NO_STRIP=true tauri build" + }, + "dependencies": { + "@tauri-apps/api": "^2.3.0", + "@tauri-apps/plugin-http": "^2.3.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@tauri-apps/cli": "^2.3.1", + "typescript": "^5.7.3", + "vite": "^6.2.1" + } +} diff --git a/apps/undefined-console/src-tauri/.gitignore b/apps/undefined-console/src-tauri/.gitignore new file mode 100644 index 00000000..4cacddf4 --- /dev/null +++ b/apps/undefined-console/src-tauri/.gitignore @@ -0,0 +1,2 @@ +/target +/gen diff --git a/apps/undefined-console/src-tauri/Cargo.lock b/apps/undefined-console/src-tauri/Cargo.lock new file mode 100644 index 00000000..105f8f54 --- /dev/null +++ b/apps/undefined-console/src-tauri/Cargo.lock @@ -0,0 +1,5225 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.12+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 2.13.0", + "selectors", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.13.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.4+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "cookie", + "cookie_store 0.22.1", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d52c379e63da659a483a958110bbde891695a0ecb53e48cc7786d5eda7bb" +dependencies = [ + "bitflags 2.11.0", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.2", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-http" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f069451c4e87e7e2636b7f065a4c52866c4ce5e60e2d53fa1038edb6d184dc" +dependencies = [ + "bytes", + "cookie_store 0.21.1", + "data-url", + "http", + "regex", + "reqwest 0.12.28", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "tokio", + "url", + "urlpattern", +] + +[[package]] +name = "tauri-runtime" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +dependencies = [ + "dunce", + "embed-resource", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 1.0.0+spec-1.1.0", + "toml_parser", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow 0.7.15", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "undefined_console" +version = "3.2.0" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-http", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wry" +version = "0.54.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/apps/undefined-console/src-tauri/Cargo.toml b/apps/undefined-console/src-tauri/Cargo.toml new file mode 100644 index 00000000..994bb06d --- /dev/null +++ b/apps/undefined-console/src-tauri/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "undefined_console" +version = "3.2.0" +description = "Undefined cross-platform management console" +authors = ["Undefined contributors"] +license = "MIT" +edition = "2021" +rust-version = "1.77" + +[build-dependencies] +tauri-build = { version = "2.0.2", features = [] } + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tauri = { version = "2.2.5", features = [] } +tauri-plugin-http = "2" diff --git a/apps/undefined-console/src-tauri/build.rs b/apps/undefined-console/src-tauri/build.rs new file mode 100644 index 00000000..d860e1e6 --- /dev/null +++ b/apps/undefined-console/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/undefined-console/src-tauri/capabilities/default.json b/apps/undefined-console/src-tauri/capabilities/default.json new file mode 100644 index 00000000..b460c72a --- /dev/null +++ b/apps/undefined-console/src-tauri/capabilities/default.json @@ -0,0 +1,27 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "main-capability", + "description": "Default capability for the Undefined Console main window.", + "windows": ["main"], + "permissions": [ + "core:default", + { + "identifier": "http:default", + "allow": [ + { + "url": "http://*:*" + }, + { + "url": "https://*:*" + }, + { + "url": "http://*" + }, + { + "url": "https://*" + } + ], + "deny": [] + } + ] +} diff --git a/apps/undefined-console/src-tauri/icons/128x128.png b/apps/undefined-console/src-tauri/icons/128x128.png new file mode 100644 index 00000000..0a855d8e Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/128x128.png differ diff --git a/apps/undefined-console/src-tauri/icons/128x128@2x.png b/apps/undefined-console/src-tauri/icons/128x128@2x.png new file mode 100644 index 00000000..b33b74fd Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/128x128@2x.png differ diff --git a/apps/undefined-console/src-tauri/icons/32x32.png b/apps/undefined-console/src-tauri/icons/32x32.png new file mode 100644 index 00000000..fd21f865 Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/32x32.png differ diff --git a/apps/undefined-console/src-tauri/icons/Square107x107Logo.png b/apps/undefined-console/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 00000000..fc1f4dc9 Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/Square107x107Logo.png differ diff --git a/apps/undefined-console/src-tauri/icons/Square142x142Logo.png b/apps/undefined-console/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 00000000..085992d4 Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/Square142x142Logo.png differ diff --git a/apps/undefined-console/src-tauri/icons/Square150x150Logo.png b/apps/undefined-console/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 00000000..4b3fa60b Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/Square150x150Logo.png differ diff --git a/apps/undefined-console/src-tauri/icons/Square284x284Logo.png b/apps/undefined-console/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 00000000..c2189dfb Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/Square284x284Logo.png differ diff --git a/apps/undefined-console/src-tauri/icons/Square30x30Logo.png b/apps/undefined-console/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 00000000..994ae7a3 Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/Square30x30Logo.png differ diff --git a/apps/undefined-console/src-tauri/icons/Square310x310Logo.png b/apps/undefined-console/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 00000000..90f71507 Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/Square310x310Logo.png differ diff --git a/apps/undefined-console/src-tauri/icons/Square44x44Logo.png b/apps/undefined-console/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 00000000..95b6d28b Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/Square44x44Logo.png differ diff --git a/apps/undefined-console/src-tauri/icons/Square71x71Logo.png b/apps/undefined-console/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 00000000..4d130f2b Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/Square71x71Logo.png differ diff --git a/apps/undefined-console/src-tauri/icons/Square89x89Logo.png b/apps/undefined-console/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 00000000..9e639117 Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/Square89x89Logo.png differ diff --git a/apps/undefined-console/src-tauri/icons/StoreLogo.png b/apps/undefined-console/src-tauri/icons/StoreLogo.png new file mode 100644 index 00000000..508d1952 Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/StoreLogo.png differ diff --git a/apps/undefined-console/src-tauri/icons/icon.icns b/apps/undefined-console/src-tauri/icons/icon.icns new file mode 100644 index 00000000..fed478c3 Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/icon.icns differ diff --git a/apps/undefined-console/src-tauri/icons/icon.ico b/apps/undefined-console/src-tauri/icons/icon.ico new file mode 100644 index 00000000..842eef18 Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/icon.ico differ diff --git a/apps/undefined-console/src-tauri/icons/icon.png b/apps/undefined-console/src-tauri/icons/icon.png new file mode 100644 index 00000000..17af5db8 Binary files /dev/null and b/apps/undefined-console/src-tauri/icons/icon.png differ diff --git a/apps/undefined-console/src-tauri/src/main.rs b/apps/undefined-console/src-tauri/src/main.rs new file mode 100644 index 00000000..55fb3863 --- /dev/null +++ b/apps/undefined-console/src-tauri/src/main.rs @@ -0,0 +1,8 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_http::init()) + .run(tauri::generate_context!()) + .expect("failed to run Undefined Console app"); +} diff --git a/apps/undefined-console/src-tauri/tauri.conf.json b/apps/undefined-console/src-tauri/tauri.conf.json new file mode 100644 index 00000000..e885fe14 --- /dev/null +++ b/apps/undefined-console/src-tauri/tauri.conf.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Undefined Console", + "version": "3.2.0", + "identifier": "com.undefined.console", + "build": { + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build", + "devUrl": "http://localhost:1420", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "Undefined Console", + "width": 1320, + "height": 860, + "minWidth": 360, + "minHeight": 640, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: asset:; font-src 'self' data: asset:; connect-src 'self' http: https: ipc: tauri: asset:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" + } + }, + "bundle": { + "active": true, + "category": "DeveloperTool", + "shortDescription": "Remote-first management console for Undefined.", + "longDescription": "A desktop and Android control surface for connecting to Undefined management and runtime endpoints.", + "targets": ["appimage", "deb", "dmg", "msi", "nsis"], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico", + "icons/icon.png" + ] + } +} diff --git a/apps/undefined-console/src/main.ts b/apps/undefined-console/src/main.ts new file mode 100644 index 00000000..4af4fbe4 --- /dev/null +++ b/apps/undefined-console/src/main.ts @@ -0,0 +1,836 @@ +import { isTauri } from "@tauri-apps/api/core"; +import { fetch as nativeFetch } from "@tauri-apps/plugin-http"; +import "./style.css"; + +type Lang = "zh" | "en"; +type Theme = "light" | "dark"; +type ProbeKind = "management" | "runtime" | "profile" | "configuration"; + +type ConnectionProfile = { + id: string; + name: string; + host: string; + managementPort: string; + runtimePort: string; + password: string; + notes: string; +}; + +type ProbeState = { + kind: ProbeKind; + status: "idle" | "ok" | "error"; + summary: string; + detail: string; +}; + +type AuthBootstrapPayload = { + accessToken: string; + refreshToken: string; + accessTokenExpiresAt: number; +}; + +const PROFILE_STORAGE_KEY = "undefined.console.profiles"; +const PREFERENCE_STORAGE_KEY = "undefined.console.preferences"; + +const messages = { + zh: { + brand: "Undefined Console", + subtitle: "保存连接、测试端点,然后直接进入真正的 WebUI。", + lang_toggle: "English", + theme_light: "浅色", + theme_dark: "深色", + hero_title: "连接后直接打开远程 WebUI", + hero_copy: + "Tauri 只负责保存连接与做基础探测;真正的管理界面直接使用现有 WebUI,因此样式和功能与浏览器版保持一致。", + button_open: "打开 WebUI", + button_test: "测试连接", + button_seed: "填入本地示例", + button_save: "保存连接", + save_success: "保存成功", + button_new: "新建连接", + button_use: "使用", + button_delete: "删除", + saved_profiles: "已保存连接", + saved_profiles_copy: "连接配置保存在当前设备本地。", + editor_title: "连接编辑器", + editor_copy: "填写同一个 IP/域名,再分别填写 Management 与 Runtime 端口。", + display_name: "显示名称", + host: "IP / 域名", + host_placeholder: "例如:192.168.2.1 或 example.com", + management_port: "Management 端口", + runtime_port: "Runtime 端口", + password: "管理密码", + password_placeholder: "填写后打开 WebUI 时会自动尝试登录", + notes: "备注", + notes_placeholder: "例如:本机、预发布、Android 备用连接", + launcher_hint: + "Tauri 只负责连接管理;真正的后台功能与样式都以 WebUI 为准。", + empty_profiles: "还没有保存的连接。", + empty_probes: "点击“测试连接”后,这里会显示探针结果。", + profile_default_name: "本地管理入口", + profile_default_notes: "推荐默认项,连接本机 Undefined-webui。", + profile_seed_name: "本地示例连接", + profile_seed_notes: "用于快速填入本机 Management / Runtime 默认端口。", + profile_new_name: "新连接", + current_in_use: "当前使用", + saved: "已保存", + status_idle: "待检测", + status_ok: "正常", + status_error: "异常", + management_kind: "Management API", + runtime_kind: "Runtime API", + profile_kind: "连接状态", + configuration_kind: "配置状态", + endpoint_unreachable: "端点不可达", + unauthorized_hint: + "未授权;如果填写了管理密码,打开 WebUI 时会自动尝试登录。", + config_missing: "还没有配置任何端点", + config_missing_detail: "请至少填写 IP/域名与 Management 端口。", + not_configured: "未配置", + endpoint_label: "端点", + tried_label: "尝试地址", + health_label: "健康检查", + openapi_label: "OpenAPI", + base_url_label: "基础地址", + cannot_open: "当前连接缺少 IP/域名或 Management 端口,无法打开 WebUI。", + opening: "正在打开远程 WebUI...", + login_failed: "自动登录失败,将直接打开 WebUI 登录页。", + }, + en: { + brand: "Undefined Console", + subtitle: "Save connections, test endpoints, then open the real WebUI.", + lang_toggle: "中文", + theme_light: "Light", + theme_dark: "Dark", + hero_title: "Open the real remote WebUI after choosing a connection", + hero_copy: + "Tauri only stores connections and runs basic probes. The actual management interface uses the existing WebUI so the look and features stay aligned with the browser version.", + button_open: "Open WebUI", + button_test: "Test connection", + button_seed: "Seed local", + button_save: "Save profile", + save_success: "Saved", + button_new: "New profile", + button_use: "Use", + button_delete: "Delete", + saved_profiles: "Saved profiles", + saved_profiles_copy: "Profiles are stored locally on this device.", + editor_title: "Profile editor", + editor_copy: + "Use one host/IP field, then provide dedicated Management and Runtime ports.", + display_name: "Display name", + host: "Host / IP", + host_placeholder: "For example: 192.168.2.1 or example.com", + management_port: "Management port", + runtime_port: "Runtime port", + password: "Management password", + password_placeholder: "If filled, WebUI will try to sign in automatically", + notes: "Notes", + notes_placeholder: "For example: local, staging, Android fallback", + launcher_hint: + "Tauri only manages connections; the real console UI and features stay in WebUI.", + empty_profiles: "No saved profiles yet.", + empty_probes: + "Probe results will appear here after you click test connection.", + profile_default_name: "Local Management", + profile_default_notes: + "Recommended default. Connect to local Undefined-webui.", + profile_seed_name: "Local seed profile", + profile_seed_notes: + "Use this to quickly fill the local Management / Runtime defaults.", + profile_new_name: "New profile", + current_in_use: "ACTIVE", + saved: "SAVED", + status_idle: "Idle", + status_ok: "Healthy", + status_error: "Error", + management_kind: "Management API", + runtime_kind: "Runtime API", + profile_kind: "Profile", + configuration_kind: "Configuration", + endpoint_unreachable: "Endpoint unreachable", + unauthorized_hint: + "Unauthorized; if a password is filled, WebUI open will try to sign in automatically.", + config_missing: "No endpoints configured", + config_missing_detail: "Add a host/IP and Management port.", + not_configured: "Not configured", + endpoint_label: "Endpoint", + tried_label: "Tried", + health_label: "Health", + openapi_label: "OpenAPI", + base_url_label: "Base URL", + cannot_open: + "This profile is missing a host/IP or Management port, so WebUI cannot be opened.", + opening: "Opening remote WebUI...", + login_failed: + "Automatic login failed. Opening the WebUI login page directly.", + }, +} as const; + +type MessageKey = keyof typeof messages.zh; + +const state = { + lang: loadPreference("lang", "zh" as Lang), + theme: loadPreference("theme", "light" as Theme), + profiles: loadProfiles(), + selectedId: "", + probes: [] as ProbeState[], + infoMessage: "", +}; + +if (!state.selectedId) { + state.selectedId = state.profiles[0]?.id ?? ""; +} + +function t(key: MessageKey): string { + return messages[state.lang][key] ?? key; +} + +function loadPreference( + key: "lang" | "theme", + fallback: T, +): T { + const raw = localStorage.getItem(PREFERENCE_STORAGE_KEY); + if (!raw) return fallback; + try { + const parsed = JSON.parse(raw) as Partial>; + return parsed[key] ?? fallback; + } catch { + return fallback; + } +} + +function parseLegacyUrl(url: string): { host: string; port: string } { + const text = String(url || "").trim(); + if (!text) return { host: "", port: "" }; + try { + const parsed = new URL(text); + return { + host: parsed.hostname || "", + port: parsed.port || (parsed.protocol === "https:" ? "443" : "80"), + }; + } catch { + return { host: "", port: "" }; + } +} + +function loadProfiles(): ConnectionProfile[] { + const raw = localStorage.getItem(PROFILE_STORAGE_KEY); + if (!raw) { + return [ + { + id: crypto.randomUUID(), + name: messages.zh.profile_default_name, + host: "127.0.0.1", + managementPort: "8787", + runtimePort: "8788", + password: "", + notes: messages.zh.profile_default_notes, + }, + ]; + } + try { + const parsed = JSON.parse(raw) as Array>; + if (!Array.isArray(parsed)) return []; + return parsed.map((item) => { + const management = parseLegacyUrl(String(item.managementUrl || "")); + const runtime = parseLegacyUrl(String(item.runtimeUrl || "")); + const host = String(item.host || management.host || runtime.host || ""); + return { + id: String(item.id || crypto.randomUUID()), + name: String(item.name || messages.zh.profile_new_name), + host, + managementPort: String( + item.managementPort || management.port || "8787", + ), + runtimePort: String(item.runtimePort || runtime.port || "8788"), + password: String(item.password || ""), + notes: String(item.notes || ""), + }; + }); + } catch { + return []; + } +} + +function persistProfiles(): void { + localStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(state.profiles)); +} + +function persistPreferences(): void { + localStorage.setItem( + PREFERENCE_STORAGE_KEY, + JSON.stringify({ lang: state.lang, theme: state.theme }), + ); +} + +function applyPreferences(): void { + document.documentElement.lang = state.lang === "zh" ? "zh-CN" : "en"; + document.documentElement.dataset.theme = state.theme; + document + .querySelector('meta[name="theme-color"]') + ?.setAttribute("content", state.theme === "dark" ? "#111315" : "#f6f1eb"); +} + +function selectedProfile(): ConnectionProfile | undefined { + return state.profiles.find((profile) => profile.id === state.selectedId); +} + +function buildManagementUrl(profile: ConnectionProfile): string { + const host = profile.host.trim(); + const port = profile.managementPort.trim(); + if (!host || !port) return ""; + return `http://${host}:${port}`; +} + +function buildRuntimeUrl(profile: ConnectionProfile): string { + const host = profile.host.trim(); + const port = profile.runtimePort.trim(); + if (!host || !port) return ""; + return `http://${host}:${port}`; +} + +function badge(status: ProbeState["status"]): string { + const label = + status === "ok" + ? t("status_ok") + : status === "error" + ? t("status_error") + : t("status_idle"); + return `${escapeHtml(label)}`; +} + +function probeKindLabel(kind: ProbeKind): string { + if (kind === "management") return t("management_kind"); + if (kind === "runtime") return t("runtime_kind"); + if (kind === "profile") return t("profile_kind"); + return t("configuration_kind"); +} + +function toggleLang(): void { + state.lang = state.lang === "zh" ? "en" : "zh"; + persistPreferences(); + render(); +} + +function toggleTheme(): void { + state.theme = state.theme === "light" ? "dark" : "light"; + persistPreferences(); + render(); +} + +async function request(url: string, init: RequestInit = {}): Promise { + if (isTauri()) { + try { + return await nativeFetch(url, { + method: init.method ?? "GET", + headers: init.headers, + body: init.body, + }); + } catch (error) { + const message = String(error || ""); + const shouldFallback = + message.includes("configured scope") || + message.includes("Load failed") || + message.includes("TypeError"); + if (!shouldFallback) throw error; + } + } + return fetch(url, init); +} + +function normalizeBootstrapAuthPayload( + payload: unknown, +): AuthBootstrapPayload | null { + if (!payload || typeof payload !== "object") return null; + const raw = payload as Record & { tokens?: unknown }; + const source = + raw.tokens && typeof raw.tokens === "object" + ? (raw.tokens as Record) + : raw; + const accessToken = String( + source.access_token || source.accessToken || "", + ).trim(); + const refreshToken = String( + source.refresh_token || source.refreshToken || "", + ).trim(); + const accessTokenExpiresAt = + Number.parseInt( + String( + source.access_token_expires_at || source.accessTokenExpiresAt || "0", + ), + 10, + ) || 0; + if (!accessToken) return null; + return { accessToken, refreshToken, accessTokenExpiresAt }; +} + +function encodeBootstrapAuth(payload: AuthBootstrapPayload): string { + const json = JSON.stringify(payload); + const bytes = new TextEncoder().encode(json); + let binary = ""; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + return btoa(binary) + .replaceAll("+", "-") + .replaceAll("/", "_") + .replace(/=+$/u, ""); +} + +async function openWebui(): Promise { + state.infoMessage = ""; + const profile = selectedProfile(); + if (!profile) { + state.infoMessage = t("cannot_open"); + render(); + return; + } + const base = buildManagementUrl(profile); + if (!base) { + state.infoMessage = t("cannot_open"); + render(); + return; + } + + const target = new URL(base); + target.searchParams.set("lang", state.lang); + target.searchParams.set("theme", state.theme); + target.searchParams.set("tab", "overview"); + target.searchParams.set("view", "app"); + target.searchParams.set("client", "native"); + target.searchParams.set("return_to", window.location.href); + + let bootstrapAuth: AuthBootstrapPayload | null = null; + if (profile.password.trim()) { + try { + const loginResponse = await request( + `${base}/api/v1/management/auth/login`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password: profile.password.trim() }), + }, + ); + if (loginResponse.ok) { + const loginPayload = await loginResponse + .clone() + .json() + .catch(() => null); + bootstrapAuth = normalizeBootstrapAuthPayload(loginPayload); + } + if (!bootstrapAuth) { + state.infoMessage = t("login_failed"); + } + } catch { + state.infoMessage = t("login_failed"); + } + } + + if (bootstrapAuth) { + target.hash = `auth=${encodeBootstrapAuth(bootstrapAuth)}`; + } + + state.infoMessage = state.infoMessage || t("opening"); + render(); + window.location.assign(target.toString()); +} + +async function runProbes(): Promise { + const profile = selectedProfile(); + if (!profile) { + state.probes = [ + { + kind: "profile", + status: "error", + summary: t("not_configured"), + detail: t("empty_profiles"), + }, + ]; + render(); + return; + } + + const probes: ProbeState[] = []; + const managementUrl = buildManagementUrl(profile); + const runtimeUrl = buildRuntimeUrl(profile); + if (managementUrl) + probes.push(await probeManagement(managementUrl, profile.password)); + if (runtimeUrl) probes.push(await probeRuntime(runtimeUrl)); + if (!probes.length) { + probes.push({ + kind: "configuration", + status: "error", + summary: t("config_missing"), + detail: t("config_missing_detail"), + }); + } + state.probes = probes; + render(); +} + +async function probeManagement( + base: string, + password: string, +): Promise { + const loginUrl = `${base}/api/v1/management/auth/login`; + const capabilityUrl = `${base}/api/v1/management/probes/capabilities`; + const sessionUrl = `${base}/api/v1/management/auth/session`; + let headers: Record | undefined; + let loginDetail = ""; + + if (password.trim()) { + try { + const loginResp = await request(loginUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password }), + }); + const loginText = await loginResp.text(); + loginDetail = `${t("endpoint_label")}: ${loginUrl}\n\n${truncate(loginText)}`; + if (loginResp.ok) { + const payload = JSON.parse(loginText) as { access_token?: string }; + if (payload.access_token) { + headers = { Authorization: `Bearer ${payload.access_token}` }; + } + } + } catch (error) { + return { + kind: "management", + status: "error", + summary: t("endpoint_unreachable"), + detail: `${t("tried_label")}: ${loginUrl}\n\n${String(error)}`, + }; + } + } + + for (const endpoint of [capabilityUrl, sessionUrl, `${base}/`]) { + try { + const response = await request(endpoint, { method: "GET", headers }); + const text = await response.text(); + const detailParts = [ + `${t("endpoint_label")}: ${endpoint}`, + truncate(text), + ]; + if (loginDetail) detailParts.unshift(loginDetail); + if (response.status === 401 && !password.trim()) { + detailParts.push(t("unauthorized_hint")); + } + return { + kind: "management", + status: response.ok ? "ok" : "error", + summary: `${t("management_kind")} · ${response.status} ${response.statusText}`, + detail: detailParts.join("\n\n"), + }; + } catch (error) { + if (endpoint === `${base}/`) { + return { + kind: "management", + status: "error", + summary: t("endpoint_unreachable"), + detail: `${t("tried_label")}: ${endpoint}\n\n${String(error)}`, + }; + } + } + } + + return { + kind: "management", + status: "error", + summary: t("endpoint_unreachable"), + detail: t("endpoint_unreachable"), + }; +} + +async function probeRuntime(base: string): Promise { + try { + const health = await request(`${base}/health`); + const healthText = await health.text(); + const openapi = await request(`${base}/openapi.json`); + const openapiText = await openapi.text(); + return { + kind: "runtime", + status: health.ok ? "ok" : "error", + summary: `${t("runtime_kind")} · ${health.status} ${health.statusText}`, + detail: `${t("health_label")}: ${truncate(healthText)}\n\n${t("openapi_label")}: ${truncate(openapiText)}`, + }; + } catch (error) { + return { + kind: "runtime", + status: "error", + summary: t("endpoint_unreachable"), + detail: `${t("base_url_label")}: ${base}\n\n${String(error)}`, + }; + } +} + +function render(): void { + applyPreferences(); + const profile = selectedProfile(); + const app = document.querySelector("#app"); + if (!app) return; + const themeButtonLabel = + state.theme === "light" ? t("theme_dark") : t("theme_light"); + + const connectionCards = state.profiles.length + ? state.profiles + .map((entry) => { + const active = entry.id === state.selectedId; + const summary = `${entry.host || t("not_configured")} · ${entry.managementPort || "-"} / ${entry.runtimePort || "-"}`; + return ` +
+
+
+ ${escapeHtml(entry.name)} +
${escapeHtml(summary)}
+
+ ${active ? t("current_in_use") : t("saved")} +
+

${escapeHtml(entry.notes || "")}

+
+ + +
+
+ `; + }) + .join("") + : `
${t("empty_profiles")}
`; + + const probeCards = state.probes.length + ? state.probes + .map( + (probe) => ` +
+
+ ${escapeHtml(probeKindLabel(probe.kind))} + ${badge(probe.status)} +
+
${escapeHtml(probe.summary)}
+
${escapeHtml(probe.detail)}
+
+ `, + ) + .join("") + : `
${t("empty_probes")}
`; + + app.innerHTML = ` +
+
+
+
+
${t("brand")}
+

${t("subtitle")}

+
+ +
+ +
+
+

${t("hero_title")}

+

${t("hero_copy")}

+
+ + + +
+ ${state.infoMessage ? `
${escapeHtml(state.infoMessage)}
` : ""} +
+ +
+
+

${t("saved_profiles")}

+

${t("saved_profiles_copy")}

+
${connectionCards}
+
+ +
+

${t("editor_title")}

+

${t("editor_copy")}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
${t("launcher_hint")}
+
+
+
+
+ +
+

${t("management_kind")} / ${t("runtime_kind")}

+
${probeCards}
+
+
+
+
+ `; + + bindEvents(); +} + +function bindEvents(): void { + document + .querySelector("#profile-form") + ?.addEventListener("submit", (event) => { + event.preventDefault(); + const form = new FormData(event.currentTarget as HTMLFormElement); + const nextProfile: ConnectionProfile = { + id: state.selectedId || crypto.randomUUID(), + name: String(form.get("name") || t("profile_new_name")).trim(), + host: String(form.get("host") || "").trim(), + managementPort: String(form.get("managementPort") || "").trim(), + runtimePort: String(form.get("runtimePort") || "").trim(), + password: String(form.get("password") || "").trim(), + notes: String(form.get("notes") || "").trim(), + }; + const existingIndex = state.profiles.findIndex( + (profile) => profile.id === nextProfile.id, + ); + if (existingIndex >= 0) { + state.profiles.splice(existingIndex, 1, nextProfile); + } else { + state.profiles.unshift(nextProfile); + state.selectedId = nextProfile.id; + } + state.infoMessage = t("save_success"); + persistProfiles(); + render(); + }); + + for (const button of document.querySelectorAll( + "[data-action='select-profile']", + )) { + button.addEventListener("click", () => { + const profileId = button.dataset.profileId; + if (!profileId) return; + state.selectedId = profileId; + state.infoMessage = ""; + render(); + }); + } + + for (const button of document.querySelectorAll( + "[data-action='delete-profile']", + )) { + button.addEventListener("click", () => { + const profileId = button.dataset.profileId; + if (!profileId) return; + state.profiles = state.profiles.filter( + (profile) => profile.id !== profileId, + ); + if (state.selectedId === profileId) { + state.selectedId = state.profiles[0]?.id ?? ""; + } + state.infoMessage = ""; + persistProfiles(); + render(); + }); + } + + document + .querySelector("[data-action='new-profile']") + ?.addEventListener("click", () => { + const profile: ConnectionProfile = { + id: crypto.randomUUID(), + name: t("profile_new_name"), + host: "", + managementPort: "", + runtimePort: "", + password: "", + notes: "", + }; + state.profiles.unshift(profile); + state.selectedId = profile.id; + state.infoMessage = ""; + persistProfiles(); + render(); + }); + + document + .querySelector("[data-action='seed-local']") + ?.addEventListener("click", () => { + const profile: ConnectionProfile = { + id: crypto.randomUUID(), + name: t("profile_seed_name"), + host: "127.0.0.1", + managementPort: "8787", + runtimePort: "8788", + password: "", + notes: t("profile_seed_notes"), + }; + state.profiles.unshift(profile); + state.selectedId = profile.id; + state.infoMessage = ""; + persistProfiles(); + render(); + }); + + document + .querySelector("[data-action='open-webui']") + ?.addEventListener("click", () => { + void openWebui(); + }); + + document + .querySelector("[data-action='test-connection']") + ?.addEventListener("click", () => { + void runProbes(); + }); + + document + .querySelector("[data-action='toggle-lang']") + ?.addEventListener("click", toggleLang); + + document + .querySelector("[data-action='toggle-theme']") + ?.addEventListener("click", toggleTheme); +} + +function escapeHtml(value: string): string { + return value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function escapeAttribute(value: string): string { + return escapeHtml(value).replaceAll("`", "`"); +} + +function truncate(value: string, maxLength = 1200): string { + return value.length > maxLength ? `${value.slice(0, maxLength)}…` : value; +} + +render(); diff --git a/apps/undefined-console/src/style.css b/apps/undefined-console/src/style.css new file mode 100644 index 00000000..761855b8 --- /dev/null +++ b/apps/undefined-console/src/style.css @@ -0,0 +1,293 @@ +:root { + color-scheme: light; + --bg: #f6f1eb; + --bg-secondary: #efe7de; + --panel: rgba(255, 255, 255, 0.82); + --panel-strong: rgba(255, 255, 255, 0.94); + --line: rgba(59, 45, 33, 0.12); + --text: #2f241a; + --muted: #756a61; + --accent: #9c6b3d; + --accent-strong: #7d4b1f; + --success: #35684e; + --danger: #b44b41; + --shadow: 0 24px 60px rgba(65, 42, 21, 0.12); + --radius-xl: 24px; + --radius-lg: 18px; + --radius-md: 14px; + --radius-sm: 10px; + font-family: Inter, "IBM Plex Sans", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + background: radial-gradient(circle at top, rgba(156, 107, 61, 0.12), transparent 24%), var(--bg); + color: var(--text); +} + +:root[data-theme="dark"] { + color-scheme: dark; + --bg: #111315; + --bg-secondary: #171a1d; + --panel: rgba(25, 28, 32, 0.82); + --panel-strong: rgba(30, 34, 39, 0.94); + --line: rgba(255, 255, 255, 0.08); + --text: #efe7de; + --muted: #b9aea3; + --accent: #d08d5f; + --accent-strong: #efab7a; + --success: #7cc79c; + --danger: #ff8d83; + --shadow: 0 28px 60px rgba(0, 0, 0, 0.32); +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + min-height: 100%; + background: var(--bg); +} + +body { + min-height: 100vh; + color: var(--text); +} + +button, +input, +select, +textarea { + font: inherit; +} + +button { + cursor: pointer; +} + +.launcher-shell { + min-height: 100vh; +} + +.launcher-content { + max-width: 1320px; + margin: 0 auto; + padding: max(28px, env(safe-area-inset-top)) 20px max(24px, env(safe-area-inset-bottom)); +} + +.launcher-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 24px; +} + +.brand { + display: flex; + align-items: center; + gap: 12px; + font-weight: 700; + letter-spacing: 0.02em; +} + +.brand-dot { + width: 12px; + height: 12px; + border-radius: 999px; + background: linear-gradient(135deg, var(--accent), #d5a87f); + box-shadow: 0 0 0 6px rgba(156, 107, 61, 0.14); +} + +.launcher-subtitle, +.panel-copy, +.input-help, +.empty-state, +.connection-meta, +.status-meta { + color: var(--muted); + line-height: 1.7; +} + +.sidebar-actions, +.button-row, +.connection-head, +.status-row, +.diagnostics-toolbar { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.hero, +.connection-list, +.status-list, +.form-grid { + display: grid; + gap: 18px; +} + +.hero-card, +.panel, +.connection-card, +.status-card { + border: 1px solid var(--line); + background: var(--panel); + backdrop-filter: blur(18px); + box-shadow: var(--shadow); + border-radius: var(--radius-xl); +} + +.hero-card, +.panel { + padding: 22px; +} + +.hero-title { + margin: 0; + font-size: clamp(30px, 4vw, 48px); + line-height: 1.08; + font-family: "IBM Plex Serif", Georgia, serif; +} + +.hero-subtitle { + margin: 12px 0 0; +} + +.hero-button-row { + margin-top: 22px; +} + +.launcher-message { + margin-top: 14px; +} + +.launcher-grid { + display: grid; + grid-template-columns: minmax(320px, 0.9fr) minmax(0, 1.1fr); + gap: 20px; +} + +.section-title { + margin: 0 0 6px; + font-size: 18px; +} + +.connection-card { + padding: 18px; +} + +.connection-head { + justify-content: space-between; +} + +.badge { + display: inline-flex; + align-items: center; + gap: 6px; + border-radius: 999px; + padding: 5px 10px; + font-size: 12px; + background: color-mix(in srgb, var(--panel-strong) 80%, transparent); + border: 1px solid var(--line); +} + +.badge.ok { + color: var(--success); +} + +.badge.error { + color: var(--danger); +} + +.badge.idle { + color: var(--muted); +} + +.form-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.field { + display: grid; + gap: 8px; +} + +.field.full { + grid-column: 1 / -1; +} + +label { + font-size: 13px; + color: var(--muted); +} + +input, +select, +textarea { + width: 100%; + border: 1px solid color-mix(in srgb, var(--text) 10%, transparent); + background: color-mix(in srgb, var(--panel-strong) 92%, transparent); + color: inherit; + border-radius: var(--radius-sm); + padding: 12px 14px; + outline: none; +} + +input:focus, +select:focus, +textarea:focus { + border-color: color-mix(in srgb, var(--accent) 50%, transparent); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 12%, transparent); +} + +textarea { + min-height: 120px; + resize: vertical; +} + +.primary, +.secondary, +.ghost, +.pref-chip { + border-radius: 999px; + padding: 10px 14px; + border: 1px solid transparent; +} + +.primary { + background: linear-gradient(135deg, var(--accent), var(--accent-strong)); + color: #fff; +} + +.secondary, +.pref-chip { + background: color-mix(in srgb, var(--panel-strong) 90%, transparent); + color: var(--text); + border-color: var(--line); +} + +.ghost { + background: transparent; + color: var(--muted); +} + +pre { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + font-size: 13px; + line-height: 1.55; +} + +@media (max-width: 960px) { + .launcher-grid, + .form-grid { + grid-template-columns: 1fr; + } + + .launcher-header { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/apps/undefined-console/tsconfig.json b/apps/undefined-console/tsconfig.json new file mode 100644 index 00000000..42f310fd --- /dev/null +++ b/apps/undefined-console/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": false, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/apps/undefined-console/vite.config.ts b/apps/undefined-console/vite.config.ts new file mode 100644 index 00000000..871ef851 --- /dev/null +++ b/apps/undefined-console/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + server: { + port: 1420, + strictPort: true, + }, + preview: { + port: 4173, + strictPort: true, + }, + build: { + target: ["es2022", "chrome110", "safari16"], + }, +}); diff --git a/biome.json b/biome.json index 47f5801a..c48ae9cf 100644 --- a/biome.json +++ b/biome.json @@ -1,12 +1,32 @@ { "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "formatter": { + "indentStyle": "space", + "indentWidth": 4 + }, "files": { - "include": ["src/Undefined/webui/static/js/**/*.js"] + "include": ["src/Undefined/webui/static/js/**/*.js"], + "ignore": ["src/Undefined/webui/static/js/vendor/**"] }, "linter": { "rules": { + "complexity": { + "noForEach": "off", + "useOptionalChain": "off", + "useArrowFunction": "off" + }, "correctness": { "noUnusedVariables": "off" + }, + "style": { + "noUnusedTemplateLiteral": "off", + "noParameterAssign": "off", + "useNumberNamespace": "off", + "useTemplate": "off" + }, + "suspicious": { + "noControlCharactersInRegex": "off", + "noRedundantUseStrict": "off" } } } diff --git a/docs/app.md b/docs/app.md new file mode 100644 index 00000000..94a38945 --- /dev/null +++ b/docs/app.md @@ -0,0 +1,184 @@ +# 跨平台 App 与远程管理 + +Undefined 当前的跨平台 App(`apps/undefined-console/`)定位为: + +- 保存多个远程连接档案 +- 对 Management API / Runtime API 做基础连通性探测 +- 在桌面端 / Android 中直接打开 **真正的远程 WebUI** + +也就是说,它不是再维护一套长期独立演进的“第二后台”,而是作为 **连接器 / 启动器**,尽量保证你最终看到的仍然是原生 WebUI 的完整体验与完整功能。 + +## 1. 支持范围 + +首期目标平台: + +- Windows +- macOS +- Linux +- Android + +说明: + +- `iOS` 当前**不在发布矩阵内** +- Release workflow 会在打 tag 时同步构建并上传非 iOS 平台安装包 + +## 2. App 的职责 + +Tauri App 当前不再尝试长期维护一套平行后台。它的职责是: + +1. 保存远程实例连接配置 +2. 做基础连通性探测 +3. 自动尝试登录 Management API +4. 把语言 / 主题偏好透传给远程 WebUI +5. 直接打开真正的 WebUI + +换句话说: + +- **浏览器 WebUI 是唯一真源** +- **Tauri App 负责连接与打开这个真源** + +## 3. 连接模型 + +每个连接现在使用以下字段: + +- `显示名称` +- `IP / 域名` +- `Management 端口` +- `Runtime 端口` +- `管理密码` +- `备注` + +这样做的好处是: + +- 不再要求用户手写两个完整 URL +- 更适合桌面端和移动端录入 +- 同一个主机下的两个服务端口更容易一起管理 + +## 4. Management 与 Runtime 的作用 + +### Management + +作用: + +- 测试 Management API 是否可达 +- 自动登录(如果填写了管理密码) +- 打开真正的 WebUI + +### Runtime + +作用: + +- 做 Runtime API 健康检查 +- 确认 OpenAPI 与运行态是否可达 + +说明:Runtime 只是辅助探测,不负责替代完整后台。 + +## 5. 语言 / 主题 / 自动登录 + +当你在 App 中点击“打开 WebUI”时,当前实现会: + +1. 如果填写了管理密码,先尝试调用登录接口 +2. 再把当前的 `lang/theme` 偏好透传给远程 WebUI +3. 默认直接跳到 WebUI 的 `app` 视图(不是 landing) + +这意味着: + +- App 里切到英文 / 暗色后,打开 WebUI 会尽量保持一致 +- 如果自动登录成功,进入 WebUI 后会直接是已登录状态 +- 如果自动登录失败,则会落到 WebUI 登录页 +- 从 WebUI 退出登录后,会回到主界面而不是停留在管理页 + +## 6. 推荐使用流程 + +1. 先在服务器或本机运行: + +```bash +uv run Undefined-webui +``` + +2. 在浏览器中完成首次密码设置与配置补齐 +3. 启动 Bot +4. 在桌面端或 Android App 中新增一个连接档案 +5. 点击“测试连接”确认 Management / Runtime 可达 +6. 点击“打开 WebUI”后,如已填写管理密码会先自动登录,再进入真正后台 + +## 7. Android 适配说明 + +Android 端仍然走同一套连接模型,但 UI 目标是: + +- 把 App 自身保持得尽量轻 +- 让真正复杂的后台界面依旧由 WebUI 本体负责 +- 尽量减少 Tauri 内再造一套移动版后台的维护成本 + +如果后续要补强 Android 体验,优先方向是: + +- 优化连接页和启动页 +- 优化打开远程 WebUI 前的引导 +- 必要时对 WebUI 本体做移动端适配 + +## 8. Release 产物 + +每次 `v*` tag 发布时,Release workflow 计划同步上传: + +- Python:`wheel` + `sdist` +- Windows:`.exe` + `.msi` +- Linux:`.AppImage` + `.deb` +- macOS:`.dmg`(Intel / Apple Silicon) +- Android:`.apk` + +## 9. 本地开发 + +App 位于: + +- `apps/undefined-console/` + +常用命令: + +```bash +cd apps/undefined-console +npm install +npm run dev +npm run tauri:dev +npm run tauri:build +npm run tauri:build:no-strip +npm run tauri:android:init +npm run tauri:android -- --apk +``` + +### 典型本地调试流程 + +1. 终端 1: + +```bash +cd /data0/Undefined +uv run Undefined-webui +``` + +2. 终端 2: + +```bash +cd /data0/Undefined/apps/undefined-console +npm run tauri:dev +``` + +3. 在 App 中: + +- 填 `IP / 域名` +- 填 `Management 端口` +- 需要时填 `Runtime 端口` +- 填管理密码 +- 点击“测试连接” +- 点击“打开 WebUI” + +## 10. 结论 + +如果你要: + +- **最完整功能** +- **最接近现有 WebUI 的视觉与交互** +- **尽量避免双份前端长期漂移** + +那么当前最稳妥的路线就是: + +- `Undefined-webui` 继续作为真正后台 +- `Tauri App` 作为远程连接器和启动器 diff --git a/docs/build.md b/docs/build.md new file mode 100644 index 00000000..297a6995 --- /dev/null +++ b/docs/build.md @@ -0,0 +1,343 @@ +# 构建指南 + +本文档说明 Undefined 当前仓库中的主要构建方式,包括: + +- Python 包构建(`wheel` / `sdist`) +- `Undefined-webui` 管理控制台的本地开发与验证 +- 跨平台连接器 `apps/undefined-console/` 的桌面端 / Android 构建 +- GitHub Release 工作流的发布矩阵 + +> 约定: +> +> - 浏览器版管理入口仍然是 `uv run Undefined-webui` +> - 桌面端 / Android App 是额外的连接器 / 容器,不替代 `Undefined-webui` +> - Release 工作流默认覆盖 `Windows / macOS / Linux / Android`,不包含 `iOS` + +## 1. 环境准备 + +### Python + +- 推荐 Python:`3.12` +- 支持范围:`3.11` ~ `3.13` +- 推荐使用 `uv` + +安装依赖: + +```bash +uv sync --group dev -p 3.12 +uv run playwright install +``` + +### Node.js / Rust / Tauri + +如果需要构建跨平台控制台,请额外准备: + +- Node.js:建议 `22` +- Rust stable +- Tauri v2 所需系统依赖 +- Android 构建时还需要 Java 17、Android SDK / NDK + +## 2. Python 包构建 + +构建发行包: + +```bash +uv build +``` + +仅构建 wheel: + +```bash +uv build --wheel +``` + +常用本地校验: + +```bash +uv run ruff check . +uv run ruff format --check . +uv run mypy . +uv run pytest tests/ +``` + +如果你修改了 `res/`、`img/`、`config.toml.example` 等打包资源,建议额外检查 wheel 内容是否齐全。 + +## 3. 浏览器版管理控制台 + +### 本地开发入口 + +推荐直接运行: + +```bash +uv run Undefined-webui +``` + +这条命令会启动管理控制台。推荐工作流: + +1. 启动 `Undefined-webui` +2. 在浏览器中打开 WebUI +3. 若 `config.toml` 缺失,先由 WebUI 自动生成模板 +4. 在 WebUI 中补齐配置、保存并校验 +5. 直接点击启动 Bot + +### 说明 + +当前仓库中的 WebUI 静态资源直接由 Python 后端托管,不需要额外执行前端打包命令。 + +如果你修改了: + +- `src/Undefined/webui/templates/` +- `src/Undefined/webui/static/js/` +- `src/Undefined/webui/static/css/` + +建议至少运行: + +```bash +uv run pytest tests/test_webui_management_api.py -q +uv run ruff check src/Undefined/webui +``` + +## 4. 跨平台控制台 App + +当前 App 的职责不是维护一套长期独立的第二后台,而是: + +- 保存连接档案 +- 使用一个 IP/域名 + 两个端口录入方式管理实例 +- 测试 Management / Runtime 入口 +- 自动尝试登录后打开真正的远程 WebUI +- 退出 WebUI 后回到主界面 + +跨平台客户端位于: + +```text +apps/undefined-console/ +``` + +### 安装依赖 + +```bash +cd apps/undefined-console +npm install +``` + +### Web 壳本地调试 + +```bash +npm run dev +``` + +### 桌面端调试 + +```bash +npm run tauri:dev +``` + +### 桌面端构建 + +```bash +npm run tauri:build +``` + +#### Linux 本地 AppImage 备注 + +在部分较新的 Linux 发行版上,本地执行 `npm run tauri:build` 可能在 AppImage 阶段失败,常见表现是: + +- `failed to run linuxdeploy` +- 或 `strip` 无法处理 `.relr.dyn` 段 + +如果你本地遇到这个问题,可直接使用: + +```bash +NO_STRIP=true npm run tauri:build +``` + +或者使用仓库里补好的快捷脚本: + +```bash +npm run tauri:build:no-strip +``` + +如果你只想在本机先验证 Linux 安装包链路,也可以优先只打 `deb`: + +```bash +npm run tauri:build:no-strip -- --bundles deb +``` + +这个问题主要是本机 `linuxdeploy` / `strip` 工具链兼容性导致,不一定代表项目代码或 Tauri 配置有问题。 + +### Android 初始化与构建 + +首次或 CI 环境中,先初始化 Android 项目: + +```bash +npm run tauri:android:init +``` + +构建 Android: + +```bash +npm run tauri:android -- --apk +``` + +当前仓库也保留了 debug APK 构建路径,便于在 CI 中稳定产出可安装 APK: + +```bash +npm run tauri:android:debug -- --apk +``` + +## 5. 平台依赖说明 + +### Linux + +构建 Tauri 桌面端通常需要: + +```bash +sudo apt-get update +sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf +``` + +### macOS + +- 可构建 `.dmg` +- 如需签名 / notarization,需额外配置 Apple 证书与 secrets +- 当前 Release workflow 预留了后续接入空间 + +### Windows + +- 可构建 `.exe` / `.msi` +- 若后续需要代码签名,可在 CI 中继续补证书配置 + +### Android + +需要: + +- Java 17 +- Android SDK +- Android NDK +- Rust Android targets + +CI 当前会优先保证每个 release 至少产出一个可安装 APK;如果后续接入正式签名,可再升级为签名 release APK / AAB。 + +## 6. Git Hook 集成 + +仓库内已提供可版本化维护的 git hooks: + +```text +.githooks/pre-commit +.githooks/pre-tag +``` + +安装方式: + +```bash +bash scripts/install_git_hooks.sh +``` + +安装后: + +- `pre-commit` 会继续执行 Python 的 `ruff + mypy` +- 当提交里包含 `apps/undefined-console/`、`src/Undefined/webui/static/js/`、`biome.json`、CI workflow 相关改动时,还会额外执行: + - `Biome` 检查 + - `TypeScript` 类型检查 + - `cargo fmt --check` + - `cargo check` + +说明:如果本机还没安装 App 依赖,需要先执行: + +```bash +cd apps/undefined-console +npm install +``` + +## 7. Release 工作流 + +当前 tag 发布工作流位于: + +```text +.github/workflows/release.yml +``` + +触发条件: + +- 推送 tag:`v*` + +工作流主要阶段: + +1. `verify-python` + - `ruff` + - `mypy` + - `pytest` + - `uv build` +2. `build-tauri-desktop` + - Linux:`.AppImage` + `.deb` + - Windows:`.exe` + `.msi` + - macOS x64:`.dmg` + - macOS arm64:`.dmg` +3. `build-tauri-android` + - Android 通用 `.apk` +4. `publish-release` + - 汇总所有产物并上传 GitHub Release +5. `publish-pypi` + - 发布 Python 包到 PyPI + +## 8. Release 产物矩阵 + +每次正式 Release 计划上传: + +- Python + - `wheel` + - `sdist` +- Windows + - `.exe` + - `.msi` +- Linux + - `.AppImage` + - `.deb` +- macOS + - Intel `.dmg` + - Apple Silicon `.dmg` +- Android + - `.apk` + +`iOS` 当前不在发布矩阵内。 + +## 9. 推荐的本地构建顺序 + +如果你准备发布一个版本,建议本地先按以下顺序自检: + +```bash +uv sync --group dev -p 3.12 +uv run ruff check . +uv run ruff format --check . +uv run mypy . +uv run pytest tests/ +uv build +``` + +如果本次改动涉及 App: + +```bash +cd apps/undefined-console +npm install +npm run check # 代码检查(lint + typecheck + cargo check) +# 注意:npm run tauri:build 会自动执行 npm run build,无需手动构建前端 +``` + +如果本次改动涉及 Android 构建链: + +```bash +npm run tauri:android:init +npm run tauri:android:debug -- --apk +``` + +## 10. 常见建议 + +- 日常开发和首次部署,优先验证 `uv run Undefined-webui` 全流程是否顺畅。 +- 改动管理接口时,优先补 `tests/test_webui_management_api.py`。 +- 改动发布矩阵时,务必同步更新 `README.md` 与本文件。 +- 改动 App 构建脚本时,注意同时检查 `apps/undefined-console/package.json` 与 `.github/workflows/release.yml`。 diff --git a/docs/deployment.md b/docs/deployment.md index 9b5f5294..9e24783b 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -48,6 +48,19 @@ Undefined-webui > `Undefined-webui` 会在检测到当前目录缺少 `config.toml` 时,自动从 `config.toml.example` 生成一份,便于直接在 WebUI 中修改。 > 提示:资源文件已随包发布,支持在非项目根目录启动;如需自定义内容,请参考下方说明。 +## Management-first 推荐流程 + +推荐把 `Undefined-webui` 当作默认入口: + +1. 运行 `Undefined-webui` 或 `uv run Undefined-webui` +2. 在浏览器中打开管理控制台 +3. 若 `config.toml` 缺失,WebUI 会自动生成模板 +4. 在控制台中补齐配置、保存并校验 +5. 直接点击启动 Bot +6. 若需要远程管理,再使用桌面端或 Android App 连接到这个 Management API + +这样可以避免“先手写配置、再反复命令行重启”的冷启动成本,尤其适合首次部署与远程运维。 + ### 完整日志(排查用) 如果你希望保留完整安装/运行日志,可直接重定向到文件: diff --git a/docs/development.md b/docs/development.md index 7e57a5d6..54a1d2d0 100644 --- a/docs/development.md +++ b/docs/development.md @@ -41,7 +41,7 @@ src/Undefined/ - 细粒度的工具白名单权限配置。 - 允许特定 Agent 相互调用的灵活授权。 -## 开发自检 (代码规范与类型检查) +## 开发自检 (代码规范、类型检查与 Git Hook) 修改完代码提交之前,请始终进行以下命令确认代码符合质量标准并拦截潜在的类型隐患: @@ -49,5 +49,35 @@ src/Undefined/ uv run ruff format . uv run ruff check . uv run mypy . +uv run pytest tests/ ``` -> **注意**:项目严格遵守类型注释规范,`mypy .` 通过是代码入库的前提条件。 + +如果你改动了跨平台控制台 `apps/undefined-console/`,或改动了 `src/Undefined/webui/static/js/` 里的前端脚本,建议额外执行: + +```bash +cd apps/undefined-console +npm install +npm run check +``` + +### Git Hook + +仓库内已经提供可维护的 git hook: + +```text +.githooks/pre-commit +.githooks/pre-tag +``` + +启用方式: + +```bash +bash scripts/install_git_hooks.sh +``` + +启用后: + +- `pre-commit` 会执行 Python 的 `ruff + mypy` +- 当提交包含 JS / Tauri / WebUI 前端相关改动时,还会自动执行 `Biome + TypeScript + cargo fmt/check` + +> **注意**:项目严格遵守类型注释规范,`mypy .` 通过是代码入库的前提条件;跨平台控制台相关改动则以 `npm run check` 通过为准。 diff --git a/docs/management-api.md b/docs/management-api.md new file mode 100644 index 00000000..04d084f0 --- /dev/null +++ b/docs/management-api.md @@ -0,0 +1,184 @@ +# Management API 与远程管理 + +本文档说明 `Undefined-webui` 暴露的 **Management API**。它负责控制面能力:登录、token 刷新、配置编辑、日志读取、Bot 启停、bootstrap 探针,以及对主进程 Runtime API 的代理访问。 + +> 简单理解: +> +> - **Management API**:面向 WebUI / 桌面端 / Android App 的管理入口 +> - **Runtime API**:面向机器人主进程运行态与集成能力 +> +> 配置缺失、配置损坏、主进程未启动时,优先依赖 Management API 完成恢复。 + +## 1. 推荐入口 + +推荐先运行: + +```bash +uv run Undefined-webui +``` + +然后: + +1. 在浏览器中打开 WebUI +2. 登录并修改密码(首次) +3. 编辑配置、保存并校验 +4. 直接在控制台启动 Bot +5. 如需远程管理,再让桌面端或 Android App 连接同一个 Management API 地址 + +## 2. 鉴权模型 + +Management API 兼容两套鉴权: + +- **Cookie Session**:浏览器 WebUI 继续沿用 +- **Bearer Token**:桌面端 / Android App 推荐使用 + +### 登录 + +- `POST /api/v1/management/auth/login` + +请求体: + +```json +{ + "password": "your-webui-password" +} +``` + +成功后返回: + +```json +{ + "success": true, + "access_token": "...", + "refresh_token": "...", + "expires_in": 900, + "refresh_expires_in": 28800, + "access_token_expires_at": 1741420800000 +} +``` + +同时浏览器会收到兼容旧逻辑的 session cookie。 + +### 刷新 token + +- `POST /api/v1/management/auth/refresh` + +请求体: + +```json +{ + "refresh_token": "..." +} +``` + +### 查询会话 + +- `GET /api/v1/management/auth/session` + +返回当前是否已认证、是否仍在使用默认密码、配置文件是否存在,以及控制面的 capabilities。 + +### 登出 + +- `POST /api/v1/management/auth/logout` + +会撤销当前 bearer token / refresh token / cookie session。 + +## 3. Bootstrap 与 Capabilities 探针 + +### Bootstrap probe + +- `GET /api/v1/management/probes/bootstrap` + +用于描述“当前实例是否已经进入可修复/可启动状态”。典型字段: + +- `config_exists` +- `toml_valid` +- `config_valid` +- `validation_error` +- `using_default_password` +- `runtime_enabled` +- `runtime_reachable` +- `advice` + +适合 WebUI 和 App 在首页直接判断: + +- 是否需要先补配置 +- 是否需要先改密码 +- 是否只是 Runtime 未启动 +- 是否可以直接点“启动 Bot” + +### Capabilities probe + +- `GET /api/v1/management/probes/capabilities` + +用于告诉客户端当前控制面支持哪些能力,例如: + +- token 鉴权 +- bootstrap probe +- runtime proxy +- config read/write/validate +- logs read/stream +- bot start/stop/update-restart +- 桌面端 / Android 客户端支持 + +## 4. 配置相关接口 + +- `GET /api/v1/management/config` +- `POST /api/v1/management/config` +- `GET /api/v1/management/config/summary` +- `POST /api/v1/management/config/patch` +- `POST /api/v1/management/config/validate` +- `POST /api/v1/management/config/sync-template` + +说明: + +- `config`:读取/整体保存源文本 +- `summary`:读取结构化配置摘要与注释映射 +- `patch`:按路径增量修改 +- `validate`:先做 TOML 语法校验,再做严格配置校验 +- `sync-template`:把 `config.toml.example` 中新增字段/注释同步到当前配置 + +## 5. 日志、系统与 Bot 控制 + +### 日志 + +- `GET /api/v1/management/logs` +- `GET /api/v1/management/logs/files` +- `GET /api/v1/management/logs/stream` + +### 系统信息 + +- `GET /api/v1/management/system` + +### Bot 控制 + +- `GET /api/v1/management/status` +- `POST /api/v1/management/bot/start` +- `POST /api/v1/management/bot/stop` +- `POST /api/v1/management/update-restart` + +## 6. Runtime 代理 + +Management API 会把运行态相关能力统一代理到主进程 Runtime API,便于客户端只连接一个入口: + +- `GET /api/v1/management/runtime/meta` +- `GET /api/v1/management/runtime/openapi` +- `GET /api/v1/management/runtime/probes/internal` +- `GET /api/v1/management/runtime/probes/external` +- `GET /api/v1/management/runtime/memory` +- `GET /api/v1/management/runtime/cognitive/events` +- `GET /api/v1/management/runtime/cognitive/profiles` +- `GET /api/v1/management/runtime/cognitive/profile/{entity_type}/{entity_id}` +- `POST /api/v1/management/runtime/chat` +- `GET /api/v1/management/runtime/chat/history` + +## 7. 适用场景 + +推荐使用 Management API 的场景: + +- 首次部署,`config.toml` 还没补齐 +- 配置损坏,主进程起不来 +- 想远程修改配置、看日志、重启 Bot +- 想让桌面端 / Android App 共用统一管理入口 + +如果你只关心主进程的运行态能力和第三方集成,可以直接看 [Runtime API / OpenAPI 文档](openapi.md)。 diff --git a/docs/openapi.md b/docs/openapi.md index db5a6f26..29e9f58d 100644 --- a/docs/openapi.md +++ b/docs/openapi.md @@ -1,6 +1,13 @@ # Runtime API / OpenAPI 指南 -本文档说明 Undefined 主进程暴露的 Runtime API(含 OpenAPI 文档),以及 WebUI 如何安全调用。 +本文档说明 Undefined 主进程暴露的 Runtime API(含 OpenAPI 文档),以及 WebUI / App 如何通过 Management API 代理安全调用。 + +> 职责边界: +> +> - **Management API**:配置、日志、Bot 启停、bootstrap probe、远程管理入口 +> - **Runtime API**:主进程运行态能力(探针、记忆、认知、AI Chat) +> +> 如果你想看控制面接口,请同时参考 [Management API 文档](management-api.md)。 ## 1. 配置项 diff --git a/pyproject.toml b/pyproject.toml index e9d13689..37b302fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "Undefined-bot" -version = "3.1.2" +version = "3.2.0" description = "QQ bot platform with cognitive memory architecture and multi-agent Skills, via OneBot V11." readme = "README.md" authors = [ diff --git a/scripts/install_git_hooks.sh b/scripts/install_git_hooks.sh new file mode 100755 index 00000000..7c0832da --- /dev/null +++ b/scripts/install_git_hooks.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +cd "$ROOT" + +git config core.hooksPath .githooks + +echo "已设置 git hooksPath -> .githooks" +echo "当前 pre-commit: .githooks/pre-commit" diff --git a/src/Undefined/__init__.py b/src/Undefined/__init__.py index 44e62cdb..e75919d6 100644 --- a/src/Undefined/__init__.py +++ b/src/Undefined/__init__.py @@ -1,3 +1,3 @@ """Undefined - A high-performance, highly scalable QQ group and private chat robot based on a self-developed architecture.""" -__version__ = "3.1.2" +__version__ = "3.2.0" diff --git a/src/Undefined/ai/client.py b/src/Undefined/ai/client.py index 7df04e19..44900489 100644 --- a/src/Undefined/ai/client.py +++ b/src/Undefined/ai/client.py @@ -649,6 +649,7 @@ async def request_model( **kwargs: Any, ) -> dict[str, Any]: tools = self.tool_manager.maybe_merge_agent_tools(call_type, tools) + message_count_for_transport = len(messages) if not ( isinstance(transport_state, dict) and transport_state.get("previous_response_id") @@ -656,7 +657,6 @@ async def request_model( messages, tools = await self._maybe_prefetch_tools( messages, tools, call_type ) - message_count_for_transport = len(messages) return await self._requester.request( model_config=model_config, messages=messages, diff --git a/src/Undefined/api/app.py b/src/Undefined/api/app.py index 0cf5528e..67efc5c7 100644 --- a/src/Undefined/api/app.py +++ b/src/Undefined/api/app.py @@ -12,14 +12,15 @@ from datetime import datetime from typing import Any, Awaitable, Callable from urllib.parse import urlsplit - from aiohttp import ClientSession, ClientTimeout, web from aiohttp.web_response import Response from Undefined import __version__ +from Undefined.config import load_webui_settings from Undefined.context import RequestContext from Undefined.context_resource_registry import collect_context_resources from Undefined.render import render_html_to_image, render_markdown_to_html # noqa: F401 +from Undefined.utils.cors import is_allowed_cors_origin, normalize_origin from Undefined.utils.recent_messages import get_recent_messages_prefer_local from Undefined.utils.xml import escape_xml_attr, escape_xml_text @@ -131,6 +132,25 @@ def _json_error(message: str, status: int = 400) -> Response: return web.json_response({"error": message}, status=status) +def _apply_cors_headers(request: web.Request, response: web.StreamResponse) -> None: + origin = normalize_origin(str(request.headers.get("Origin") or "")) + settings = load_webui_settings() + response.headers.setdefault("Vary", "Origin") + response.headers.setdefault("Access-Control-Allow-Methods", "GET,POST,OPTIONS") + response.headers.setdefault( + "Access-Control-Allow-Headers", + "Authorization, Content-Type, X-Undefined-API-Key", + ) + response.headers.setdefault("Access-Control-Max-Age", "86400") + if origin and is_allowed_cors_origin( + origin, + configured_host=str(settings.url or ""), + configured_port=settings.port, + ): + response.headers.setdefault("Access-Control-Allow-Origin", origin) + response.headers.setdefault("Access-Control-Allow-Credentials", "true") + + def _optional_query_param(request: web.Request, key: str) -> str | None: raw = request.query.get(key) if raw is None: @@ -437,12 +457,21 @@ async def _auth_middleware( request: web.Request, handler: Callable[[web.Request], Awaitable[web.StreamResponse]], ) -> web.StreamResponse: + response: web.StreamResponse + if request.method == "OPTIONS": + response = web.Response(status=204) + _apply_cors_headers(request, response) + return response if request.path.startswith("/api/"): expected = str(self._context.config_getter().api.auth_key or "") provided = request.headers.get(_AUTH_HEADER, "") if not expected or provided != expected: - return _json_error("Unauthorized", status=401) - return await handler(request) + response = _json_error("Unauthorized", status=401) + _apply_cors_headers(request, response) + return response + response = await handler(request) + _apply_cors_headers(request, response) + return response app = web.Application(middlewares=[_auth_middleware]) app["runtime_api_context"] = self._context diff --git a/src/Undefined/utils/cors.py b/src/Undefined/utils/cors.py new file mode 100644 index 00000000..1fe07f83 --- /dev/null +++ b/src/Undefined/utils/cors.py @@ -0,0 +1,57 @@ +import ipaddress +from urllib.parse import urlsplit + + +def normalize_origin(origin: str) -> str: + text = str(origin or "").strip().rstrip("/") + if not text: + return "" + return text.lower() + + +def _is_loopback_host(host: str) -> bool: + text = str(host or "").strip().lower() + if not text: + return False + if text == "localhost": + return True + try: + return ipaddress.ip_address(text).is_loopback + except ValueError: + return False + + +def _is_loopback_http_origin(origin: str) -> bool: + try: + parsed = urlsplit(origin) + except ValueError: + return False + if parsed.scheme not in {"http", "https"}: + return False + return _is_loopback_host(parsed.hostname or "") + + +def is_allowed_cors_origin( + origin: str, + *, + configured_host: str = "", + configured_port: int | None = None, + extra_origins: set[str] | None = None, +) -> bool: + normalized = normalize_origin(origin) + if not normalized: + return False + + allowed = {normalize_origin(item) for item in extra_origins or set() if item} + host = str(configured_host or "").strip() + if host: + for scheme in ("http", "https"): + allowed.add(normalize_origin(f"{scheme}://{host}")) + if configured_port is not None: + allowed.add(normalize_origin(f"{scheme}://{host}:{configured_port}")) + + if normalized in allowed: + return True + if normalized == "tauri://localhost": + return True + return _is_loopback_http_origin(normalized) diff --git a/src/Undefined/webui/app.py b/src/Undefined/webui/app.py index dbf0b890..df41ff79 100644 --- a/src/Undefined/webui/app.py +++ b/src/Undefined/webui/app.py @@ -8,8 +8,15 @@ from aiohttp import web from Undefined.config import load_webui_settings, get_config_manager, get_config +from Undefined.utils.cors import is_allowed_cors_origin, normalize_origin from .core import BotProcessController, SessionStore from .routes import routes +from .routes._shared import ( + BOT_APP_KEY, + REDIRECT_TO_CONFIG_ONCE_APP_KEY, + SESSION_STORE_APP_KEY, + SETTINGS_APP_KEY, +) from .utils import ensure_config_toml from Undefined.utils.self_update import ( GitUpdatePolicy, @@ -77,6 +84,39 @@ def _gzip_compress(data: bytes) -> bytes | None: return compressed if len(compressed) < len(data) else None +def _apply_cors_headers(request: web.Request, response: web.StreamResponse) -> None: + origin = normalize_origin(str(request.headers.get("Origin") or "")) + settings = load_webui_settings() + response.headers.setdefault("Vary", "Origin") + response.headers.setdefault("Access-Control-Allow-Methods", "GET,POST,OPTIONS") + response.headers.setdefault( + "Access-Control-Allow-Headers", + "Authorization, Content-Type, X-Auth-Token, X-Refresh-Token, X-Undefined-API-Key", + ) + response.headers.setdefault("Access-Control-Max-Age", "86400") + if origin and is_allowed_cors_origin( + origin, + configured_host=str(settings.url or ""), + configured_port=settings.port, + ): + response.headers.setdefault("Access-Control-Allow-Origin", origin) + response.headers.setdefault("Access-Control-Allow-Credentials", "true") + + +@web.middleware +async def cors_middleware( + request: web.Request, + handler: Callable[[web.Request], Awaitable[web.StreamResponse]], +) -> web.StreamResponse: + response: web.StreamResponse + if request.method == "OPTIONS": + response = web.Response(status=204) + else: + response = await handler(request) + _apply_cors_headers(request, response) + return response + + @web.middleware async def gzip_middleware( request: web.Request, @@ -191,7 +231,7 @@ async def on_startup(app: web.Application) -> None: marker = Path("data/cache/pending_bot_autostart") if marker.exists(): marker.unlink(missing_ok=True) - bot: BotProcessController = app["bot"] + bot = app[BOT_APP_KEY] await bot.start() logger.info("[WebUI] 检测到自动恢复标记,已尝试启动机器人进程") except Exception: @@ -199,7 +239,7 @@ async def on_startup(app: web.Application) -> None: async def on_shutdown(app: web.Application) -> None: - bot: BotProcessController = app["bot"] + bot = app[BOT_APP_KEY] status = bot.status() if not status.get("running"): logger.info("[WebUI] 正在关闭,无运行中的机器人进程") @@ -217,18 +257,20 @@ async def on_cleanup(app: web.Application) -> None: def create_app(*, redirect_to_config_once: bool = False) -> web.Application: - app = web.Application(middlewares=[gzip_middleware, security_headers_middleware]) + app = web.Application( + middlewares=[cors_middleware, gzip_middleware, security_headers_middleware] + ) # 初始化核心组件 - app["bot"] = BotProcessController() - app["session_store"] = SessionStore() + app[BOT_APP_KEY] = BotProcessController() + app[SESSION_STORE_APP_KEY] = SessionStore() # 配置 WebUI 设置热重载 config_manager = get_config_manager() - app["settings"] = load_webui_settings() + app[SETTINGS_APP_KEY] = load_webui_settings() # 一次性客户端重定向提示(由 index 处理) - app["redirect_to_config_once"] = redirect_to_config_once + app[REDIRECT_TO_CONFIG_ONCE_APP_KEY] = redirect_to_config_once def _on_config_change(config: Any, changes: dict[str, Any]) -> None: webui_keys = {"webui_url", "webui_port", "webui_password"} @@ -236,7 +278,7 @@ def _on_config_change(config: Any, changes: dict[str, Any]) -> None: changes ): logger.info("[WebUI] 检测到 WebUI 配置变更,正在热重载设置...") - app["settings"] = load_webui_settings() + app[SETTINGS_APP_KEY] = load_webui_settings() config_manager.subscribe(_on_config_change) diff --git a/src/Undefined/webui/core.py b/src/Undefined/webui/core.py index f6cd7d4f..f8028f28 100644 --- a/src/Undefined/webui/core.py +++ b/src/Undefined/webui/core.py @@ -1,4 +1,5 @@ import asyncio +from dataclasses import dataclass import logging import os import signal @@ -10,30 +11,94 @@ logger = logging.getLogger(__name__) SESSION_TTL_SECONDS = 8 * 60 * 60 +ACCESS_TOKEN_TTL_SECONDS = 15 * 60 +REFRESH_TOKEN_TTL_SECONDS = 8 * 60 * 60 BOT_COMMAND = ("uv", "run", "Undefined") +@dataclass +class SessionEntry: + expires_at: float + kind: str = "session" + + class SessionStore: - def __init__(self, ttl_seconds: int = SESSION_TTL_SECONDS) -> None: + def __init__( + self, + ttl_seconds: int = SESSION_TTL_SECONDS, + access_ttl_seconds: int = ACCESS_TOKEN_TTL_SECONDS, + refresh_ttl_seconds: int = REFRESH_TOKEN_TTL_SECONDS, + ) -> None: self._ttl_seconds = ttl_seconds - self._sessions: dict[str, float] = {} + self._access_ttl_seconds = access_ttl_seconds + self._refresh_ttl_seconds = refresh_ttl_seconds + self._sessions: dict[str, SessionEntry] = {} - def create(self) -> str: + def create(self, *, ttl_seconds: int | None = None, kind: str = "session") -> str: token = secrets.token_urlsafe(32) - self._sessions[token] = time.time() + self._ttl_seconds + ttl = self._ttl_seconds if ttl_seconds is None else ttl_seconds + self._sessions[token] = SessionEntry(expires_at=time.time() + ttl, kind=kind) return token - def is_valid(self, token: str | None) -> bool: + def _resolve_entry(self, token: str | None) -> SessionEntry | None: if not token: - return False - expiry = self._sessions.get(token) - if not expiry: - return False - if expiry < time.time(): + return None + entry = self._sessions.get(token) + if entry is None: + return None + if entry.expires_at < time.time(): self._sessions.pop(token, None) + return None + return entry + + def is_valid( + self, + token: str | None, + *, + allowed_kinds: set[str] | None = None, + ) -> bool: + entry = self._resolve_entry(token) + if entry is None: + return False + if allowed_kinds is not None and entry.kind not in allowed_kinds: return False return True + def get_expiry_ms(self, token: str | None) -> int: + entry = self._resolve_entry(token) + if entry is None: + return 0 + return int(entry.expires_at * 1000) + + def get_kind(self, token: str | None) -> str | None: + entry = self._resolve_entry(token) + return None if entry is None else entry.kind + + def issue_auth_tokens(self) -> dict[str, int | str]: + access_token = self.create( + ttl_seconds=self._access_ttl_seconds, + kind="access", + ) + refresh_token = self.create( + ttl_seconds=self._refresh_ttl_seconds, + kind="refresh", + ) + return { + "access_token": access_token, + "refresh_token": refresh_token, + "expires_in": self._access_ttl_seconds, + "refresh_expires_in": self._refresh_ttl_seconds, + "access_token_expires_at": self.get_expiry_ms(access_token), + } + + def refresh_auth_tokens( + self, refresh_token: str | None + ) -> dict[str, int | str] | None: + if not self.is_valid(refresh_token, allowed_kinds={"refresh"}): + return None + self.revoke(refresh_token) + return self.issue_auth_tokens() + def revoke(self, token: str | None) -> None: if not token: return diff --git a/src/Undefined/webui/routes/_auth.py b/src/Undefined/webui/routes/_auth.py index 52689781..bcab3ecc 100644 --- a/src/Undefined/webui/routes/_auth.py +++ b/src/Undefined/webui/routes/_auth.py @@ -6,12 +6,17 @@ from Undefined.config.loader import CONFIG_PATH, DEFAULT_WEBUI_PASSWORD from Undefined.config import get_config_manager, load_webui_settings from ._shared import ( + auth_capabilities, routes, SESSION_COOKIE, TOKEN_COOKIE, SESSION_TTL_SECONDS, + SETTINGS_APP_KEY, + get_bearer_token, + get_refresh_token, get_settings, get_session_store, + get_valid_auth_token, check_auth, _get_client_ip, _is_local_request, @@ -22,6 +27,36 @@ from ..utils import read_config_source, apply_patch, load_comment_map, render_toml +def _build_session_payload( + request: web.Request, authenticated: bool +) -> dict[str, object]: + settings = get_settings(request) + sessions = get_session_store(request) + active_token = get_valid_auth_token(request) + token_kind = sessions.get_kind(active_token) + return { + "authenticated": authenticated, + "using_default_password": settings.using_default_password, + "config_exists": settings.config_exists, + "summary": f"{settings.url}:{settings.port} | ready" + if authenticated + else "locked", + "capabilities": { + "management_api_v1": True, + "runtime_proxy": True, + "bootstrap_probe": True, + "desktop_app": True, + "android_app": True, + "auth": auth_capabilities(), + }, + "token_kind": token_kind, + "access_token_expires_at": sessions.get_expiry_ms(active_token) + if token_kind == "access" + else 0, + } + + +@routes.post("/api/v1/management/auth/login") @routes.post("/api/login") async def login_handler(request: web.Request) -> Response: try: @@ -59,8 +94,17 @@ async def login_handler(request: web.Request) -> Response: if password == settings.password: _clear_login_failures(client_ip) - token = get_session_store(request).create() - resp = web.json_response({"success": True}) + sessions = get_session_store(request) + token = sessions.create() + auth_tokens = sessions.issue_auth_tokens() + resp = web.json_response( + { + "success": True, + **auth_tokens, + "tokens": auth_tokens, + "capabilities": auth_capabilities(), + } + ) resp.set_cookie( SESSION_COOKIE, token, @@ -68,6 +112,13 @@ async def login_handler(request: web.Request) -> Response: samesite="Lax", max_age=SESSION_TTL_SECONDS, ) + resp.set_cookie( + TOKEN_COOKIE, + str(auth_tokens["refresh_token"]), + httponly=True, + samesite="Lax", + max_age=SESSION_TTL_SECONDS, + ) return resp ok, block_seconds = _record_login_failure(client_ip) @@ -86,31 +137,66 @@ async def login_handler(request: web.Request) -> Response: ) +@routes.get("/api/v1/management/auth/session") @routes.get("/api/session") async def session_handler(request: web.Request) -> Response: - settings = get_settings(request) authenticated = check_auth(request) - summary = f"{settings.url}:{settings.port} | ready" if authenticated else "locked" - return web.json_response( + return web.json_response(_build_session_payload(request, authenticated)) + + +@routes.post("/api/v1/management/auth/refresh") +async def refresh_handler(request: web.Request) -> Response: + try: + data = await request.json() + except Exception: + data = {} + refresh_token = get_refresh_token(request, data) + tokens = get_session_store(request).refresh_auth_tokens(refresh_token) + if tokens is None: + return web.json_response( + {"success": False, "error": "Unauthorized"}, status=401 + ) + resp = web.json_response( { - "authenticated": authenticated, - "using_default_password": settings.using_default_password, - "config_exists": settings.config_exists, - "summary": summary, + "success": True, + **tokens, + "tokens": tokens, + "capabilities": auth_capabilities(), } ) + resp.set_cookie( + TOKEN_COOKIE, + str(tokens["refresh_token"]), + httponly=True, + samesite="Lax", + max_age=SESSION_TTL_SECONDS, + ) + return resp +@routes.post("/api/v1/management/auth/logout") @routes.post("/api/logout") async def logout_handler(request: web.Request) -> Response: - token = request.cookies.get(SESSION_COOKIE) or request.headers.get("X-Auth-Token") - get_session_store(request).revoke(token) + try: + data = await request.json() + except Exception: + data = {} + sessions = get_session_store(request) + for token in { + get_valid_auth_token(request), + request.cookies.get(SESSION_COOKIE), + request.headers.get("X-Auth-Token"), + get_refresh_token(request, data), + get_bearer_token(request), + }: + sessions.revoke(token) resp = web.json_response({"success": True}) resp.del_cookie(SESSION_COOKIE) resp.del_cookie(TOKEN_COOKIE) return resp +@routes.post("/api/v1/management/auth/password") @routes.post("/api/password") async def password_handler(request: web.Request) -> Response: try: @@ -175,7 +261,7 @@ async def password_handler(request: web.Request) -> Response: render_toml(patched, comments=load_comment_map()), encoding="utf-8" ) get_config_manager().reload() - request.app["settings"] = load_webui_settings() + request.app[SETTINGS_APP_KEY] = load_webui_settings() get_session_store(request).clear() resp = web.json_response({"success": True, "message": "Password updated"}) diff --git a/src/Undefined/webui/routes/_bot.py b/src/Undefined/webui/routes/_bot.py index 0d46692c..3d4d72f8 100644 --- a/src/Undefined/webui/routes/_bot.py +++ b/src/Undefined/webui/routes/_bot.py @@ -20,6 +20,18 @@ def _truncate(text: str, *, max_chars: int = 12000) -> str: return text[:max_chars] + "\n... (truncated)" +async def _run_bot_action(request: web.Request, action: str) -> Response: + if not check_auth(request): + return web.json_response({"error": "Unauthorized"}, status=401) + bot = get_bot(request) + if action == "start": + return web.json_response(await bot.start()) + if action == "stop": + return web.json_response(await bot.stop()) + return web.json_response({"error": "Invalid action"}, status=400) + + +@routes.get("/api/v1/management/status") @routes.get("/api/status") async def status_handler(request: web.Request) -> Response: bot = get_bot(request) @@ -31,19 +43,23 @@ async def status_handler(request: web.Request) -> Response: return web.json_response(status) +@routes.post("/api/v1/management/bot/{action}") @routes.post("/api/bot/{action}") async def bot_action_handler(request: web.Request) -> Response: - if not check_auth(request): - return web.json_response({"error": "Unauthorized"}, status=401) - action = request.match_info["action"] - bot = get_bot(request) - if action == "start": - return web.json_response(await bot.start()) - elif action == "stop": - return web.json_response(await bot.stop()) - return web.json_response({"error": "Invalid action"}, status=400) + return await _run_bot_action(request, request.match_info["action"]) + + +@routes.post("/api/v1/management/bot/start") +async def bot_start_handler(request: web.Request) -> Response: + return await _run_bot_action(request, "start") + + +@routes.post("/api/v1/management/bot/stop") +async def bot_stop_handler(request: web.Request) -> Response: + return await _run_bot_action(request, "stop") +@routes.post("/api/v1/management/update-restart") @routes.post("/api/update-restart") async def update_restart_handler(request: web.Request) -> Response: if not check_auth(request): diff --git a/src/Undefined/webui/routes/_config.py b/src/Undefined/webui/routes/_config.py index e5128569..a284d807 100644 --- a/src/Undefined/webui/routes/_config.py +++ b/src/Undefined/webui/routes/_config.py @@ -1,8 +1,11 @@ import tomllib +from pathlib import Path +from tempfile import NamedTemporaryFile from aiohttp import web from aiohttp.web_response import Response +from Undefined.config.loader import Config from Undefined.config.loader import CONFIG_PATH, load_toml_data from Undefined.config import get_config_manager from ._shared import routes, check_auth @@ -20,6 +23,7 @@ ) +@routes.get("/api/v1/management/config") @routes.get("/api/config") async def get_config_handler(request: web.Request) -> Response: if not check_auth(request): @@ -27,6 +31,7 @@ async def get_config_handler(request: web.Request) -> Response: return web.json_response(read_config_source()) +@routes.post("/api/v1/management/config") @routes.post("/api/config") async def save_config_handler(request: web.Request) -> Response: if not check_auth(request): @@ -53,6 +58,46 @@ async def save_config_handler(request: web.Request) -> Response: return web.json_response({"success": False, "error": str(e)}, status=500) +@routes.post("/api/v1/management/config/validate") +async def validate_config_handler(request: web.Request) -> Response: + if not check_auth(request): + return web.json_response({"error": "Unauthorized"}, status=401) + try: + data = await request.json() + except Exception: + return web.json_response({"error": "Invalid JSON"}, status=400) + content = str(data.get("content") or "") + syntax_valid, syntax_message = validate_toml(content) + strict_valid = False + strict_message = "" + if syntax_valid: + with NamedTemporaryFile( + "w", encoding="utf-8", suffix=".toml", delete=False + ) as f: + f.write(content) + f.flush() + temp_path = Path(f.name) + try: + Config.load(temp_path, strict=True) + strict_valid = True + strict_message = "OK" + except Exception as exc: + strict_valid = False + strict_message = str(exc) + finally: + temp_path.unlink(missing_ok=True) + return web.json_response( + { + "success": syntax_valid and strict_valid, + "syntax_valid": syntax_valid, + "syntax_message": syntax_message, + "strict_valid": strict_valid, + "strict_message": strict_message, + } + ) + + +@routes.get("/api/v1/management/config/summary") @routes.get("/api/config/summary") async def config_summary_handler(request: web.Request) -> Response: if not check_auth(request): @@ -65,6 +110,7 @@ async def config_summary_handler(request: web.Request) -> Response: return web.json_response({"data": ordered, "comments": comments}) +@routes.post("/api/v1/management/config/patch") @routes.post("/api/patch") async def config_patch_handler(request: web.Request) -> Response: if not check_auth(request): @@ -100,6 +146,7 @@ async def config_patch_handler(request: web.Request) -> Response: ) +@routes.post("/api/v1/management/config/sync-template") @routes.post("/api/config/sync-template") async def sync_config_template_handler(request: web.Request) -> Response: if not check_auth(request): diff --git a/src/Undefined/webui/routes/_index.py b/src/Undefined/webui/routes/_index.py index 14f317f6..c48a2b5b 100644 --- a/src/Undefined/webui/routes/_index.py +++ b/src/Undefined/webui/routes/_index.py @@ -4,7 +4,12 @@ from aiohttp.web_response import Response from Undefined import __version__ -from ._shared import routes, TEMPLATE_DIR, get_settings +from ._shared import ( + REDIRECT_TO_CONFIG_ONCE_APP_KEY, + TEMPLATE_DIR, + get_settings, + routes, +) @routes.get("/") @@ -22,9 +27,27 @@ async def index_handler(request: web.Request) -> Response: if license_file.exists(): license_text = license_file.read_text(encoding="utf-8") - lang = request.cookies.get("undefined_lang", "zh") - theme = request.cookies.get("undefined_theme", "light") - redirect_to_config = bool(request.app.get("redirect_to_config_once")) + query_lang = str(request.query.get("lang") or "").strip().lower() + query_theme = str(request.query.get("theme") or "").strip().lower() + query_view = str(request.query.get("view") or "").strip().lower() + query_tab = str(request.query.get("tab") or "").strip().lower() + query_client = str(request.query.get("client") or "").strip().lower() + query_return_to = str(request.query.get("return_to") or "").strip() + + lang = ( + query_lang + if query_lang in {"zh", "en"} + else request.cookies.get("undefined_lang", "zh") + ) + theme = ( + query_theme + if query_theme in {"light", "dark"} + else request.cookies.get("undefined_theme", "light") + ) + initial_view = query_view if query_view in {"landing", "app"} else "landing" + initial_tab = query_tab or "overview" + launcher_mode = query_client in {"native", "app"} + redirect_to_config = bool(request.app.get(REDIRECT_TO_CONFIG_ONCE_APP_KEY, False)) initial_state = { "using_default_password": settings.using_default_password, @@ -34,11 +57,29 @@ async def index_handler(request: web.Request) -> Response: "license": license_text, "lang": lang, "theme": theme, + "initial_tab": initial_tab, + "launcher_mode": launcher_mode, + "return_to": query_return_to if launcher_mode else "", } if redirect_to_config: - request.app["redirect_to_config_once"] = False + request.app[REDIRECT_TO_CONFIG_ONCE_APP_KEY] = False initial_state_json = json.dumps(initial_state).replace(" Path | None: return next((p for p in _list_all_log_files(log_dir) if p.name == file_name), None) +@routes.get("/api/v1/management/logs") @routes.get("/api/logs") async def logs_handler(request: web.Request) -> Response: if not check_auth(request): @@ -95,6 +96,7 @@ async def logs_handler(request: web.Request) -> Response: return web.Response(text=tail_file(target_path, lines)) +@routes.get("/api/v1/management/logs/files") @routes.get("/api/logs/files") async def logs_files_handler(request: web.Request) -> Response: if not check_auth(request): @@ -138,6 +140,7 @@ async def logs_files_handler(request: web.Request) -> Response: return web.json_response({"files": files, "current": current_name}) +@routes.get("/api/v1/management/logs/stream") @routes.get("/api/logs/stream") async def logs_stream_handler(request: web.Request) -> web.StreamResponse: if not check_auth(request): diff --git a/src/Undefined/webui/routes/_runtime.py b/src/Undefined/webui/routes/_runtime.py index df8c6d76..77e4a4f0 100644 --- a/src/Undefined/webui/routes/_runtime.py +++ b/src/Undefined/webui/routes/_runtime.py @@ -191,6 +191,7 @@ async def _proxy_runtime_stream( ) +@routes.get("/api/v1/management/runtime/meta") @routes.get("/api/runtime/meta") async def runtime_meta_handler(request: web.Request) -> Response: if not check_auth(request): @@ -206,6 +207,7 @@ async def runtime_meta_handler(request: web.Request) -> Response: ) +@routes.get("/api/v1/management/runtime/openapi") @routes.get("/api/runtime/openapi") async def runtime_openapi_handler(request: web.Request) -> Response: if not check_auth(request): @@ -216,6 +218,7 @@ async def runtime_openapi_handler(request: web.Request) -> Response: ) +@routes.get("/api/v1/management/runtime/probes/internal") @routes.get("/api/runtime/probes/internal") async def runtime_probe_internal_handler(request: web.Request) -> Response: if not check_auth(request): @@ -223,6 +226,7 @@ async def runtime_probe_internal_handler(request: web.Request) -> Response: return await _proxy_runtime(method="GET", path="/api/v1/probes/internal") +@routes.get("/api/v1/management/runtime/probes/external") @routes.get("/api/runtime/probes/external") async def runtime_probe_external_handler(request: web.Request) -> Response: if not check_auth(request): @@ -230,6 +234,7 @@ async def runtime_probe_external_handler(request: web.Request) -> Response: return await _proxy_runtime(method="GET", path="/api/v1/probes/external") +@routes.get("/api/v1/management/runtime/memory") @routes.get("/api/runtime/memory") async def runtime_memory_handler(request: web.Request) -> Response: if not check_auth(request): @@ -241,6 +246,7 @@ async def runtime_memory_handler(request: web.Request) -> Response: ) +@routes.get("/api/v1/management/runtime/cognitive/events") @routes.get("/api/runtime/cognitive/events") async def runtime_cognitive_events_handler(request: web.Request) -> Response: if not check_auth(request): @@ -252,6 +258,7 @@ async def runtime_cognitive_events_handler(request: web.Request) -> Response: ) +@routes.get("/api/v1/management/runtime/cognitive/profiles") @routes.get("/api/runtime/cognitive/profiles") async def runtime_cognitive_profiles_handler(request: web.Request) -> Response: if not check_auth(request): @@ -263,6 +270,7 @@ async def runtime_cognitive_profiles_handler(request: web.Request) -> Response: ) +@routes.get("/api/v1/management/runtime/cognitive/profile/{entity_type}/{entity_id}") @routes.get("/api/runtime/cognitive/profile/{entity_type}/{entity_id}") async def runtime_cognitive_profile_handler(request: web.Request) -> Response: if not check_auth(request): @@ -275,6 +283,7 @@ async def runtime_cognitive_profile_handler(request: web.Request) -> Response: ) +@routes.post("/api/v1/management/runtime/chat") @routes.post("/api/runtime/chat") async def runtime_chat_handler(request: web.Request) -> web.StreamResponse: if not check_auth(request): @@ -308,6 +317,7 @@ async def runtime_chat_handler(request: web.Request) -> web.StreamResponse: ) +@routes.get("/api/v1/management/runtime/chat/history") @routes.get("/api/runtime/chat/history") async def runtime_chat_history_handler(request: web.Request) -> Response: if not check_auth(request): @@ -319,6 +329,7 @@ async def runtime_chat_history_handler(request: web.Request) -> Response: ) +@routes.get("/api/v1/management/runtime/chat/image") @routes.get("/api/runtime/chat/image") async def runtime_chat_image_handler(request: web.Request) -> web.StreamResponse: if not check_auth(request): @@ -332,6 +343,7 @@ async def runtime_chat_image_handler(request: web.Request) -> web.StreamResponse return web.FileResponse(path=image_path) +@routes.get("/api/v1/management/runtime/chat/file") @routes.get("/api/runtime/chat/file") async def runtime_chat_file_handler(request: web.Request) -> web.StreamResponse: """提供 WebUI 虚拟私聊文件下载。""" diff --git a/src/Undefined/webui/routes/_shared.py b/src/Undefined/webui/routes/_shared.py index ac6216ab..8169398f 100644 --- a/src/Undefined/webui/routes/_shared.py +++ b/src/Undefined/webui/routes/_shared.py @@ -1,17 +1,28 @@ import ipaddress import time from pathlib import Path -from typing import Any, cast +from typing import Any from aiohttp import web -from ..core import BotProcessController, SessionStore +from Undefined.config import WebUISettings +from ..core import ( + ACCESS_TOKEN_TTL_SECONDS, + REFRESH_TOKEN_TTL_SECONDS, + BotProcessController, + SessionStore, +) routes = web.RouteTableDef() STATIC_DIR = Path(__file__).parent.parent / "static" TEMPLATE_DIR = Path(__file__).parent.parent / "templates" +BOT_APP_KEY = web.AppKey("bot", BotProcessController) +SESSION_STORE_APP_KEY = web.AppKey("session_store", SessionStore) +SETTINGS_APP_KEY = web.AppKey("settings", WebUISettings) +REDIRECT_TO_CONFIG_ONCE_APP_KEY = web.AppKey("redirect_to_config_once", bool) + SESSION_COOKIE = "undefined_webui" TOKEN_COOKIE = "undefined_webui_token" SESSION_TTL_SECONDS = 8 * 60 * 60 @@ -24,21 +35,88 @@ def get_bot(request: web.Request) -> BotProcessController: - return cast(BotProcessController, request.app["bot"]) + return request.app[BOT_APP_KEY] def get_session_store(request: web.Request) -> SessionStore: - return cast(SessionStore, request.app["session_store"]) + return request.app[SESSION_STORE_APP_KEY] + + +def get_settings(request: web.Request) -> WebUISettings: + return request.app[SETTINGS_APP_KEY] -def get_settings(request: web.Request) -> Any: - return request.app["settings"] +def get_bearer_token(request: web.Request) -> str | None: + auth_header = str(request.headers.get("Authorization") or "").strip() + if not auth_header: + return None + prefix = "bearer " + if auth_header.lower().startswith(prefix): + token = auth_header[len(prefix) :].strip() + return token or None + return None + + +def get_auth_token(request: web.Request) -> str | None: + return ( + get_bearer_token(request) + or request.cookies.get(SESSION_COOKIE) + or request.headers.get("X-Auth-Token") + ) + + +def get_auth_tokens(request: web.Request) -> list[str]: + tokens = [ + get_bearer_token(request), + request.cookies.get(SESSION_COOKIE), + request.headers.get("X-Auth-Token"), + ] + result: list[str] = [] + for token in tokens: + text = str(token or "").strip() + if text and text not in result: + result.append(text) + return result + + +def get_refresh_token( + request: web.Request, payload: dict[str, Any] | None = None +) -> str | None: + if payload is not None: + value = payload.get("refresh_token") or payload.get("refreshToken") + text = str(value or "").strip() + if text: + return text + return ( + request.cookies.get(TOKEN_COOKIE) + or request.headers.get("X-Refresh-Token") + or get_bearer_token(request) + ) def check_auth(request: web.Request) -> bool: sessions = get_session_store(request) - token = request.cookies.get(SESSION_COOKIE) or request.headers.get("X-Auth-Token") - return sessions.is_valid(token) + for token in get_auth_tokens(request): + if sessions.is_valid(token, allowed_kinds={"session", "access"}): + return True + return False + + +def get_valid_auth_token(request: web.Request) -> str | None: + sessions = get_session_store(request) + for token in get_auth_tokens(request): + if sessions.is_valid(token, allowed_kinds={"session", "access"}): + return token + return None + + +def auth_capabilities() -> dict[str, Any]: + return { + "cookie_supported": True, + "bearer_supported": True, + "access_token_ttl_seconds": ACCESS_TOKEN_TTL_SECONDS, + "refresh_token_ttl_seconds": REFRESH_TOKEN_TTL_SECONDS, + } def _get_client_ip(request: web.Request) -> str: diff --git a/src/Undefined/webui/routes/_system.py b/src/Undefined/webui/routes/_system.py index 70cebc7f..e8d3ae4c 100644 --- a/src/Undefined/webui/routes/_system.py +++ b/src/Undefined/webui/routes/_system.py @@ -2,11 +2,14 @@ import platform from pathlib import Path +from aiohttp import ClientSession, ClientTimeout from aiohttp import web from aiohttp.web_response import Response from Undefined import __version__ -from ._shared import routes, check_auth +from Undefined.config import get_config +from ._shared import auth_capabilities, routes, check_auth, get_settings +from ..utils import load_bootstrap_probe_data try: import psutil @@ -112,6 +115,94 @@ def _read_memory_info() -> tuple[float, float, float] | None: ) +async def _runtime_health_status() -> tuple[bool, bool, str]: + cfg = get_config(strict=False) + if not bool(cfg.api.enabled): + return False, False, "disabled" + url = f"http://{cfg.api.host}:{cfg.api.port}/health" + try: + timeout = ClientTimeout(total=3.0) + async with ClientSession(timeout=timeout) as session: + async with session.get(url) as resp: + return True, resp.status < 500, f"HTTP {resp.status}" + except Exception as exc: + return True, False, str(exc) + + +def _bootstrap_advice(data: dict[str, object]) -> list[str]: + advice: list[str] = [] + if not bool(data.get("config_exists")): + advice.append("config.toml 缺失,建议先在控制台补齐基础配置。") + if not bool(data.get("toml_valid")): + advice.append("配置文件存在 TOML 语法错误,请先修复语法。") + if ( + bool(data.get("toml_valid")) + and bool(data.get("config_exists")) + and not bool(data.get("config_valid")) + ): + error = str(data.get("validation_error") or "").strip() + advice.append(error or "配置尚未通过严格校验,保存修复后即可启动 Bot。") + if bool(data.get("using_default_password")): + advice.append("默认 WebUI 密码已禁用,请先设置新密码后再继续。") + if not advice: + advice.append("管理入口已就绪,可继续编辑配置、查看日志或启动 Bot。") + return advice + + +@routes.get("/api/v1/management/probes/bootstrap") +async def bootstrap_probe_handler(request: web.Request) -> Response: + if not check_auth(request): + return web.json_response({"error": "Unauthorized"}, status=401) + bootstrap = load_bootstrap_probe_data() + runtime_enabled, runtime_reachable, runtime_detail = await _runtime_health_status() + payload = { + **bootstrap, + "runtime_enabled": runtime_enabled, + "runtime_reachable": runtime_reachable, + "runtime_detail": runtime_detail, + "auth_mode": "token" if request.headers.get("Authorization") else "cookie", + "management_url": f"http://{get_settings(request).url}:{get_settings(request).port}", + } + payload["advice"] = _bootstrap_advice(payload) + return web.json_response(payload) + + +@routes.get("/api/v1/management/probes/capabilities") +async def capabilities_probe_handler(request: web.Request) -> Response: + if not check_auth(request): + return web.json_response({"error": "Unauthorized"}, status=401) + runtime_enabled, runtime_reachable, runtime_detail = await _runtime_health_status() + return web.json_response( + { + "management_api_v1": True, + "runtime_proxy": True, + "bootstrap_probe": True, + "desktop_app": True, + "android_app": True, + "auth": auth_capabilities(), + "config": { + "read": True, + "write": True, + "validate": True, + "sync_template": True, + }, + "logs": {"read": True, "stream": True}, + "bot": { + "status": True, + "start": True, + "stop": True, + "update_restart": True, + }, + "runtime": { + "enabled": runtime_enabled, + "reachable": runtime_reachable, + "detail": runtime_detail, + }, + } + ) + + +@routes.get("/api/v1/management/system") @routes.get("/api/system") async def system_info_handler(request: web.Request) -> Response: if not check_auth(request): diff --git a/src/Undefined/webui/static/css/app.css b/src/Undefined/webui/static/css/app.css index ecc89c4b..2d5441d3 100644 --- a/src/Undefined/webui/static/css/app.css +++ b/src/Undefined/webui/static/css/app.css @@ -1,10 +1,15 @@ .sidebar { + position: sticky; + top: 0; + height: 100vh; background-color: var(--bg-sidebar); border-right: 1px solid var(--border-color); padding: 24px; display: flex; flex-direction: column; gap: 32px; + overflow-y: auto; + scrollbar-gutter: stable; } .brand { font-family: var(--font-serif); font-size: 22px; font-weight: 500; display: flex; align-items: center; gap: 12px; } @@ -53,13 +58,36 @@ .mobile-tabs .nav-item { border-radius: 999px; padding: 8px 14px; border: 1px solid var(--border-color); background: var(--bg-card); white-space: nowrap; font-size: 12px; } .mobile-tabs .nav-item.active { color: var(--accent-color); border-color: rgba(217, 119, 87, 0.4); } +.probe-advice-list { + display: grid; + gap: 8px; + margin-top: 12px; +} + +.probe-advice-item { + padding: 10px 12px; + border-radius: var(--radius-md); + background: var(--bg-app); + border: 1px solid var(--border-color); + color: var(--text-secondary); + font-size: 13px; + line-height: 1.5; +} + .sidebar-footer { margin-top: auto; display: flex; flex-direction: column; gap: 16px; padding-top: 20px; border-top: 1px solid var(--border-color); } .sidebar-footer-actions { display: flex; gap: 8px; } .tab-content { display: none; } .tab-content.active { display: block; } -.main-content { padding: 40px 60px; overflow-y: auto; max-width: 1400px; scrollbar-gutter: stable; } +.main-content { + min-width: 0; + min-height: 100vh; + padding: 20px 60px 40px; + overflow: visible; + max-width: 1400px; + width: 100%; +} .main-content.chat-layout { max-width: none; } .main-content.chat-layout #tab-chat .runtime-card { max-width: 100%; } .main-content.chat-layout #tab-chat .chat-runtime-card { @@ -98,8 +126,8 @@ .header.sticky { position: sticky; top: 0; z-index: 100; background: var(--bg-app); - padding: 20px 0; - margin: -40px -60px 32px -60px; + padding: 14px 0; + margin: -20px -60px 24px -60px; padding-left: 60px; padding-right: 60px; border-bottom: 1px solid var(--border-color); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03); diff --git a/src/Undefined/webui/static/css/base.css b/src/Undefined/webui/static/css/base.css index f32df4df..59822a3d 100644 --- a/src/Undefined/webui/static/css/base.css +++ b/src/Undefined/webui/static/css/base.css @@ -8,6 +8,7 @@ body { line-height: 1.5; transition: background-color 0.3s ease, color 0.3s ease; min-height: 100vh; + overflow-y: auto; } .noise { @@ -27,4 +28,6 @@ body { display: grid; grid-template-columns: var(--sidebar-width) 1fr; min-height: 100vh; + overflow: visible; + align-items: stretch; } diff --git a/src/Undefined/webui/static/css/responsive.css b/src/Undefined/webui/static/css/responsive.css index c8e13ec9..d4fcc9a0 100644 --- a/src/Undefined/webui/static/css/responsive.css +++ b/src/Undefined/webui/static/css/responsive.css @@ -3,19 +3,39 @@ } @media (max-width: 960px) { - .main-content { padding: 30px; } + .main-content { padding: 16px 30px 30px; } .header { flex-direction: column; align-items: flex-start; gap: 16px; } - .header.sticky { margin: -30px -30px 24px -30px; padding-left: 30px; padding-right: 30px; } + .header.sticky { margin: -16px -30px 20px -30px; padding-left: 30px; padding-right: 30px; } .main-content.chat-layout #tab-chat .chat-runtime-card { height: clamp(460px, calc(100vh - 220px), 760px); } } @media (max-width: 768px) { - .app-container { grid-template-columns: 1fr; } + .app-container { + grid-template-columns: 1fr; + height: auto; + min-height: 100vh; + overflow: visible; + } .sidebar { display: none; } .mobile-nav { display: flex; } - .landing-header, .landing-hero, .main-content { padding: 20px; } + .landing-header, .landing-hero, .main-content { padding: 20px 16px; } + .main-content { + min-height: 100vh; + } .header.sticky { margin: -20px -20px 20px -20px; padding-left: 20px; padding-right: 20px; } .card { padding: 22px; } + .overview-grid, .runtime-grid { grid-template-columns: 1fr; } + .toolbar { width: 100%; } + .toolbar > * { flex: 1 1 auto; } + .cta-row { display: grid; grid-template-columns: 1fr 1fr; } + .hero-highlights { gap: 8px; } + .hero-highlight-pill { width: 100%; justify-content: center; } + .mobile-nav { + padding-top: calc(12px + env(safe-area-inset-top)); + padding-bottom: 14px; + } + .mobile-tabs { width: 100%; } + .mobile-actions { width: 100%; justify-content: space-between; } .form-grid { column-count: 1; column-width: auto; } .form-fields { grid-template-columns: 1fr; } .log-viewer { height: 380px; } @@ -44,6 +64,16 @@ } } +@media (max-width: 480px) { + .header { gap: 12px; } + .card { padding: 18px; } + .cta-row { grid-template-columns: 1fr; } + .mobile-tabs .nav-item { font-size: 11px; padding: 8px 12px; } + .log-viewer { height: 52vh; } + .probe-item { grid-template-columns: 1fr; gap: 6px; } + .probe-item-value { justify-self: flex-start; } +} + @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; diff --git a/src/Undefined/webui/static/js/api.js b/src/Undefined/webui/static/js/api.js index f9ab2829..8ab88a3e 100644 --- a/src/Undefined/webui/static/js/api.js +++ b/src/Undefined/webui/static/js/api.js @@ -1,13 +1,182 @@ -async function api(path, options = {}) { - const headers = options.headers || {}; +const AUTH_ENDPOINTS = { + login: [ + "/api/v1/management/auth/login", + "/api/v1/management/login", + "/api/login", + ], + refresh: [ + "/api/v1/management/auth/refresh", + "/api/v1/management/refresh", + "/api/refresh", + ], + session: [ + "/api/v1/management/auth/session", + "/api/v1/management/session", + "/api/session", + ], + logout: [ + "/api/v1/management/auth/logout", + "/api/v1/management/logout", + "/api/logout", + ], +}; + +function authEndpointCandidates(name) { + return Array.isArray(AUTH_ENDPOINTS[name]) ? AUTH_ENDPOINTS[name] : []; +} + +function shouldRetryCandidate(res) { + return !!res && [404, 405, 501].includes(res.status); +} + +async function requestOnce(path, options = {}) { + const headers = { ...(options.headers || {}) }; if (options.method === "POST" && options.body && !headers["Content-Type"]) { headers["Content-Type"] = "application/json"; } - const res = await fetch(path, { + if (state.authAccessToken && !headers.Authorization) { + headers.Authorization = `Bearer ${state.authAccessToken}`; + } + return fetch(path, { ...options, headers, credentials: options.credentials || "same-origin", }); +} + +async function apiWithFallback(paths, options = {}) { + const candidates = Array.isArray(paths) ? paths : [paths]; + let lastResponse = null; + for (const path of candidates) { + const response = await requestOnce(path, options); + lastResponse = response; + if ( + !shouldRetryCandidate(response) || + path === candidates[candidates.length - 1] + ) { + return response; + } + } + return lastResponse; +} + +function normalizeAuthPayload(payload) { + const source = + payload && payload.tokens && typeof payload.tokens === "object" + ? payload.tokens + : payload || {}; + const accessToken = String( + source.access_token || source.accessToken || "", + ).trim(); + const refreshToken = String( + source.refresh_token || source.refreshToken || "", + ).trim(); + const expiresIn = + Number.parseInt( + String(source.expires_in || source.expiresIn || "0"), + 10, + ) || 0; + const expiresAtRaw = + Number.parseInt( + String( + source.access_token_expires_at || + source.accessTokenExpiresAt || + "0", + ), + 10, + ) || 0; + const accessTokenExpiresAt = + expiresAtRaw > 0 + ? expiresAtRaw + : expiresIn > 0 + ? Date.now() + expiresIn * 1000 + : 0; + return { accessToken, refreshToken, accessTokenExpiresAt }; +} + +function scheduleAuthRefresh() { + if (state.authRefreshTimer) { + clearTimeout(state.authRefreshTimer); + state.authRefreshTimer = null; + } + if (!state.authRefreshToken || !state.authAccessTokenExpiresAt) { + return; + } + const delay = Math.max( + 10_000, + state.authAccessTokenExpiresAt - Date.now() - 60_000, + ); + state.authRefreshTimer = window.setTimeout(() => { + refreshAccessToken().catch(() => { + // keep silent; next user action will force re-auth if needed + }); + }, delay); +} + +function updateAuthFromPayload(payload) { + const next = normalizeAuthPayload(payload); + if (!next.accessToken && !next.refreshToken) { + return false; + } + storeAuthTokens(next); + scheduleAuthRefresh(); + return true; +} + +async function refreshAccessToken() { + if (!state.authRefreshToken) { + throw new Error("Unauthorized"); + } + const response = await apiWithFallback(authEndpointCandidates("refresh"), { + method: "POST", + body: JSON.stringify({ refresh_token: state.authRefreshToken }), + headers: state.authAccessToken + ? { Authorization: `Bearer ${state.authAccessToken}` } + : {}, + }); + const payload = await response + .clone() + .json() + .catch(() => ({})); + if (!response.ok || !updateAuthFromPayload(payload)) { + clearStoredAuthTokens(); + throw new Error("Unauthorized"); + } + return payload; +} + +async function api(path, options = {}) { + const candidates = Array.isArray(path) ? path : [path]; + const method = String(options.method || "GET").toUpperCase(); + const accessExpiringSoon = + !!state.authRefreshToken && + !!state.authAccessTokenExpiresAt && + state.authAccessTokenExpiresAt <= Date.now() + 60_000; + + if (accessExpiringSoon && method !== "OPTIONS") { + try { + await refreshAccessToken(); + } catch (_error) { + // ignore and let request surface authorization result + } + } + + let res = await apiWithFallback(candidates, options); + if ( + res.status === 401 && + state.authRefreshToken && + !options._skipRefreshRetry + ) { + try { + await refreshAccessToken(); + res = await apiWithFallback(candidates, { + ...options, + _skipRefreshRetry: true, + }); + } catch (_error) { + clearStoredAuthTokens(); + } + } if (res.status === 401) { state.authenticated = false; refreshUI(); @@ -47,7 +216,10 @@ function stopStatusTimer() { function startSystemTimer() { if (!state.systemTimer) { - state.systemTimer = setInterval(fetchSystemInfo, REFRESH_INTERVALS.system); + state.systemTimer = setInterval( + fetchSystemInfo, + REFRESH_INTERVALS.system, + ); } } diff --git a/src/Undefined/webui/static/js/auth.js b/src/Undefined/webui/static/js/auth.js index a47a3ff9..e7d84f1c 100644 --- a/src/Undefined/webui/static/js/auth.js +++ b/src/Undefined/webui/static/js/auth.js @@ -4,12 +4,13 @@ async function login(pwd, statusId, buttonId) { s.innerText = t("auth.signing_in"); setButtonLoading(button, true); try { - const res = await api("/api/login", { + const res = await api(authEndpointCandidates("login"), { method: "POST", - body: JSON.stringify({ password: pwd }) + body: JSON.stringify({ password: pwd }), }); const data = await res.json(); if (data.success) { + updateAuthFromPayload(data); state.authenticated = true; await checkSession(); refreshUI(); @@ -45,10 +46,16 @@ async function changePassword(currentId, newId, statusId, buttonId) { if (statusEl) statusEl.innerText = t("common.loading"); setButtonLoading(button, true); try { - const res = await api("/api/password", { - method: "POST", - body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }) - }); + const res = await api( + ["/api/v1/management/auth/password", "/api/password"], + { + method: "POST", + body: JSON.stringify({ + current_password: currentPassword, + new_password: newPassword, + }), + }, + ); const data = await res.json(); if (data.success) { if (statusEl) statusEl.innerText = t("auth.password_updated_login"); @@ -58,9 +65,10 @@ async function changePassword(currentId, newId, statusId, buttonId) { state.usingDefaultPassword = false; await login(newPassword, statusId, buttonId); } else { - const msg = data.code === "local_required" - ? t("auth.password_change_local") - : (data.error || t("auth.password_update_failed")); + const msg = + data.code === "local_required" + ? t("auth.password_change_local") + : data.error || t("auth.password_update_failed"); if (statusEl) statusEl.innerText = msg; showToast(msg, "error", 5000); } @@ -75,12 +83,17 @@ async function changePassword(currentId, newId, statusId, buttonId) { async function checkSession() { try { - const res = await api("/api/session"); + const res = await api(authEndpointCandidates("session")); const data = await res.json(); + updateAuthFromPayload(data); state.authenticated = data.authenticated; state.usingDefaultPassword = !!data.using_default_password; + state.capabilities = data.capabilities || state.capabilities; const warning = get("warningBox"); - if (warning) warning.style.display = data.using_default_password ? "block" : "none"; + if (warning) + warning.style.display = data.using_default_password + ? "block" + : "none"; const navFooter = get("navFooter"); if (navFooter) navFooter.innerText = data.summary || ""; updateAuthPanels(); diff --git a/src/Undefined/webui/static/js/bot.js b/src/Undefined/webui/static/js/bot.js index 23f487fb..4d079724 100644 --- a/src/Undefined/webui/static/js/bot.js +++ b/src/Undefined/webui/static/js/bot.js @@ -19,8 +19,12 @@ function updateBotUI() { if (state.bot.running) { badge.innerText = t("bot.status.running"); badge.className = "badge success"; - const pidText = state.bot.pid != null ? `PID: ${state.bot.pid}` : "PID: --"; - const uptimeText = state.bot.uptime_seconds != null ? `Uptime: ${Math.round(state.bot.uptime_seconds)}s` : ""; + const pidText = + state.bot.pid != null ? `PID: ${state.bot.pid}` : "PID: --"; + const uptimeText = + state.bot.uptime_seconds != null + ? `Uptime: ${Math.round(state.bot.uptime_seconds)}s` + : ""; const parts = [pidText, uptimeText].filter(Boolean); metaL.innerText = parts.length ? parts.join(" | ") : "--"; hintL.innerText = t("bot.hint.running"); @@ -40,7 +44,7 @@ async function botAction(action) { try { await api(`/api/bot/${action}`, { method: "POST" }); await fetchStatus(); - } catch (e) { } + } catch (e) {} } function startWebuiRestartPoll() { @@ -48,9 +52,14 @@ function startWebuiRestartPoll() { const timer = setInterval(async () => { attempts += 1; try { - const res = await fetch("/api/session", { credentials: "same-origin" }); - if (res.ok) { clearInterval(timer); location.reload(); } - } catch (e) { } + const res = await fetch("/api/session", { + credentials: "same-origin", + }); + if (res.ok) { + clearInterval(timer); + location.reload(); + } + } catch (e) {} if (attempts > 60) clearInterval(timer); }, 1000); } @@ -67,7 +76,11 @@ async function updateAndRestartWebui(button) { const data = await res.json(); if (!data.success) throw new Error(data.error || t("update.failed")); if (!data.eligible) { - showToast(`${t("update.not_eligible")}: ${data.reason || ""}`.trim(), "warning", 7000); + showToast( + `${t("update.not_eligible")}: ${data.reason || ""}`.trim(), + "warning", + 7000, + ); return; } if (data.will_restart === false) { @@ -75,11 +88,20 @@ async function updateAndRestartWebui(button) { showToast(t("update.no_restart"), "warning", 8000); return; } - showToast(data.updated ? t("update.updated_restarting") : t("update.uptodate_restarting"), - data.updated ? "success" : "info", 6000); + showToast( + data.updated + ? t("update.updated_restarting") + : t("update.uptodate_restarting"), + data.updated ? "success" : "info", + 6000, + ); startWebuiRestartPoll(); } catch (e) { - showToast(`${t("update.failed")}: ${e.message || e}`.trim(), "error", 8000); + showToast( + `${t("update.failed")}: ${e.message || e}`.trim(), + "error", + 8000, + ); } finally { setButtonLoading(button, false); } @@ -94,19 +116,25 @@ async function fetchSystemInfo() { const memUsage = data.memory_usage_percent ?? 0; get("systemCpuModel").innerText = data.cpu_model || "--"; - get("systemCpuUsage").innerText = data.cpu_usage_percent != null ? `${cpuUsage}%` : "--"; + get("systemCpuUsage").innerText = + data.cpu_usage_percent != null ? `${cpuUsage}%` : "--"; get("systemMemory").innerText = data.memory_total_gb != null && data.memory_used_gb != null - ? `${data.memory_used_gb} GB / ${data.memory_total_gb} GB` : "--"; - get("systemMemoryUsage").innerText = data.memory_usage_percent != null ? `${memUsage}%` : "--"; + ? `${data.memory_used_gb} GB / ${data.memory_total_gb} GB` + : "--"; + get("systemMemoryUsage").innerText = + data.memory_usage_percent != null ? `${memUsage}%` : "--"; get("systemVersion").innerText = data.system_version || "--"; get("systemArch").innerText = data.system_arch || "--"; get("systemKernel").innerText = data.system_release || "--"; get("systemPythonVersion").innerText = data.python_version || "--"; - get("systemUndefinedVersion").innerText = data.undefined_version || "--"; + get("systemUndefinedVersion").innerText = + data.undefined_version || "--"; - get("systemCpuBar").style.width = `${Math.min(100, Math.max(0, cpuUsage))}%`; - get("systemMemoryBar").style.width = `${Math.min(100, Math.max(0, memUsage))}%`; + get("systemCpuBar").style.width = + `${Math.min(100, Math.max(0, cpuUsage))}%`; + get("systemMemoryBar").style.width = + `${Math.min(100, Math.max(0, memUsage))}%`; recordFetchSuccess("system"); } catch (e) { recordFetchError("system"); diff --git a/src/Undefined/webui/static/js/config-form.js b/src/Undefined/webui/static/js/config-form.js index 2a89538b..328b6cf6 100644 --- a/src/Undefined/webui/static/js/config-form.js +++ b/src/Undefined/webui/static/js/config-form.js @@ -27,53 +27,61 @@ function getComment(path) { } function updateCommentTexts() { - document.querySelectorAll("[data-comment-path]").forEach(el => { + document.querySelectorAll("[data-comment-path]").forEach((el) => { const path = el.getAttribute("data-comment-path"); if (path) el.innerText = getComment(path); }); } function updateConfigSearchIndex() { - document.querySelectorAll(".form-group").forEach(group => { + document.querySelectorAll(".form-group").forEach((group) => { const label = group.querySelector(".form-label"); const hint = group.querySelector(".form-hint"); const path = group.dataset.path || ""; - group.dataset.searchText = `${path} ${label ? label.innerText : ""} ${hint ? hint.innerText : ""}`.toLowerCase(); + group.dataset.searchText = + `${path} ${label ? label.innerText : ""} ${hint ? hint.innerText : ""}`.toLowerCase(); }); } function isPlainObject(value) { - return value !== null && typeof value === "object" && !Array.isArray(value) + return value !== null && typeof value === "object" && !Array.isArray(value); } function isAotCollection(path, value) { - return Array.isArray(value) && (AOT_PATHS.has(path) || value.some(item => item !== null && typeof item === "object")) + return ( + Array.isArray(value) && + (AOT_PATHS.has(path) || + value.some((item) => item !== null && typeof item === "object")) + ); } function isRequestParamsPath(path) { - return path === "request_params" || path.endsWith(".request_params") + return path === "request_params" || path.endsWith(".request_params"); } function scheduleAutoSave() { - if (state.saveTimer) clearTimeout(state.saveTimer) - showSaveStatus("saving", t("config.typing")) + if (state.saveTimer) clearTimeout(state.saveTimer); + showSaveStatus("saving", t("config.typing")); state.saveTimer = setTimeout(() => { - state.saveTimer = null - autoSave() - }, 1000) + state.saveTimer = null; + autoSave(); + }, 1000); } function createEditorNode(path, value) { if (isRequestParamsPath(path)) { - return createRequestParamsWidget(path, isPlainObject(value) ? value : {}) + return createRequestParamsWidget( + path, + isPlainObject(value) ? value : {}, + ); } if (isPlainObject(value)) { - return createSubSubSection(path, value) + return createSubSubSection(path, value); } if (isAotCollection(path, value)) { - return createAotWidget(path, value) + return createAotWidget(path, value); } - return createField(path, value) + return createField(path, value); } function buildConfigForm() { @@ -106,7 +114,9 @@ function buildConfigForm() { toggle.className = "btn ghost btn-sm"; toggle.dataset.section = section; toggle.setAttribute("aria-expanded", collapsed ? "false" : "true"); - toggle.innerText = collapsed ? t("config.expand_section") : t("config.collapse_section"); + toggle.innerText = collapsed + ? t("config.expand_section") + : t("config.collapse_section"); toggle.addEventListener("click", () => toggleSection(section)); actions.appendChild(toggle); @@ -138,27 +148,31 @@ function buildConfigForm() { function toggleSection(section) { state.configCollapsed[section] = !state.configCollapsed[section]; - document.querySelectorAll(".config-card").forEach(card => { + document.querySelectorAll(".config-card").forEach((card) => { if (card.dataset.section !== section) return; const collapsed = !!state.configCollapsed[section]; card.classList.toggle("is-collapsed", collapsed); const toggle = card.querySelector(".config-card-actions button"); if (toggle) { - toggle.innerText = collapsed ? t("config.expand_section") : t("config.collapse_section"); + toggle.innerText = collapsed + ? t("config.expand_section") + : t("config.collapse_section"); toggle.setAttribute("aria-expanded", collapsed ? "false" : "true"); } }); } function setAllSectionsCollapsed(collapsed) { - document.querySelectorAll(".config-card").forEach(card => { + document.querySelectorAll(".config-card").forEach((card) => { const section = card.dataset.section; if (!section) return; state.configCollapsed[section] = collapsed; card.classList.toggle("is-collapsed", collapsed); const toggle = card.querySelector(".config-card-actions button"); if (toggle) { - toggle.innerText = collapsed ? t("config.expand_section") : t("config.collapse_section"); + toggle.innerText = collapsed + ? t("config.expand_section") + : t("config.collapse_section"); toggle.setAttribute("aria-expanded", collapsed ? "false" : "true"); } }); @@ -168,19 +182,27 @@ function applyConfigFilter() { if (!state.configLoaded) return; const query = state.configSearch.trim().toLowerCase(); let matchCount = 0; - document.querySelectorAll(".config-card").forEach(card => { + document.querySelectorAll(".config-card").forEach((card) => { let cardMatches = 0; - card.querySelectorAll(".form-group").forEach(group => { - const isMatch = !query || (group.dataset.searchText || "").includes(query); + card.querySelectorAll(".form-group").forEach((group) => { + const isMatch = + !query || (group.dataset.searchText || "").includes(query); group.classList.toggle("is-hidden", !isMatch); group.classList.toggle("is-match", isMatch && query.length > 0); if (isMatch) cardMatches += 1; }); - card.querySelectorAll(".form-subsection").forEach(section => { - section.style.display = section.querySelector(".form-group:not(.is-hidden)") ? "" : "none"; + card.querySelectorAll(".form-subsection").forEach((section) => { + section.style.display = section.querySelector( + ".form-group:not(.is-hidden)", + ) + ? "" + : "none"; }); card.classList.toggle("force-open", query.length > 0); - card.classList.toggle("is-hidden", query.length > 0 && cardMatches === 0); + card.classList.toggle( + "is-hidden", + query.length > 0 && cardMatches === 0, + ); matchCount += cardMatches; }); if (query.length > 0 && matchCount === 0) { @@ -202,7 +224,11 @@ function showSaveStatus(status, text) { el.classList.remove("active"); txt.innerText = text || t("config.saved"); setTimeout(() => { - if (!state.saveTimer) { el.style.opacity = "0"; state.saveStatus = "idle"; updateSaveStatusText(); } + if (!state.saveTimer) { + el.style.opacity = "0"; + state.saveStatus = "idle"; + updateSaveStatusText(); + } }, 2000); } else if (status === "error") { el.classList.remove("active"); @@ -212,21 +238,25 @@ function showSaveStatus(status, text) { } function isSensitiveKey(path) { - return /(password|token|secret|api_key|apikey|access_key|private_key)/i.test(path); + return /(password|token|secret|api_key|apikey|access_key|private_key)/i.test( + path, + ); } function isLongText(value) { - return typeof value === "string" && (value.length > 80 || value.includes("\n")); + return ( + typeof value === "string" && (value.length > 80 || value.includes("\n")) + ); } const FIELD_SELECT_OPTIONS = { api_mode: ["chat_completions", "responses"], reasoning_effort: ["none", "minimal", "low", "medium", "high", "xhigh"], -} +}; function getFieldSelectOptions(path) { - const key = path.split(".").pop() - return FIELD_SELECT_OPTIONS[key] || null + const key = path.split(".").pop(); + return FIELD_SELECT_OPTIONS[key] || null; } function createField(path, val) { @@ -280,7 +310,7 @@ function createField(path, val) { input = document.createElement("select"); input.className = "form-control config-input"; input.dataset.valueType = "string"; - selectOptions.forEach(optionValue => { + selectOptions.forEach((optionValue) => { const option = document.createElement("option"); option.value = optionValue; option.innerText = optionValue; @@ -304,12 +334,17 @@ function createField(path, val) { input.type = "text"; input.value = val.join(", "); input.dataset.valueType = "array"; - input.dataset.arrayType = val.every(item => typeof item === "number") ? "number" : "string"; + input.dataset.arrayType = val.every( + (item) => typeof item === "number", + ) + ? "number" + : "string"; } else { input.type = isSecret ? "password" : "text"; input.value = val == null ? "" : String(val); input.dataset.valueType = "string"; - if (isSecret) input.setAttribute("autocomplete", "new-password"); + if (isSecret) + input.setAttribute("autocomplete", "new-password"); } } @@ -324,7 +359,10 @@ function createField(path, val) { return group; } -const AOT_PATHS = new Set(["models.chat.pool.models", "models.agent.pool.models"]); +const AOT_PATHS = new Set([ + "models.chat.pool.models", + "models.agent.pool.models", +]); function createSubSubSection(path, obj) { const div = document.createElement("div"); @@ -353,546 +391,665 @@ function createSubSubSection(path, obj) { function buildEmptyStructuredValue(value) { if (Array.isArray(value)) { - return value.length > 0 ? [buildEmptyStructuredValue(value[0])] : [] + return value.length > 0 ? [buildEmptyStructuredValue(value[0])] : []; } if (isPlainObject(value)) { - return Object.fromEntries(Object.keys(value).map(key => [key, buildEmptyStructuredValue(value[key])])) + return Object.fromEntries( + Object.keys(value).map((key) => [ + key, + buildEmptyStructuredValue(value[key]), + ]), + ); } - if (typeof value === "boolean") return false - if (value === null) return null - return "" + if (typeof value === "boolean") return false; + if (value === null) return null; + return ""; } function inferStructuredType(value) { - if (Array.isArray(value)) return "array" - if (isPlainObject(value)) return "object" - return "scalar" + if (Array.isArray(value)) return "array"; + if (isPlainObject(value)) return "object"; + return "scalar"; } function inferScalarType(value) { - if (value === null) return "null" - if (typeof value === "number") return "number" - if (typeof value === "boolean") return "boolean" - return "string" + if (value === null) return "null"; + if (typeof value === "number") return "number"; + if (typeof value === "boolean") return "boolean"; + return "string"; } function createRequestParamsWidget(path, value) { - const group = document.createElement("div") - group.className = "form-group" - group.dataset.path = path + const group = document.createElement("div"); + group.className = "form-group"; + group.dataset.path = path; - const label = document.createElement("label") - label.className = "form-label" - label.innerText = path.split(".").pop() - group.appendChild(label) + const label = document.createElement("label"); + label.className = "form-label"; + label.innerText = path.split(".").pop(); + group.appendChild(label); - const comment = getComment(path) + const comment = getComment(path); if (comment) { - const hint = document.createElement("div") - hint.className = "form-hint" - hint.innerText = comment - hint.dataset.commentPath = path - group.appendChild(hint) + const hint = document.createElement("div"); + hint.className = "form-hint"; + hint.innerText = comment; + hint.dataset.commentPath = path; + group.appendChild(hint); } - group.dataset.searchText = `${path} ${comment || ""}`.toLowerCase() - const editor = createStructuredValueEditor(value, { rootType: "object" }) - editor.dataset.requestParamsRoot = "true" - group.appendChild(editor) - return group + group.dataset.searchText = `${path} ${comment || ""}`.toLowerCase(); + const editor = createStructuredValueEditor(value, { rootType: "object" }); + editor.dataset.requestParamsRoot = "true"; + group.appendChild(editor); + return group; } function createStructuredActionButton(text, onClick) { - const button = document.createElement("button") - button.type = "button" - button.className = "btn ghost btn-sm" - button.innerText = text - button.onclick = onClick - return button + const button = document.createElement("button"); + button.type = "button"; + button.className = "btn ghost btn-sm"; + button.innerText = text; + button.onclick = onClick; + return button; } function createStructuredValueEditor(value, options = {}) { - const rootType = options.rootType || inferStructuredType(value) + const rootType = options.rootType || inferStructuredType(value); if (rootType === "object") { - return createStructuredObjectEditor(isPlainObject(value) ? value : {}) + return createStructuredObjectEditor(isPlainObject(value) ? value : {}); } if (rootType === "array") { - return createStructuredArrayEditor(Array.isArray(value) ? value : []) + return createStructuredArrayEditor(Array.isArray(value) ? value : []); } - return createStructuredScalarEditor(value) + return createStructuredScalarEditor(value); } function createStructuredObjectEditor(value) { - const wrapper = document.createElement("div") - wrapper.dataset.structuredType = "object" + const wrapper = document.createElement("div"); + wrapper.dataset.structuredType = "object"; - const body = document.createElement("div") - body.className = "request-params-object-body" - wrapper.appendChild(body) + const body = document.createElement("div"); + body.className = "request-params-object-body"; + wrapper.appendChild(body); Object.entries(value).forEach(([key, itemValue]) => { - body.appendChild(createStructuredObjectEntry(key, itemValue)) - }) - - const actions = document.createElement("div") - actions.style.marginTop = "8px" - actions.style.display = "flex" - actions.style.gap = "8px" - actions.style.flexWrap = "wrap" - actions.appendChild(createStructuredActionButton(`${t("config.aot_add")} Field`, () => { - body.appendChild(createStructuredObjectEntry("", "")) - autoSave() - })) - actions.appendChild(createStructuredActionButton(`${t("config.aot_add")} Object`, () => { - body.appendChild(createStructuredObjectEntry("", {})) - autoSave() - })) - actions.appendChild(createStructuredActionButton(`${t("config.aot_add")} Array`, () => { - body.appendChild(createStructuredObjectEntry("", [])) - autoSave() - })) - wrapper.appendChild(actions) - - return wrapper + body.appendChild(createStructuredObjectEntry(key, itemValue)); + }); + + const actions = document.createElement("div"); + actions.style.marginTop = "8px"; + actions.style.display = "flex"; + actions.style.gap = "8px"; + actions.style.flexWrap = "wrap"; + actions.appendChild( + createStructuredActionButton(`${t("config.aot_add")} Field`, () => { + body.appendChild(createStructuredObjectEntry("", "")); + autoSave(); + }), + ); + actions.appendChild( + createStructuredActionButton(`${t("config.aot_add")} Object`, () => { + body.appendChild(createStructuredObjectEntry("", {})); + autoSave(); + }), + ); + actions.appendChild( + createStructuredActionButton(`${t("config.aot_add")} Array`, () => { + body.appendChild(createStructuredObjectEntry("", [])); + autoSave(); + }), + ); + wrapper.appendChild(actions); + + return wrapper; } function createStructuredObjectEntry(key, value) { - const entry = document.createElement("div") - entry.className = "request-params-object-entry" - entry.style.cssText = "border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:8px;" - - const keyGroup = document.createElement("div") - keyGroup.className = "form-group" - const keyLabel = document.createElement("label") - keyLabel.className = "form-label" - keyLabel.innerText = "key" - keyGroup.appendChild(keyLabel) - const keyInput = document.createElement("input") - keyInput.type = "text" - keyInput.className = "form-control request-params-key-input" - keyInput.value = key || "" - keyInput.oninput = () => scheduleAutoSave() - keyGroup.appendChild(keyInput) - entry.appendChild(keyGroup) - - const valueContainer = document.createElement("div") - valueContainer.className = "request-params-entry-value" - valueContainer.appendChild(createStructuredValueEditor(value)) - entry.appendChild(valueContainer) - - const removeBtn = createStructuredActionButton(t("config.aot_remove"), () => { - entry.remove() - autoSave() - }) - entry.appendChild(removeBtn) - - return entry + const entry = document.createElement("div"); + entry.className = "request-params-object-entry"; + entry.style.cssText = + "border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:8px;"; + + const keyGroup = document.createElement("div"); + keyGroup.className = "form-group"; + const keyLabel = document.createElement("label"); + keyLabel.className = "form-label"; + keyLabel.innerText = "key"; + keyGroup.appendChild(keyLabel); + const keyInput = document.createElement("input"); + keyInput.type = "text"; + keyInput.className = "form-control request-params-key-input"; + keyInput.value = key || ""; + keyInput.oninput = () => scheduleAutoSave(); + keyGroup.appendChild(keyInput); + entry.appendChild(keyGroup); + + const valueContainer = document.createElement("div"); + valueContainer.className = "request-params-entry-value"; + valueContainer.appendChild(createStructuredValueEditor(value)); + entry.appendChild(valueContainer); + + const removeBtn = createStructuredActionButton( + t("config.aot_remove"), + () => { + entry.remove(); + autoSave(); + }, + ); + entry.appendChild(removeBtn); + + return entry; } function createStructuredArrayEditor(value) { - const wrapper = document.createElement("div") - wrapper.dataset.structuredType = "array" - - const body = document.createElement("div") - body.className = "request-params-array-body" - wrapper.appendChild(body) - - value.forEach(itemValue => { - body.appendChild(createStructuredArrayEntry(itemValue)) - }) - - const actions = document.createElement("div") - actions.style.marginTop = "8px" - actions.style.display = "flex" - actions.style.gap = "8px" - actions.style.flexWrap = "wrap" - actions.appendChild(createStructuredActionButton(`${t("config.aot_add")} Value`, () => { - body.appendChild(createStructuredArrayEntry("")) - autoSave() - })) - actions.appendChild(createStructuredActionButton(`${t("config.aot_add")} Object`, () => { - body.appendChild(createStructuredArrayEntry({})) - autoSave() - })) - actions.appendChild(createStructuredActionButton(`${t("config.aot_add")} Array`, () => { - body.appendChild(createStructuredArrayEntry([])) - autoSave() - })) - wrapper.appendChild(actions) - - return wrapper -} + const wrapper = document.createElement("div"); + wrapper.dataset.structuredType = "array"; -function createStructuredArrayEntry(value) { - const entry = document.createElement("div") - entry.className = "request-params-array-entry" - entry.style.cssText = "border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:8px;" + const body = document.createElement("div"); + body.className = "request-params-array-body"; + wrapper.appendChild(body); - const valueContainer = document.createElement("div") - valueContainer.className = "request-params-entry-value" - valueContainer.appendChild(createStructuredValueEditor(value)) - entry.appendChild(valueContainer) + value.forEach((itemValue) => { + body.appendChild(createStructuredArrayEntry(itemValue)); + }); - const removeBtn = createStructuredActionButton(t("config.aot_remove"), () => { - entry.remove() - autoSave() - }) - entry.appendChild(removeBtn) + const actions = document.createElement("div"); + actions.style.marginTop = "8px"; + actions.style.display = "flex"; + actions.style.gap = "8px"; + actions.style.flexWrap = "wrap"; + actions.appendChild( + createStructuredActionButton(`${t("config.aot_add")} Value`, () => { + body.appendChild(createStructuredArrayEntry("")); + autoSave(); + }), + ); + actions.appendChild( + createStructuredActionButton(`${t("config.aot_add")} Object`, () => { + body.appendChild(createStructuredArrayEntry({})); + autoSave(); + }), + ); + actions.appendChild( + createStructuredActionButton(`${t("config.aot_add")} Array`, () => { + body.appendChild(createStructuredArrayEntry([])); + autoSave(); + }), + ); + wrapper.appendChild(actions); + + return wrapper; +} - return entry +function createStructuredArrayEntry(value) { + const entry = document.createElement("div"); + entry.className = "request-params-array-entry"; + entry.style.cssText = + "border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:8px;"; + + const valueContainer = document.createElement("div"); + valueContainer.className = "request-params-entry-value"; + valueContainer.appendChild(createStructuredValueEditor(value)); + entry.appendChild(valueContainer); + + const removeBtn = createStructuredActionButton( + t("config.aot_remove"), + () => { + entry.remove(); + autoSave(); + }, + ); + entry.appendChild(removeBtn); + + return entry; } function createStructuredScalarEditor(value) { - const wrapper = document.createElement("div") - wrapper.dataset.structuredType = "scalar" - - const typeGroup = document.createElement("div") - typeGroup.className = "form-group" - const typeLabel = document.createElement("label") - typeLabel.className = "form-label" - typeLabel.innerText = "type" - typeGroup.appendChild(typeLabel) - - const typeSelect = document.createElement("select") - typeSelect.className = "form-control request-params-scalar-type" - ;["string", "number", "boolean", "null"].forEach(type => { - const option = document.createElement("option") - option.value = type - option.innerText = type - typeSelect.appendChild(option) - }) - typeSelect.value = inferScalarType(value) - typeGroup.appendChild(typeSelect) - wrapper.appendChild(typeGroup) - - const valueContainer = document.createElement("div") - valueContainer.className = "request-params-scalar-value" - wrapper.appendChild(valueContainer) + const wrapper = document.createElement("div"); + wrapper.dataset.structuredType = "scalar"; + + const typeGroup = document.createElement("div"); + typeGroup.className = "form-group"; + const typeLabel = document.createElement("label"); + typeLabel.className = "form-label"; + typeLabel.innerText = "type"; + typeGroup.appendChild(typeLabel); + + const typeSelect = document.createElement("select"); + typeSelect.className = "form-control request-params-scalar-type"; + ["string", "number", "boolean", "null"].forEach((type) => { + const option = document.createElement("option"); + option.value = type; + option.innerText = type; + typeSelect.appendChild(option); + }); + typeSelect.value = inferScalarType(value); + typeGroup.appendChild(typeSelect); + wrapper.appendChild(typeGroup); + + const valueContainer = document.createElement("div"); + valueContainer.className = "request-params-scalar-value"; + wrapper.appendChild(valueContainer); const renderValueInput = () => { - const scalarType = typeSelect.value - valueContainer.textContent = "" + const scalarType = typeSelect.value; + valueContainer.textContent = ""; if (scalarType === "null") { - const emptyHint = document.createElement("div") - emptyHint.className = "form-hint" - emptyHint.innerText = "null" - valueContainer.appendChild(emptyHint) - return + const emptyHint = document.createElement("div"); + emptyHint.className = "form-hint"; + emptyHint.innerText = "null"; + valueContainer.appendChild(emptyHint); + return; } if (scalarType === "boolean") { - const booleanWrap = document.createElement("label") - booleanWrap.className = "toggle-wrapper" - const booleanInput = document.createElement("input") - booleanInput.type = "checkbox" - booleanInput.className = "toggle-input request-params-scalar-input" - booleanInput.checked = typeof value === "boolean" ? value : false - booleanInput.onchange = () => autoSave() - const track = document.createElement("span") - track.className = "toggle-track" - const handle = document.createElement("span") - handle.className = "toggle-handle" - track.appendChild(handle) - booleanWrap.appendChild(booleanInput) - booleanWrap.appendChild(track) - valueContainer.appendChild(booleanWrap) - return + const booleanWrap = document.createElement("label"); + booleanWrap.className = "toggle-wrapper"; + const booleanInput = document.createElement("input"); + booleanInput.type = "checkbox"; + booleanInput.className = "toggle-input request-params-scalar-input"; + booleanInput.checked = typeof value === "boolean" ? value : false; + booleanInput.onchange = () => autoSave(); + const track = document.createElement("span"); + track.className = "toggle-track"; + const handle = document.createElement("span"); + handle.className = "toggle-handle"; + track.appendChild(handle); + booleanWrap.appendChild(booleanInput); + booleanWrap.appendChild(track); + valueContainer.appendChild(booleanWrap); + return; } - const input = document.createElement("input") - input.className = "form-control request-params-scalar-input" - input.type = scalarType === "number" ? "number" : "text" + const input = document.createElement("input"); + input.className = "form-control request-params-scalar-input"; + input.type = scalarType === "number" ? "number" : "text"; if (scalarType === "number") { - input.step = "any" - input.value = typeof value === "number" ? String(value) : "" + input.step = "any"; + input.value = typeof value === "number" ? String(value) : ""; } else { - input.value = value == null ? "" : String(value) + input.value = value == null ? "" : String(value); } - input.oninput = () => scheduleAutoSave() - valueContainer.appendChild(input) - } + input.oninput = () => scheduleAutoSave(); + valueContainer.appendChild(input); + }; typeSelect.onchange = () => { - value = typeSelect.value === "boolean" ? false : typeSelect.value === "null" ? null : "" - renderValueInput() - autoSave() - } - - renderValueInput() - return wrapper + value = + typeSelect.value === "boolean" + ? false + : typeSelect.value === "null" + ? null + : ""; + renderValueInput(); + autoSave(); + }; + + renderValueInput(); + return wrapper; } function getStructuredValueChild(container) { - return Array.from(container.children).find(child => child.dataset && child.dataset.structuredType) + return Array.from(container.children).find( + (child) => child.dataset && child.dataset.structuredType, + ); } function readStructuredValueEditor(node) { - const type = node.dataset.structuredType + const type = node.dataset.structuredType; if (type === "object") { - const result = {} - const body = node.querySelector(".request-params-object-body") - Array.from(body ? body.children : []).forEach(entry => { - const keyInput = entry.querySelector(".request-params-key-input") - const key = keyInput ? keyInput.value.trim() : "" - if (!key) return - const valueContainer = entry.querySelector(".request-params-entry-value") - const valueNode = valueContainer ? getStructuredValueChild(valueContainer) : null - if (!valueNode) return - result[key] = readStructuredValueEditor(valueNode) - }) - return result + const result = {}; + const body = node.querySelector(".request-params-object-body"); + Array.from(body ? body.children : []).forEach((entry) => { + const keyInput = entry.querySelector(".request-params-key-input"); + const key = keyInput ? keyInput.value.trim() : ""; + if (!key) return; + const valueContainer = entry.querySelector( + ".request-params-entry-value", + ); + const valueNode = valueContainer + ? getStructuredValueChild(valueContainer) + : null; + if (!valueNode) return; + result[key] = readStructuredValueEditor(valueNode); + }); + return result; } if (type === "array") { - const body = node.querySelector(".request-params-array-body") - return Array.from(body ? body.children : []).map(entry => { - const valueContainer = entry.querySelector(".request-params-entry-value") - const valueNode = valueContainer ? getStructuredValueChild(valueContainer) : null - return valueNode ? readStructuredValueEditor(valueNode) : null - }) + const body = node.querySelector(".request-params-array-body"); + return Array.from(body ? body.children : []).map((entry) => { + const valueContainer = entry.querySelector( + ".request-params-entry-value", + ); + const valueNode = valueContainer + ? getStructuredValueChild(valueContainer) + : null; + return valueNode ? readStructuredValueEditor(valueNode) : null; + }); } - const scalarType = node.querySelector(".request-params-scalar-type")?.value || "string" - if (scalarType === "null") return null - const scalarInput = node.querySelector(".request-params-scalar-input") + const scalarType = + node.querySelector(".request-params-scalar-type")?.value || "string"; + if (scalarType === "null") return null; + const scalarInput = node.querySelector(".request-params-scalar-input"); if (scalarType === "boolean") { - return Boolean(scalarInput?.checked) + return Boolean(scalarInput?.checked); } - const raw = scalarInput ? scalarInput.value : "" + const raw = scalarInput ? scalarInput.value : ""; if (scalarType === "number") { - const parsed = Number(raw) - return Number.isNaN(parsed) ? 0 : parsed + const parsed = Number(raw); + return Number.isNaN(parsed) ? 0 : parsed; } - return raw + return raw; } function createAotScalarInput(key, value) { - const isLong = isLongText(value) - const isNumber = typeof value === "number" - const isBoolean = typeof value === "boolean" - const isArray = Array.isArray(value) - const isSecret = isSensitiveKey(key) + const isLong = isLongText(value); + const isNumber = typeof value === "number"; + const isBoolean = typeof value === "boolean"; + const isArray = Array.isArray(value); + const isSecret = isSensitiveKey(key); if (isBoolean) { - const wrapper = document.createElement("label") - wrapper.className = "toggle-wrapper" - const toggle = document.createElement("input") - toggle.type = "checkbox" - toggle.className = "toggle-input aot-field-input" - toggle.dataset.valueType = "boolean" - toggle.checked = Boolean(value) - toggle.onchange = () => autoSave() - const track = document.createElement("span") - track.className = "toggle-track" - const handle = document.createElement("span") - handle.className = "toggle-handle" - track.appendChild(handle) - wrapper.appendChild(toggle) - wrapper.appendChild(track) - return wrapper - } - - let input + const wrapper = document.createElement("label"); + wrapper.className = "toggle-wrapper"; + const toggle = document.createElement("input"); + toggle.type = "checkbox"; + toggle.className = "toggle-input aot-field-input"; + toggle.dataset.valueType = "boolean"; + toggle.checked = Boolean(value); + toggle.onchange = () => autoSave(); + const track = document.createElement("span"); + track.className = "toggle-track"; + const handle = document.createElement("span"); + handle.className = "toggle-handle"; + track.appendChild(handle); + wrapper.appendChild(toggle); + wrapper.appendChild(track); + return wrapper; + } + + let input; if (isLong) { - input = document.createElement("textarea") - input.className = "form-control form-textarea aot-field-input" + input = document.createElement("textarea"); + input.className = "form-control form-textarea aot-field-input"; } else { - input = document.createElement("input") - input.className = "form-control aot-field-input" - input.type = isNumber ? "number" : isSecret ? "password" : "text" - if (isNumber) input.step = "any" - if (isSecret) input.setAttribute("autocomplete", "new-password") + input = document.createElement("input"); + input.className = "form-control aot-field-input"; + input.type = isNumber ? "number" : isSecret ? "password" : "text"; + if (isNumber) input.step = "any"; + if (isSecret) input.setAttribute("autocomplete", "new-password"); } - input.dataset.valueType = isNumber ? "number" : isArray ? "array" : "string" + input.dataset.valueType = isNumber + ? "number" + : isArray + ? "array" + : "string"; if (isArray) { - input.dataset.arrayType = value.every(item => typeof item === "number") ? "number" : "string" - input.value = value.join(", ") + input.dataset.arrayType = value.every( + (item) => typeof item === "number", + ) + ? "number" + : "string"; + input.value = value.join(", "); } else { - input.value = value == null ? "" : String(value) + input.value = value == null ? "" : String(value); } - input.oninput = () => scheduleAutoSave() - return input + input.oninput = () => scheduleAutoSave(); + return input; } function createAotEntry(path, entry) { - const div = document.createElement("div") - div.className = "aot-entry" - div.style.cssText = "border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:8px;" - const fields = document.createElement("div") - fields.className = "form-fields" + const div = document.createElement("div"); + div.className = "aot-entry"; + div.style.cssText = + "border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:8px;"; + const fields = document.createElement("div"); + fields.className = "form-fields"; for (const [key, value] of Object.entries(entry)) { - const field = document.createElement("div") - field.className = "form-group aot-entry-field" - field.dataset.fieldKey = key - field.dataset.path = `${path}[].${key}` + const field = document.createElement("div"); + field.className = "form-group aot-entry-field"; + field.dataset.fieldKey = key; + field.dataset.path = `${path}[].${key}`; - const label = document.createElement("label") - label.className = "form-label" - label.innerText = key - field.appendChild(label) + const label = document.createElement("label"); + label.className = "form-label"; + label.innerText = key; + field.appendChild(label); - const fieldPath = `${path}.${key}` + const fieldPath = `${path}.${key}`; if (isPlainObject(value) || Array.isArray(value)) { - field.dataset.fieldEditor = "structured" - const editor = createStructuredValueEditor(value, { rootType: isRequestParamsPath(fieldPath) ? "object" : inferStructuredType(value) }) - field.appendChild(editor) + field.dataset.fieldEditor = "structured"; + const editor = createStructuredValueEditor(value, { + rootType: isRequestParamsPath(fieldPath) + ? "object" + : inferStructuredType(value), + }); + field.appendChild(editor); } else { - field.dataset.fieldEditor = "scalar" - field.appendChild(createAotScalarInput(key, value)) + field.dataset.fieldEditor = "scalar"; + field.appendChild(createAotScalarInput(key, value)); } - fields.appendChild(field) + fields.appendChild(field); } - div.appendChild(fields) - const removeBtn = document.createElement("button") - removeBtn.type = "button" - removeBtn.className = "btn ghost btn-sm" - removeBtn.innerText = t("config.aot_remove") - removeBtn.onclick = () => { div.remove(); autoSave() } - div.appendChild(removeBtn) - return div + div.appendChild(fields); + const removeBtn = document.createElement("button"); + removeBtn.type = "button"; + removeBtn.className = "btn ghost btn-sm"; + removeBtn.innerText = t("config.aot_remove"); + removeBtn.onclick = () => { + div.remove(); + autoSave(); + }; + div.appendChild(removeBtn); + return div; } function buildAotTemplate(path, arr) { if (arr && arr.length > 0) { - const template = buildEmptyStructuredValue(arr[0]) + const template = buildEmptyStructuredValue(arr[0]); if (AOT_PATHS.has(path)) { - if (!Object.prototype.hasOwnProperty.call(template, "request_params")) { - template.request_params = {} + if ( + !Object.prototype.hasOwnProperty.call( + template, + "request_params", + ) + ) { + template.request_params = {}; } if (!Object.prototype.hasOwnProperty.call(template, "api_mode")) { - template.api_mode = "chat_completions" + template.api_mode = "chat_completions"; } - if (!Object.prototype.hasOwnProperty.call(template, "thinking_tool_call_compat")) { - template.thinking_tool_call_compat = true + if ( + !Object.prototype.hasOwnProperty.call( + template, + "thinking_tool_call_compat", + ) + ) { + template.thinking_tool_call_compat = true; } - if (!Object.prototype.hasOwnProperty.call(template, "responses_tool_choice_compat")) { - template.responses_tool_choice_compat = false + if ( + !Object.prototype.hasOwnProperty.call( + template, + "responses_tool_choice_compat", + ) + ) { + template.responses_tool_choice_compat = false; } - if (!Object.prototype.hasOwnProperty.call(template, "responses_force_stateless_replay")) { - template.responses_force_stateless_replay = false + if ( + !Object.prototype.hasOwnProperty.call( + template, + "responses_force_stateless_replay", + ) + ) { + template.responses_force_stateless_replay = false; } - if (!Object.prototype.hasOwnProperty.call(template, "reasoning_enabled")) { - template.reasoning_enabled = false + if ( + !Object.prototype.hasOwnProperty.call( + template, + "reasoning_enabled", + ) + ) { + template.reasoning_enabled = false; } - if (!Object.prototype.hasOwnProperty.call(template, "reasoning_effort")) { - template.reasoning_effort = "medium" + if ( + !Object.prototype.hasOwnProperty.call( + template, + "reasoning_effort", + ) + ) { + template.reasoning_effort = "medium"; } } - return template + return template; } - return { model_name: "", api_url: "", api_key: "", api_mode: "chat_completions", thinking_tool_call_compat: true, responses_tool_choice_compat: false, responses_force_stateless_replay: false, reasoning_enabled: false, reasoning_effort: "medium", request_params: {} } + return { + model_name: "", + api_url: "", + api_key: "", + api_mode: "chat_completions", + thinking_tool_call_compat: true, + responses_tool_choice_compat: false, + responses_force_stateless_replay: false, + reasoning_enabled: false, + reasoning_effort: "medium", + request_params: {}, + }; } function createAotWidget(path, arr) { - const container = document.createElement("div") - container.className = "form-group" - container.dataset.path = path - const lbl = document.createElement("div") - lbl.className = "form-label" - lbl.innerText = path.split(".").pop() - container.appendChild(lbl) - const comment = getComment(path) + const container = document.createElement("div"); + container.className = "form-group"; + container.dataset.path = path; + const lbl = document.createElement("div"); + lbl.className = "form-label"; + lbl.innerText = path.split(".").pop(); + container.appendChild(lbl); + const comment = getComment(path); if (comment) { - const hint = document.createElement("div") - hint.className = "form-hint" - hint.innerText = comment - hint.dataset.commentPath = path - container.appendChild(hint) - } - container.dataset.searchText = `${path} ${comment || ""}`.toLowerCase() - const entriesDiv = document.createElement("div") - entriesDiv.dataset.aotPath = path - container.appendChild(entriesDiv) - ;(arr || []).forEach(entry => entriesDiv.appendChild(createAotEntry(path, entry))) - const addBtn = document.createElement("button") - addBtn.type = "button" - addBtn.className = "btn ghost btn-sm" - addBtn.style.marginTop = "4px" - addBtn.innerText = t("config.aot_add") - addBtn.onclick = () => { - entriesDiv.appendChild(createAotEntry(path, buildAotTemplate(path, arr))) - autoSave() + const hint = document.createElement("div"); + hint.className = "form-hint"; + hint.innerText = comment; + hint.dataset.commentPath = path; + container.appendChild(hint); } - container.appendChild(addBtn) - return container + container.dataset.searchText = `${path} ${comment || ""}`.toLowerCase(); + const entriesDiv = document.createElement("div"); + entriesDiv.dataset.aotPath = path; + container.appendChild(entriesDiv); + (arr || []).forEach((entry) => + entriesDiv.appendChild(createAotEntry(path, entry)), + ); + const addBtn = document.createElement("button"); + addBtn.type = "button"; + addBtn.className = "btn ghost btn-sm"; + addBtn.style.marginTop = "4px"; + addBtn.innerText = t("config.aot_add"); + addBtn.onclick = () => { + entriesDiv.appendChild( + createAotEntry(path, buildAotTemplate(path, arr)), + ); + autoSave(); + }; + container.appendChild(addBtn); + return container; } function parseInputValue(input) { - const valueType = input.dataset.valueType || "string" + const valueType = input.dataset.valueType || "string"; if (valueType === "boolean") { - return input.checked + return input.checked; } - const raw = input.value + const raw = input.value; if (valueType === "number") { - const trimmed = raw.trim() - if (!trimmed) return "" - const parsed = trimmed.includes(".") ? parseFloat(trimmed) : parseInt(trimmed, 10) - return Number.isNaN(parsed) ? raw : parsed + const trimmed = raw.trim(); + if (!trimmed) return ""; + const parsed = trimmed.includes(".") + ? parseFloat(trimmed) + : parseInt(trimmed, 10); + return Number.isNaN(parsed) ? raw : parsed; } if (valueType === "array") { - const items = raw.split(",").map(item => item.trim()).filter(Boolean) + const items = raw + .split(",") + .map((item) => item.trim()) + .filter(Boolean); return input.dataset.arrayType === "number" - ? items.map(item => { - const number = Number(item) - return Number.isNaN(number) ? item : number - }) - : items + ? items.map((item) => { + const number = Number(item); + return Number.isNaN(number) ? item : number; + }) + : items; } - return raw + return raw; } async function autoSave() { - showSaveStatus("saving") - - const patch = {} - document.querySelectorAll(".config-input").forEach(input => { - patch[input.dataset.path] = parseInputValue(input) - }) - - document.querySelectorAll("[data-request-params-root]").forEach(editor => { - const group = editor.closest(".form-group") - if (!group?.dataset.path) return - patch[group.dataset.path] = readStructuredValueEditor(editor) - }) - - document.querySelectorAll("[data-aot-path]").forEach(container => { - const aotPath = container.dataset.aotPath - const entries = [] - container.querySelectorAll(".aot-entry").forEach(entry => { - const obj = {} - entry.querySelectorAll(".aot-entry-field").forEach(field => { - const key = field.dataset.fieldKey - if (!key) return + showSaveStatus("saving"); + + const patch = {}; + document.querySelectorAll(".config-input").forEach((input) => { + patch[input.dataset.path] = parseInputValue(input); + }); + + document + .querySelectorAll("[data-request-params-root]") + .forEach((editor) => { + const group = editor.closest(".form-group"); + if (!group?.dataset.path) return; + patch[group.dataset.path] = readStructuredValueEditor(editor); + }); + + document.querySelectorAll("[data-aot-path]").forEach((container) => { + const aotPath = container.dataset.aotPath; + const entries = []; + container.querySelectorAll(".aot-entry").forEach((entry) => { + const obj = {}; + entry.querySelectorAll(".aot-entry-field").forEach((field) => { + const key = field.dataset.fieldKey; + if (!key) return; if (field.dataset.fieldEditor === "structured") { - const valueNode = getStructuredValueChild(field) - obj[key] = valueNode ? readStructuredValueEditor(valueNode) : {} - return + const valueNode = getStructuredValueChild(field); + obj[key] = valueNode + ? readStructuredValueEditor(valueNode) + : {}; + return; } - const input = field.querySelector(".aot-field-input") - if (!input) return - obj[key] = parseInputValue(input) - }) - entries.push(obj) - }) - patch[aotPath] = entries - }) + const input = field.querySelector(".aot-field-input"); + if (!input) return; + obj[key] = parseInputValue(input); + }); + entries.push(obj); + }); + patch[aotPath] = entries; + }); try { - const res = await api("/api/patch", { method: "POST", body: JSON.stringify({ patch }) }) - const data = await res.json() + const res = await api("/api/patch", { + method: "POST", + body: JSON.stringify({ patch }), + }); + const data = await res.json(); if (data.success) { - showSaveStatus("saved") - if (data.warning) showToast(`${t("common.warning")}: ${data.warning}`, "warning", 5000) + showSaveStatus("saved"); + if (data.warning) + showToast( + `${t("common.warning")}: ${data.warning}`, + "warning", + 5000, + ); } else { - showSaveStatus("error", t("config.save_error")) - showToast(`${t("common.error")}: ${data.error}`, "error", 5000) + showSaveStatus("error", t("config.save_error")); + showToast(`${t("common.error")}: ${data.error}`, "error", 5000); } } catch (e) { - showSaveStatus("error", t("config.save_network_error")) - showToast(`${t("common.error")}: ${e.message}`, "error", 5000) + showSaveStatus("error", t("config.save_network_error")); + showToast(`${t("common.error")}: ${e.message}`, "error", 5000); } } @@ -905,15 +1062,25 @@ async function syncConfigTemplate(button) { const data = await res.json(); if (!data.success) { showSaveStatus("error", t("config.save_error")); - showToast(`${t("common.error")}: ${data.error || t("config.sync_error")}`, "error", 5000); + showToast( + `${t("common.error")}: ${data.error || t("config.sync_error")}`, + "error", + 5000, + ); return; } await loadConfig(); showSaveStatus("saved", t("config.saved")); if (data.warning) { - showToast(`${t("common.warning")}: ${data.warning}`, "warning", 5000); + showToast( + `${t("common.warning")}: ${data.warning}`, + "warning", + 5000, + ); } - const suffix = Number.isFinite(data.added_count) ? ` (+${data.added_count})` : ""; + const suffix = Number.isFinite(data.added_count) + ? ` (+${data.added_count})` + : ""; showToast(`${t("config.sync_success")}${suffix}`, "info", 4000); } catch (e) { showSaveStatus("error", t("config.sync_error")); diff --git a/src/Undefined/webui/static/js/i18n.js b/src/Undefined/webui/static/js/i18n.js index 59c913e3..2959eec8 100644 --- a/src/Undefined/webui/static/js/i18n.js +++ b/src/Undefined/webui/static/js/i18n.js @@ -53,7 +53,8 @@ const I18N = { "auth.placeholder": "请输入 WebUI 密码", "auth.sign_in": "登 录", "auth.sign_out": "退出登录", - "auth.default_password": "默认密码仍在使用,请尽快修改 webui.password 并重启 WebUI。", + "auth.default_password": + "默认密码仍在使用,请尽快修改 webui.password 并重启 WebUI。", "auth.change_required": "默认密码已禁用,请先设置新密码。", "auth.reset_title": "设置新密码", "auth.current_placeholder": "当前密码", @@ -70,9 +71,11 @@ const I18N = { "config.subtitle": "按分类逐项调整配置,保存后自动触发热更新。", "config.save": "保存更改", "config.reset": "重置更改", - "config.reset_confirm": "确定要撤销所有本地更改吗?这将从服务器重新加载配置。", + "config.reset_confirm": + "确定要撤销所有本地更改吗?这将从服务器重新加载配置。", "config.sync_template": "同步模板", - "config.sync_confirm": "确定要把 config.toml.example 中的新配置项和注释同步到当前 config.toml 吗?现有配置值会保留。", + "config.sync_confirm": + "确定要把 config.toml.example 中的新配置项和注释同步到当前 config.toml 吗?现有配置值会保留。", "config.syncing": "同步模板中...", "config.sync_success": "模板同步完成", "config.sync_error": "模板同步失败", @@ -92,7 +95,8 @@ const I18N = { "config.save_network_error": "网络错误", "config.reload_success": "配置已从服务器重新加载。", "config.reload_error": "配置重载失败。", - "config.bootstrap_created": "检测到缺少 config.toml,已从示例生成;请在此页完善配置并保存。", + "config.bootstrap_created": + "检测到缺少 config.toml,已从示例生成;请在此页完善配置并保存。", "logs.title": "运行日志", "logs.subtitle": "实时查看日志尾部输出。", "logs.auto": "自动刷新", @@ -133,6 +137,7 @@ const I18N = { "probes.section_queues": "请求队列", "probes.section_services": "服务状态", "probes.section_skills": "技能统计", + "probes.section_bootstrap": "引导与管理探针", "probes.version": "版本", "probes.platform": "平台", "probes.uptime": "运行时间", @@ -145,6 +150,18 @@ const I18N = { "probes.api_listen": "监听地址", "probes.tools": "基础工具", "probes.agents": "智能体", + "probes.bootstrap_config": "配置文件", + "probes.bootstrap_validation": "严格校验", + "probes.bootstrap_auth": "认证状态", + "probes.bootstrap_runtime": "运行态", + "probes.bootstrap_config_exists": + "已检测到 config.toml,可继续在控制台编辑。", + "probes.bootstrap_config_missing": + "尚未检测到 config.toml,控制台会先生成模板供你补齐。", + "probes.bootstrap_runtime_pending": + "管理层在线,但运行态尚未就绪;完善配置后可直接启动 Bot。", + "probes.bootstrap_ready": + "管理入口已就绪,可继续编辑配置、查看日志或启动实例。", "probes.all_ok": "所有外部端点均正常", "probes.some_failed": "部分外部端点异常", "memory.title": "记忆检索", @@ -173,7 +190,8 @@ const I18N = { "runtime.cognitive_profile_get": "按实体查看侧写", "runtime.fetch": "获取", "runtime.chat_title": "AI Chat(虚拟私聊 system#42)", - "runtime.chat_hint": "该会话由 WebUI 发起,权限为 superadmin;私聊里可直接使用 /命令。", + "runtime.chat_hint": + "该会话由 WebUI 发起,权限为 superadmin;私聊里可直接使用 /命令。", "runtime.chat_placeholder": "输入消息,或直接 /help 这样的命令", "runtime.image": "图片", "runtime.image_added": "已插入图片", @@ -208,7 +226,8 @@ const I18N = { en: { "landing.title": "Undefined Console", "landing.kicker": "WebUI", - "landing.subtitle": "A unified entry point for configuration, log tracking, and runtime control.", + "landing.subtitle": + "A unified entry point for configuration, log tracking, and runtime control.", "landing.cta": "Enter Console", "landing.config": "Edit Config", "landing.logs": "View Logs", @@ -259,8 +278,10 @@ const I18N = { "auth.placeholder": "WebUI password", "auth.sign_in": "Sign In", "auth.sign_out": "Sign Out", - "auth.default_password": "Default password is in use. Please change webui.password and restart.", - "auth.change_required": "Default password is disabled. Please set a new password.", + "auth.default_password": + "Default password is in use. Please change webui.password and restart.", + "auth.change_required": + "Default password is disabled. Please set a new password.", "auth.reset_title": "Set New Password", "auth.current_placeholder": "Current password", "auth.new_placeholder": "New password", @@ -268,17 +289,21 @@ const I18N = { "auth.password_updated": "Password updated. Please sign in again.", "auth.password_updated_login": "Password updated. Signing in...", "auth.password_update_failed": "Password update failed", - "auth.password_change_local": "Password change requires local access when using default password.", + "auth.password_change_local": + "Password change requires local access when using default password.", "auth.signing_in": "Signing in...", "auth.login_failed": "Login failed", "auth.unauthorized": "Unauthorized or session expired", "config.title": "Configuration", - "config.subtitle": "Adjust settings by category. Changes trigger hot reload.", + "config.subtitle": + "Adjust settings by category. Changes trigger hot reload.", "config.save": "Save Changes", "config.reset": "Revert Changes", - "config.reset_confirm": "Are you sure you want to revert all local changes? This will reload the configuration from the server.", + "config.reset_confirm": + "Are you sure you want to revert all local changes? This will reload the configuration from the server.", "config.sync_template": "Sync Template", - "config.sync_confirm": "Sync new settings and comments from config.toml.example into the current config.toml? Existing configured values will be kept.", + "config.sync_confirm": + "Sync new settings and comments from config.toml.example into the current config.toml? Existing configured values will be kept.", "config.syncing": "Syncing template...", "config.sync_success": "Template sync completed", "config.sync_error": "Template sync failed", @@ -298,7 +323,8 @@ const I18N = { "config.save_network_error": "Network error", "config.reload_success": "Configuration reloaded from server.", "config.reload_error": "Failed to reload configuration.", - "config.bootstrap_created": "config.toml was missing and has been generated from the example. Please review and save your configuration.", + "config.bootstrap_created": + "config.toml was missing and has been generated from the example. Please review and save your configuration.", "logs.title": "System Logs", "logs.subtitle": "Real-time view of recent log output.", "logs.auto": "Auto Refresh", @@ -339,6 +365,7 @@ const I18N = { "probes.section_queues": "Request Queues", "probes.section_services": "Service Status", "probes.section_skills": "Skill Statistics", + "probes.section_bootstrap": "Bootstrap & Management", "probes.version": "Version", "probes.platform": "Platform", "probes.uptime": "Uptime", @@ -351,10 +378,23 @@ const I18N = { "probes.api_listen": "Listen Addr", "probes.tools": "Tools", "probes.agents": "Agents", + "probes.bootstrap_config": "Config File", + "probes.bootstrap_validation": "Strict Validation", + "probes.bootstrap_auth": "Auth", + "probes.bootstrap_runtime": "Runtime", + "probes.bootstrap_config_exists": + "config.toml is present and can be edited in the console.", + "probes.bootstrap_config_missing": + "config.toml is missing; the console will bootstrap a template for recovery.", + "probes.bootstrap_runtime_pending": + "Management is online but runtime is not ready yet; finish config and start the bot.", + "probes.bootstrap_ready": + "Management entry is ready for config edits, logs, and bot startup.", "probes.all_ok": "All external endpoints are healthy", "probes.some_failed": "Some external endpoints are unhealthy", "memory.title": "Memory Hub", - "memory.subtitle": "Read-only search for memory, cognitive events, and profiles.", + "memory.subtitle": + "Read-only search for memory, cognitive events, and profiles.", "memory.refresh": "Refresh", "runtime.title": "Runtime Hub", "runtime.subtitle": "Probes plus memory/profile read-only queries.", @@ -379,7 +419,8 @@ const I18N = { "runtime.cognitive_profile_get": "Get Profile by Entity", "runtime.fetch": "Fetch", "runtime.chat_title": "AI Chat (virtual private chat system#42)", - "runtime.chat_hint": "This WebUI session runs as superadmin; slash commands work directly in private chat.", + "runtime.chat_hint": + "This WebUI session runs as superadmin; slash commands work directly in private chat.", "runtime.chat_placeholder": "Type a message, or run /help directly", "runtime.image": "Image", "runtime.image_added": "Image inserted", @@ -391,7 +432,8 @@ const I18N = { "runtime.disabled": "Runtime API is disabled", "runtime.found": "Found", "runtime.not_found": "Not found", - "runtime.api_start_hint": "Please start the bot process in WebUI first.", + "runtime.api_start_hint": + "Please start the bot process in WebUI first.", "chat.title": "AI Dialog", "chat.subtitle": "Virtual private session system#42.", "about.title": "About Project", @@ -405,10 +447,11 @@ const I18N = { "update.working": "Checking for updates...", "update.updated_restarting": "Updated. Restarting WebUI...", "update.uptodate_restarting": "Up to date. Restarting WebUI...", - "update.not_eligible": "Update not eligible (official origin/main only)", + "update.not_eligible": + "Update not eligible (official origin/main only)", "update.failed": "Update failed", "update.no_restart": "Updated but not restarted (check uv sync output)", "config.aot_add": "+ Add Entry", "config.aot_remove": "Remove", - } + }, }; diff --git a/src/Undefined/webui/static/js/log-view.js b/src/Undefined/webui/static/js/log-view.js index d2c61a19..23d6ff8b 100644 --- a/src/Undefined/webui/static/js/log-view.js +++ b/src/Undefined/webui/static/js/log-view.js @@ -11,7 +11,10 @@ async function fetchLogs(force = false) { return; } try { - const params = new URLSearchParams({ lines: "200", type: state.logType }); + const params = new URLSearchParams({ + lines: "200", + type: state.logType, + }); if (state.logFile) params.set("file", state.logFile); const res = await api(`/api/logs?${params.toString()}`); const text = await res.text(); @@ -22,7 +25,10 @@ async function fetchLogs(force = false) { recordFetchError("logs"); if (!container) return; container.dataset.placeholder = "true"; - container.innerText = e.message === "Unauthorized" ? t("logs.unauthorized") : t("logs.error"); + container.innerText = + e.message === "Unauthorized" + ? t("logs.unauthorized") + : t("logs.error"); updateLogMeta(0, 0); } } @@ -31,14 +37,20 @@ function filterLogLines(raw) { const query = state.logSearch.trim().toLowerCase(); const rawLines = raw ? raw.split(/\r?\n/) : []; const base = window.LogsController - ? window.LogsController.filterLogLines(raw, { level: state.logLevel, gte: state.logLevelGte }) + ? window.LogsController.filterLogLines(raw, { + level: state.logLevel, + gte: state.logLevelGte, + }) : { filtered: rawLines, total: rawLines.length }; let filtered = base.filtered; - if (query) filtered = filtered.filter(line => line.toLowerCase().includes(query)); + if (query) + filtered = filtered.filter((line) => + line.toLowerCase().includes(query), + ); const total = base.total ?? rawLines.length; - const matched = filtered.filter(line => line.length > 0).length; + const matched = filtered.filter((line) => line.length > 0).length; return { filtered, total, matched }; } @@ -48,11 +60,14 @@ function formatLogText(text) { const query = state.logSearch.trim(); if (query) { const regex = new RegExp(escapeRegExp(query), "gi"); - escaped = escaped.replace(regex, '$&'); + escaped = escaped.replace( + regex, + '$&', + ); } escaped = escaped.replace( /(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)/g, - '$1' + '$1', ); return escaped .replace(/\x1b\[31m/g, '') @@ -61,7 +76,7 @@ function formatLogText(text) { .replace(/\x1b\[34m/g, '') .replace(/\x1b\[35m/g, '') .replace(/\x1b\[36m/g, '') - .replace(/\x1b\[0m/g, ''); + .replace(/\x1b\[0m/g, ""); } function renderLogs() { @@ -86,7 +101,8 @@ function renderLogs() { } container.innerHTML = formatLogText(filtered.join("\n")); container.dataset.placeholder = "false"; - if (state.logAutoRefresh && state.logAtBottom) container.scrollTop = container.scrollHeight; + if (state.logAutoRefresh && state.logAtBottom) + container.scrollTop = container.scrollHeight; updateLogJumpButton(); updateLogMeta(total, matched); } @@ -96,8 +112,14 @@ function updateLogMeta(total, matched) { if (!meta) return; const parts = []; if (state.logsPaused) parts.push(t("logs.paused")); - if (state.logLevel !== "all" || state.logSearch.trim() || state.logLevelGte) { - parts.push(`${t("logs.filtered")}: ${total > 0 ? `${matched}/${total}` : "0/0"}`); + if ( + state.logLevel !== "all" || + state.logSearch.trim() || + state.logLevelGte + ) { + parts.push( + `${t("logs.filtered")}: ${total > 0 ? `${matched}/${total}` : "0/0"}`, + ); } meta.innerText = parts.join(" | "); } @@ -114,7 +136,11 @@ function bindLogScroll() { const container = get("logContainer"); if (!container) return; container.addEventListener("scroll", () => { - state.logAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 24; + state.logAtBottom = + container.scrollHeight - + container.scrollTop - + container.clientHeight < + 24; updateLogJumpButton(); }); state.logScrollBound = true; @@ -122,7 +148,8 @@ function bindLogScroll() { } function startLogStream() { - if (state.logStream || state.logStreamFailed || !window.EventSource) return false; + if (state.logStream || state.logStreamFailed || !window.EventSource) + return false; if (!state.logStreamEnabled) return false; state.logStreamFailed = false; const params = new URLSearchParams({ lines: "200", type: state.logType }); @@ -142,24 +169,46 @@ function startLogStream() { } function stopLogStream() { - if (state.logStream) { state.logStream.close(); state.logStream = null; } + if (state.logStream) { + state.logStream.close(); + state.logStream = null; + } } function updateLogRefreshState() { - if (state.view !== "app" || state.tab !== "logs" || document.hidden || !state.authenticated) { - stopLogStream(); stopLogTimer(); return; + if ( + state.view !== "app" || + state.tab !== "logs" || + document.hidden || + !state.authenticated + ) { + stopLogStream(); + stopLogTimer(); + return; } if (state.logsPaused || !state.logAutoRefresh) { - stopLogStream(); stopLogTimer(); return; + stopLogStream(); + stopLogTimer(); + return; + } + if (!state.logStreamEnabled) { + stopLogStream(); + startLogTimer(); + return; + } + if (startLogStream()) { + stopLogTimer(); + return; } - if (!state.logStreamEnabled) { stopLogStream(); startLogTimer(); return; } - if (startLogStream()) { stopLogTimer(); return; } startLogTimer(); } async function copyLogsToClipboard() { const text = state.logsRaw || ""; - if (!text) { showToast(t("logs.empty"), "info"); return; } + if (!text) { + showToast(t("logs.empty"), "info"); + return; + } try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); @@ -168,7 +217,8 @@ async function copyLogsToClipboard() { textarea.value = text; textarea.style.cssText = "position:fixed;opacity:0"; document.body.appendChild(textarea); - textarea.focus(); textarea.select(); + textarea.focus(); + textarea.select(); document.execCommand("copy"); textarea.remove(); } @@ -179,7 +229,10 @@ async function copyLogsToClipboard() { } async function fetchLogFiles(force = false) { - if (state.logFiles[state.logType] && !force) { updateLogFileSelect(); return; } + if (state.logFiles[state.logType] && !force) { + updateLogFileSelect(); + return; + } try { const res = await api(`/api/logs/files?type=${state.logType}`); const data = await res.json(); @@ -205,7 +258,10 @@ function setLogType(type) { function downloadLogs() { const text = state.logsRaw || ""; - if (!text) { showToast(t("logs.empty"), "info"); return; } + if (!text) { + showToast(t("logs.empty"), "info"); + return; + } const blob = new Blob([text], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); diff --git a/src/Undefined/webui/static/js/logs.js b/src/Undefined/webui/static/js/logs.js index b22edd66..7abf367a 100644 --- a/src/Undefined/webui/static/js/logs.js +++ b/src/Undefined/webui/static/js/logs.js @@ -63,7 +63,7 @@ function getLogLevels() { const levels = Object.keys(LOG_LEVEL_DEFS).sort( - (a, b) => LOG_LEVEL_DEFS[b].rank - LOG_LEVEL_DEFS[a].rank + (a, b) => LOG_LEVEL_DEFS[b].rank - LOG_LEVEL_DEFS[a].rank, ); return ["all", ...levels]; } diff --git a/src/Undefined/webui/static/js/main.js b/src/Undefined/webui/static/js/main.js index a9d036bd..aa64f839 100644 --- a/src/Undefined/webui/static/js/main.js +++ b/src/Undefined/webui/static/js/main.js @@ -1,6 +1,7 @@ function refreshUI() { updateI18N(); - get("view-landing").className = state.view === "landing" ? "full-view active" : "full-view"; + get("view-landing").className = + state.view === "landing" ? "full-view active" : "full-view"; get("view-app").style.display = state.view === "app" ? "grid" : "none"; if (state.view === "app") { @@ -24,15 +25,20 @@ function refreshUI() { mainContent.classList.toggle("chat-layout", state.tab === "chat"); } - if (initialState && initialState.version) get("about-version-display").innerText = initialState.version; - if (initialState && initialState.license) get("about-license-display").innerText = initialState.license; + if (initialState && initialState.version) + get("about-version-display").innerText = initialState.version; + if (initialState && initialState.license) + get("about-license-display").innerText = initialState.license; updateAuthPanels(); if (state.view !== "app" || !state.authenticated) { - stopSystemTimer(); stopLogStream(); stopLogTimer(); + stopSystemTimer(); + stopLogStream(); + stopLogTimer(); } - if (state.view === "app" && state.tab === "logs" && state.authenticated) fetchLogFiles(); + if (state.view === "app" && state.tab === "logs" && state.authenticated) + fetchLogFiles(); updateLogRefreshState(); } @@ -42,15 +48,18 @@ function switchTab(tab) { if (mainContent) { mainContent.classList.toggle("chat-layout", tab === "chat"); } - document.querySelectorAll(".nav-item").forEach(el => { + document.querySelectorAll(".nav-item").forEach((el) => { el.classList.toggle("active", el.getAttribute("data-tab") === tab); }); - document.querySelectorAll(".tab-content").forEach(el => { + document.querySelectorAll(".tab-content").forEach((el) => { el.classList.toggle("active", el.id === `tab-${tab}`); }); if (tab === "overview") { - if (!document.hidden) { startSystemTimer(); fetchSystemInfo(); } + if (!document.hidden) { + startSystemTimer(); + fetchSystemInfo(); + } } else { stopSystemTimer(); } @@ -62,20 +71,47 @@ function switchTab(tab) { if (!state.logsPaused) fetchLogs(true); } } else { - stopLogStream(); stopLogTimer(); + stopLogStream(); + stopLogTimer(); } - if (window.RuntimeController && typeof window.RuntimeController.onTabActivated === "function") { + if ( + window.RuntimeController && + typeof window.RuntimeController.onTabActivated === "function" + ) { window.RuntimeController.onTabActivated(tab); } } +function canReturnToLauncher(url) { + try { + const parsed = new URL(String(url || "")); + const protocol = parsed.protocol.toLowerCase(); + const hostname = parsed.hostname.toLowerCase(); + if (protocol === "tauri:") { + return hostname === "localhost" || hostname === ""; + } + if (!["http:", "https:"].includes(protocol)) return false; + return ( + hostname === "localhost" || + hostname === "127.0.0.1" || + hostname === "::1" || + hostname.endsWith(".localhost") + ); + } catch (_error) { + return false; + } +} + async function init() { - if (window.RuntimeController && typeof window.RuntimeController.init === "function") { + if ( + window.RuntimeController && + typeof window.RuntimeController.init === "function" + ) { window.RuntimeController.init(); } - document.querySelectorAll('[data-action="toggle-lang"]').forEach(btn => { + document.querySelectorAll('[data-action="toggle-lang"]').forEach((btn) => { btn.addEventListener("click", () => { state.lang = state.lang === "zh" ? "en" : "zh"; setCookie("undefined_lang", state.lang); @@ -83,72 +119,137 @@ async function init() { }); }); - document.querySelectorAll('[data-action="toggle-theme"]').forEach(btn => { - btn.addEventListener("click", () => applyTheme(state.theme === "dark" ? "light" : "dark")); + document.querySelectorAll('[data-action="toggle-theme"]').forEach((btn) => { + btn.addEventListener("click", () => + applyTheme(state.theme === "dark" ? "light" : "dark"), + ); }); - document.querySelectorAll('[data-action="open-app"]').forEach(el => { - el.onclick = () => { state.view = "app"; switchTab(el.getAttribute("data-tab")); refreshUI(); }; + document.querySelectorAll('[data-action="open-app"]').forEach((el) => { + el.onclick = () => { + state.view = "app"; + switchTab(el.getAttribute("data-tab")); + refreshUI(); + }; }); get("botStartBtnLanding").onclick = () => { - if (!state.authenticated) { get("landingLoginStatus").innerText = t("auth.subtitle"); get("landingPasswordInput").focus(); return; } + if (!state.authenticated) { + get("landingLoginStatus").innerText = t("auth.subtitle"); + get("landingPasswordInput").focus(); + return; + } botAction("start"); }; get("botStopBtnLanding").onclick = () => { - if (!state.authenticated) { get("landingLoginStatus").innerText = t("auth.subtitle"); get("landingPasswordInput").focus(); return; } + if (!state.authenticated) { + get("landingLoginStatus").innerText = t("auth.subtitle"); + get("landingPasswordInput").focus(); + return; + } botAction("stop"); }; - get("landingLoginBtn").onclick = () => login(get("landingPasswordInput").value, "landingLoginStatus", "landingLoginBtn"); - get("appLoginBtn").onclick = () => login(get("appPasswordInput").value, "appLoginStatus", "appLoginBtn"); + get("landingLoginBtn").onclick = () => + login( + get("landingPasswordInput").value, + "landingLoginStatus", + "landingLoginBtn", + ); + get("appLoginBtn").onclick = () => + login(get("appPasswordInput").value, "appLoginStatus", "appLoginBtn"); const landingResetBtn = get("landingResetPasswordBtn"); if (landingResetBtn) { - landingResetBtn.onclick = () => changePassword("landingCurrentPasswordInput", "landingNewPasswordInput", "landingResetStatus", "landingResetPasswordBtn"); + landingResetBtn.onclick = () => + changePassword( + "landingCurrentPasswordInput", + "landingNewPasswordInput", + "landingResetStatus", + "landingResetPasswordBtn", + ); } const appResetBtn = get("appResetPasswordBtn"); if (appResetBtn) { - appResetBtn.onclick = () => changePassword("appCurrentPasswordInput", "appNewPasswordInput", "appResetStatus", "appResetPasswordBtn"); + appResetBtn.onclick = () => + changePassword( + "appCurrentPasswordInput", + "appNewPasswordInput", + "appResetStatus", + "appResetPasswordBtn", + ); } const bindEnterLogin = (inputId, statusId, btnId) => { const el = get(inputId); - if (el) el.addEventListener("keydown", e => { if (e.key === "Enter") login(el.value, statusId, btnId); }); + if (el) + el.addEventListener("keydown", (e) => { + if (e.key === "Enter") login(el.value, statusId, btnId); + }); }; - bindEnterLogin("landingPasswordInput", "landingLoginStatus", "landingLoginBtn"); + bindEnterLogin( + "landingPasswordInput", + "landingLoginStatus", + "landingLoginBtn", + ); bindEnterLogin("appPasswordInput", "appLoginStatus", "appLoginBtn"); const bindEnterReset = (currentId, newId, statusId, btnId) => { - [get(currentId), get(newId)].forEach(el => { - if (el) el.addEventListener("keydown", e => { if (e.key === "Enter") changePassword(currentId, newId, statusId, btnId); }); + [get(currentId), get(newId)].forEach((el) => { + if (el) + el.addEventListener("keydown", (e) => { + if (e.key === "Enter") + changePassword(currentId, newId, statusId, btnId); + }); }); }; - bindEnterReset("landingCurrentPasswordInput", "landingNewPasswordInput", "landingResetStatus", "landingResetPasswordBtn"); - bindEnterReset("appCurrentPasswordInput", "appNewPasswordInput", "appResetStatus", "appResetPasswordBtn"); - - document.querySelectorAll(".nav-item").forEach(el => { + bindEnterReset( + "landingCurrentPasswordInput", + "landingNewPasswordInput", + "landingResetStatus", + "landingResetPasswordBtn", + ); + bindEnterReset( + "appCurrentPasswordInput", + "appNewPasswordInput", + "appResetStatus", + "appResetPasswordBtn", + ); + + document.querySelectorAll(".nav-item").forEach((el) => { el.addEventListener("click", () => { const v = el.getAttribute("data-view"); const tab = el.getAttribute("data-tab"); - if (v === "landing") { state.view = "landing"; refreshUI(); } - else if (tab) switchTab(tab); + if (v === "landing") { + state.view = "landing"; + refreshUI(); + } else if (tab) switchTab(tab); + }); + el.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + el.click(); + } }); - el.addEventListener("keydown", e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); el.click(); } }); }); const resetBtn = get("btnResetConfig"); if (resetBtn) resetBtn.onclick = resetConfig; const syncConfigBtn = get("btnSyncConfigTemplate"); - if (syncConfigBtn) syncConfigBtn.onclick = () => syncConfigTemplate(syncConfigBtn); + if (syncConfigBtn) + syncConfigBtn.onclick = () => syncConfigTemplate(syncConfigBtn); const refreshLogsBtn = get("btnRefreshLogs"); if (refreshLogsBtn) { refreshLogsBtn.onclick = async () => { state.logStreamFailed = false; setButtonLoading(refreshLogsBtn, true); - try { await fetchLogs(true); } finally { setButtonLoading(refreshLogsBtn, false); } + try { + await fetchLogs(true); + } finally { + setButtonLoading(refreshLogsBtn, false); + } }; } @@ -156,26 +257,40 @@ async function init() { if (refreshOverviewBtn) { refreshOverviewBtn.onclick = async () => { setButtonLoading(refreshOverviewBtn, true); - try { await fetchSystemInfo(); } finally { setButtonLoading(refreshOverviewBtn, false); } + try { + await fetchSystemInfo(); + } finally { + setButtonLoading(refreshOverviewBtn, false); + } }; } const updateRestartBtn = get("btnUpdateRestart"); - if (updateRestartBtn) updateRestartBtn.onclick = () => updateAndRestartWebui(updateRestartBtn); + if (updateRestartBtn) + updateRestartBtn.onclick = () => + updateAndRestartWebui(updateRestartBtn); const pauseLogsBtn = get("btnPauseLogs"); if (pauseLogsBtn) { pauseLogsBtn.onclick = () => { state.logsPaused = !state.logsPaused; - pauseLogsBtn.innerText = state.logsPaused ? t("logs.resume") : t("logs.pause"); + pauseLogsBtn.innerText = state.logsPaused + ? t("logs.resume") + : t("logs.pause"); renderLogs(); updateLogRefreshState(); - if (!state.logsPaused) { state.logStreamFailed = false; fetchLogs(true); } + if (!state.logsPaused) { + state.logStreamFailed = false; + fetchLogs(true); + } }; } - document.querySelectorAll(".log-tab").forEach(tab => { - tab.addEventListener("click", () => { setLogType(tab.dataset.logType || "bot"); fetchLogs(true); }); + document.querySelectorAll(".log-tab").forEach((tab) => { + tab.addEventListener("click", () => { + setLogType(tab.dataset.logType || "bot"); + fetchLogs(true); + }); }); const logFileSelect = get("logFileSelect"); @@ -193,29 +308,46 @@ async function init() { state.logAutoRefresh = logAutoRefresh.checked; logAutoRefresh.onchange = () => { state.logAutoRefresh = logAutoRefresh.checked; - if (state.logAutoRefresh) { state.logStreamFailed = false; if (!state.logsPaused) fetchLogs(true); } + if (state.logAutoRefresh) { + state.logStreamFailed = false; + if (!state.logsPaused) fetchLogs(true); + } updateLogRefreshState(); }; } const logLevelFilter = get("logLevelFilter"); if (logLevelFilter) { - logLevelFilter.onchange = () => { state.logLevel = logLevelFilter.value || "all"; renderLogs(); }; + logLevelFilter.onchange = () => { + state.logLevel = logLevelFilter.value || "all"; + renderLogs(); + }; } const logLevelGteToggle = get("logLevelGteToggle"); if (logLevelGteToggle) { state.logLevelGte = logLevelGteToggle.checked; - logLevelGteToggle.onchange = () => { state.logLevelGte = logLevelGteToggle.checked; renderLogs(); }; + logLevelGteToggle.onchange = () => { + state.logLevelGte = logLevelGteToggle.checked; + renderLogs(); + }; } const logSearchInput = get("logSearchInput"); if (logSearchInput) { - logSearchInput.addEventListener("input", () => { state.logSearch = logSearchInput.value || ""; renderLogs(); }); + logSearchInput.addEventListener("input", () => { + state.logSearch = logSearchInput.value || ""; + renderLogs(); + }); } const logClearBtn = get("btnClearLogs"); - if (logClearBtn) logClearBtn.onclick = () => { state.logsRaw = ""; renderLogs(); showToast(t("logs.cleared"), "info"); }; + if (logClearBtn) + logClearBtn.onclick = () => { + state.logsRaw = ""; + renderLogs(); + showToast(t("logs.cleared"), "info"); + }; const logCopyBtn = get("btnCopyLogs"); if (logCopyBtn) logCopyBtn.onclick = copyLogsToClipboard; @@ -227,53 +359,103 @@ async function init() { if (logJumpBtn) { logJumpBtn.onclick = () => { const container = get("logContainer"); - if (container) { container.scrollTop = container.scrollHeight; state.logAtBottom = true; updateLogJumpButton(); } + if (container) { + container.scrollTop = container.scrollHeight; + state.logAtBottom = true; + updateLogJumpButton(); + } }; } const configSearchInput = get("configSearchInput"); if (configSearchInput) { - configSearchInput.addEventListener("input", () => { state.configSearch = configSearchInput.value || ""; applyConfigFilter(); }); + configSearchInput.addEventListener("input", () => { + state.configSearch = configSearchInput.value || ""; + applyConfigFilter(); + }); } const configSearchClear = get("configSearchClear"); if (configSearchClear && configSearchInput) { - configSearchClear.onclick = () => { configSearchInput.value = ""; state.configSearch = ""; applyConfigFilter(); configSearchInput.focus(); }; + configSearchClear.onclick = () => { + configSearchInput.value = ""; + state.configSearch = ""; + applyConfigFilter(); + configSearchInput.focus(); + }; } const expandAllBtn = get("btnExpandAll"); - if (expandAllBtn) expandAllBtn.onclick = () => setAllSectionsCollapsed(false); + if (expandAllBtn) + expandAllBtn.onclick = () => setAllSectionsCollapsed(false); const collapseAllBtn = get("btnCollapseAll"); - if (collapseAllBtn) collapseAllBtn.onclick = () => setAllSectionsCollapsed(true); + if (collapseAllBtn) + collapseAllBtn.onclick = () => setAllSectionsCollapsed(true); const logout = async () => { - try { await api("/api/logout", { method: "POST" }); } catch (e) { } + try { + await api(authEndpointCandidates("logout"), { method: "POST" }); + } catch (e) {} + clearStoredAuthTokens(); state.authenticated = false; state.view = "landing"; - refreshUI(); + if (state.launcherMode && canReturnToLauncher(state.returnTo)) { + window.location.assign(state.returnTo); + return; + } + const target = new URL(window.location.origin + "/"); + target.searchParams.set("lang", state.lang); + target.searchParams.set("theme", state.theme); + target.searchParams.set("view", "landing"); + window.location.assign(target.toString()); }; get("logoutBtn").onclick = logout; get("mobileLogoutBtn").onclick = logout; - applyTheme(initialState && initialState.theme ? initialState.theme : "light"); + applyTheme( + initialState && initialState.theme ? initialState.theme : "light", + ); try { const session = await checkSession(); state.authenticated = !!session.authenticated; + if ( + state.authenticated && + state.authRefreshToken && + state.authAccessTokenExpiresAt + ) { + scheduleAuthRefresh(); + } } catch (e) { state.authenticated = false; } - const shouldRedirectToConfig = !!(initialState && initialState.redirect_to_config); - if (shouldRedirectToConfig) { state.view = "app"; switchTab("config"); } + if (state.view === "app") { + switchTab(state.tab || "overview"); + } + + const shouldRedirectToConfig = !!( + initialState && initialState.redirect_to_config + ); + if (shouldRedirectToConfig) { + state.view = "app"; + switchTab("config"); + } document.addEventListener("visibilitychange", () => { if (document.hidden) { - stopStatusTimer(); stopSystemTimer(); stopLogStream(); stopLogTimer(); return; + stopStatusTimer(); + stopSystemTimer(); + stopLogStream(); + stopLogTimer(); + return; } startStatusTimer(); - if (state.view === "app" && state.tab === "overview") { startSystemTimer(); fetchSystemInfo(); } + if (state.view === "app" && state.tab === "overview") { + startSystemTimer(); + fetchSystemInfo(); + } if (state.view === "app" && state.tab === "logs") { updateLogRefreshState(); if (!state.logsPaused) fetchLogs(true); @@ -281,7 +463,8 @@ async function init() { }); refreshUI(); - if (shouldRedirectToConfig) showToast(t("config.bootstrap_created"), "info", 6500); + if (shouldRedirectToConfig) + showToast(t("config.bootstrap_created"), "info", 6500); bindLogScroll(); fetchStatus(); if (!document.hidden) startStatusTimer(); diff --git a/src/Undefined/webui/static/js/runtime.js b/src/Undefined/webui/static/js/runtime.js index 12abb432..c1a1ef8d 100644 --- a/src/Undefined/webui/static/js/runtime.js +++ b/src/Undefined/webui/static/js/runtime.js @@ -2,6 +2,7 @@ const runtimeState = { initialized: false, probesLoaded: false, + bootstrapLoaded: false, memoryLoaded: false, runtimeMetaLoaded: false, runtimeEnabled: true, @@ -48,8 +49,10 @@ } function probeStatusBadge(status) { - const cls = status === "ok" ? "ok" : status === "skipped" ? "skipped" : "error"; - const label = status === "ok" ? "OK" : status === "skipped" ? "Skipped" : "Error"; + const cls = + status === "ok" ? "ok" : status === "skipped" ? "skipped" : "error"; + const label = + status === "ok" ? "OK" : status === "skipped" ? "Skipped" : "Error"; return `${escapeHtml(label)}`; } @@ -73,12 +76,28 @@ html += `
`; html += `
${t("probes.section_system")}
`; html += `
`; - html += probeItem(t("probes.version"), `v${escapeHtml(data.version || "--")}`); - html += probeItem("Python", `${escapeHtml(data.python || "--")}`); - html += probeItem(t("probes.platform"), escapeHtml(data.platform || "--")); - html += probeItem(t("probes.uptime"), `${formatUptime(data.uptime_seconds)}`); + html += probeItem( + t("probes.version"), + `v${escapeHtml(data.version || "--")}`, + ); + html += probeItem( + "Python", + `${escapeHtml(data.python || "--")}`, + ); + html += probeItem( + t("probes.platform"), + escapeHtml(data.platform || "--"), + ); + html += probeItem( + t("probes.uptime"), + `${formatUptime(data.uptime_seconds)}`, + ); html += probeItem("OneBot", probeStatusBadge(obStatus)); - if (ob.ws_url) html += probeItem("WS URL", `${escapeHtml(ob.ws_url)}`); + if (ob.ws_url) + html += probeItem( + "WS URL", + `${escapeHtml(ob.ws_url)}`, + ); html += `
`; // Models @@ -92,8 +111,10 @@ html += `
`; html += `
${escapeHtml(label)}
`; html += `
`; - if (m.model_name) html += `${t("probes.model")}: ${escapeHtml(m.model_name)}`; - if (m.api_url) html += `URL: ${escapeHtml(m.api_url)}`; + if (m.model_name) + html += `${t("probes.model")}: ${escapeHtml(m.model_name)}`; + if (m.api_url) + html += `URL: ${escapeHtml(m.api_url)}`; html += `
`; if (m.thinking_enabled !== undefined) { html += `
${m.thinking_enabled ? "Thinking ✓" : "Thinking ✗"}
`; @@ -109,9 +130,21 @@ html += `
`; html += `
${t("probes.section_queues")}
`; html += `
`; - if (q.processor_count !== undefined) html += probeItem(t("probes.processors"), String(q.processor_count)); - if (q.inflight_count !== undefined) html += probeItem(t("probes.inflight"), String(q.inflight_count)); - if (q.model_count !== undefined) html += probeItem(t("probes.model_queues"), String(q.model_count)); + if (q.processor_count !== undefined) + html += probeItem( + t("probes.processors"), + String(q.processor_count), + ); + if (q.inflight_count !== undefined) + html += probeItem( + t("probes.inflight"), + String(q.inflight_count), + ); + if (q.model_count !== undefined) + html += probeItem( + t("probes.model_queues"), + String(q.model_count), + ); html += `
`; if (q.totals) { html += `
`; @@ -130,10 +163,20 @@ html += `
${t("probes.section_services")}
`; html += `
`; html += probeItem(t("probes.memory_count"), String(mem.count ?? "--")); - html += probeItem(t("probes.cognitive"), probeStatusBadge(cog.enabled ? "ok" : "skipped")); + html += probeItem( + t("probes.cognitive"), + probeStatusBadge(cog.enabled ? "ok" : "skipped"), + ); const apiInfo = data.api || {}; - html += probeItem("Runtime API", probeStatusBadge(apiInfo.enabled ? "ok" : "error")); - if (apiInfo.enabled) html += probeItem(t("probes.api_listen"), `${escapeHtml(apiInfo.host || "")}:${apiInfo.port || ""}`); + html += probeItem( + "Runtime API", + probeStatusBadge(apiInfo.enabled ? "ok" : "error"), + ); + if (apiInfo.enabled) + html += probeItem( + t("probes.api_listen"), + `${escapeHtml(apiInfo.host || "")}:${apiInfo.port || ""}`, + ); html += `
`; // Skills @@ -142,9 +185,21 @@ html += `
`; html += `
${t("probes.section_skills")}
`; html += `
`; - if (sk.tools) html += probeItem(t("probes.tools"), `${sk.tools.loaded ?? 0} / ${sk.tools.count ?? 0}`); - if (sk.agents) html += probeItem(t("probes.agents"), `${sk.agents.loaded ?? 0} / ${sk.agents.count ?? 0}`); - if (sk.anthropic_skills) html += probeItem("Anthropic Skills", `${sk.anthropic_skills.loaded ?? 0} / ${sk.anthropic_skills.count ?? 0}`); + if (sk.tools) + html += probeItem( + t("probes.tools"), + `${sk.tools.loaded ?? 0} / ${sk.tools.count ?? 0}`, + ); + if (sk.agents) + html += probeItem( + t("probes.agents"), + `${sk.agents.loaded ?? 0} / ${sk.agents.count ?? 0}`, + ); + if (sk.anthropic_skills) + html += probeItem( + "Anthropic Skills", + `${sk.anthropic_skills.loaded ?? 0} / ${sk.anthropic_skills.count ?? 0}`, + ); html += `
`; // Show active skills (ones with calls > 0) const activeItems = []; @@ -165,7 +220,8 @@ html += ``; html += `${item.calls} calls`; html += `${item.success} ok`; - if (item.failure > 0) html += `${item.failure} fail`; + if (item.failure > 0) + html += `${item.failure} fail`; html += `
`; } if (activeItems.length > 10) { @@ -204,21 +260,31 @@ const results = data.results || []; html += `
`; for (const r of results) { - const statusCls = r.status === "ok" ? "ok" : r.status === "skipped" ? "skipped" : "error"; + const statusCls = + r.status === "ok" + ? "ok" + : r.status === "skipped" + ? "skipped" + : "error"; html += `
`; html += `
`; html += `
${escapeHtml((r.name || "").replace(/_/g, " "))}
`; html += `
`; - if (r.model_name) html += `${t("probes.model")}: ${escapeHtml(r.model_name)}`; - if (r.url) html += `URL: ${escapeHtml(r.url)}`; - if (r.host) html += `Host: ${escapeHtml(r.host)}${r.port ? ":" + r.port : ""}`; + if (r.model_name) + html += `${t("probes.model")}: ${escapeHtml(r.model_name)}`; + if (r.url) + html += `URL: ${escapeHtml(r.url)}`; + if (r.host) + html += `Host: ${escapeHtml(r.host)}${r.port ? ":" + r.port : ""}`; if (r.http_status) html += `HTTP ${r.http_status}`; - if (r.error) html += `${escapeHtml(r.error)}`; + if (r.error) + html += `${escapeHtml(r.error)}`; if (r.reason) html += `${escapeHtml(r.reason)}`; html += `
`; html += `
`; html += probeStatusBadge(r.status); - if (r.latency_ms !== undefined) html += `${r.latency_ms} ms`; + if (r.latency_ms !== undefined) + html += `${r.latency_ms} ms`; html += `
`; } html += `
`; @@ -256,7 +322,10 @@ const raw = String(text || "").trim(); if (!raw) return escapeHtml(text || ""); - if ((raw.startsWith("{") && raw.endsWith("}")) || (raw.startsWith("[") && raw.endsWith("]"))) { + if ( + (raw.startsWith("{") && raw.endsWith("}")) || + (raw.startsWith("[") && raw.endsWith("]")) + ) { try { const parsed = JSON.parse(raw); return `
${escapeHtml(JSON.stringify(parsed, null, 2))}
`; @@ -271,7 +340,9 @@ const flushList = () => { if (!listItems.length) return; - blocks.push(`
    ${listItems.join("")}
`); + blocks.push( + `
    ${listItems.join("")}
`, + ); listItems = []; }; @@ -285,7 +356,9 @@ const heading = textLine.match(/^#{1,3}\s+(.+)$/); if (heading) { flushList(); - blocks.push(`
${escapeHtml(heading[1])}
`); + blocks.push( + `
${escapeHtml(heading[1])}
`, + ); continue; } @@ -299,23 +372,30 @@ if (kv) { flushList(); blocks.push( - `
${escapeHtml(kv[1])}${escapeHtml(kv[2])}
` + `
${escapeHtml(kv[1])}${escapeHtml(kv[2])}
`, ); continue; } flushList(); - blocks.push(`

${escapeHtml(textLine)}

`); + blocks.push( + `

${escapeHtml(textLine)}

`, + ); } flushList(); - return blocks.join("") || `

${escapeHtml(raw)}

`; + return ( + blocks.join("") || + `

${escapeHtml(raw)}

` + ); } function appendChatMessage(role, content) { const log = get("runtimeChatLog"); if (!log) return; const isBot = role !== "user"; - const contentClass = isBot ? "runtime-chat-content markdown" : "runtime-chat-content"; + const contentClass = isBot + ? "runtime-chat-content markdown" + : "runtime-chat-content"; const item = document.createElement("div"); item.className = `runtime-chat-item ${role}`; item.innerHTML = `
${role === "user" ? "You" : "AI"}
${renderChatContent(content, isBot)}
`; @@ -354,7 +434,11 @@ if (raw.startsWith("/") || /^[A-Za-z]:[\\/]/.test(raw)) { return `/api/runtime/chat/image?path=${encodeURIComponent(raw)}`; } - if (raw.startsWith("http://") || raw.startsWith("https://") || raw.startsWith("data:image/")) { + if ( + raw.startsWith("http://") || + raw.startsWith("https://") || + raw.startsWith("data:image/") + ) { return raw; } return ""; @@ -374,14 +458,16 @@ const size = formatFileSize(attrs.size); if (!fileId) return `[file]`; const href = `/api/runtime/chat/file?id=${encodeURIComponent(fileId)}`; - return `
` - + `
📄
` - + `
` - + `
${name}
` - + (size ? `
${size}
` : "") - + `
` - + `${t("runtime.download") || "Download"}` - + `
`; + return ( + `
` + + `
📄
` + + `
` + + `
${name}
` + + (size ? `
${size}
` : "") + + `
` + + `${t("runtime.download") || "Download"}` + + `
` + ); } function renderChatContent(content, useMarkdown) { @@ -390,7 +476,7 @@ // Extract CQ file codes into placeholders const filePattern = /\[CQ:file,([^\]]+)\]/g; const filePlaceholders = []; - let step1 = text.replace(filePattern, (match, attrStr) => { + const step1 = text.replace(filePattern, (match, attrStr) => { const attrs = parseCqAttributes(attrStr); const idx = filePlaceholders.length; filePlaceholders.push(renderFileCard(attrs)); @@ -405,7 +491,9 @@ const src = resolveCqImageSource(attrs); if (src) { const idx = images.length; - images.push(`image`); + images.push( + `image`, + ); return `CQIMGPH${idx}CQIMGPH`; } return match; @@ -424,12 +512,21 @@ // Restore placeholders for (let i = 0; i < images.length; i++) { - html = html.replace(new RegExp(`CQIMGPH${i}CQIMGPH`, "g"), images[i]); + html = html.replace( + new RegExp(`CQIMGPH${i}CQIMGPH`, "g"), + images[i], + ); } for (let i = 0; i < filePlaceholders.length; i++) { // marked may wrap placeholder in

, strip it for block-level card - html = html.replace(new RegExp(`

\\s*CQFILEPH${i}CQFILEPH\\s*

`, "g"), filePlaceholders[i]); - html = html.replace(new RegExp(`CQFILEPH${i}CQFILEPH`, "g"), filePlaceholders[i]); + html = html.replace( + new RegExp(`

\\s*CQFILEPH${i}CQFILEPH\\s*

`, "g"), + filePlaceholders[i], + ); + html = html.replace( + new RegExp(`CQFILEPH${i}CQFILEPH`, "g"), + filePlaceholders[i], + ); } return html || escapeHtml(text); @@ -462,7 +559,8 @@ } function buildRequestError(res, payload) { - const fallback = `${res.status} ${res.statusText || "Request failed"}`.trim(); + const fallback = + `${res.status} ${res.statusText || "Request failed"}`.trim(); if (!payload || typeof payload !== "object") return fallback; const base = payload.error ? String(payload.error) : fallback; return payload.detail ? `${base}: ${payload.detail}` : base; @@ -472,11 +570,12 @@ const text = String(message || "").trim(); if (!text) return text; const normalized = text.toLowerCase(); - const unreachable = normalized.includes("runtime api unreachable") - || normalized.includes("failed to fetch") - || normalized.includes("networkerror") - || normalized.includes(" 502 ") - || normalized.startsWith("502 "); + const unreachable = + normalized.includes("runtime api unreachable") || + normalized.includes("failed to fetch") || + normalized.includes("networkerror") || + normalized.includes(" 502 ") || + normalized.startsWith("502 "); if (!unreachable) return text; const hint = t("runtime.api_start_hint"); if (!hint || text.includes(hint)) return text; @@ -536,10 +635,12 @@ const container = get("runtimeMemoryList"); const meta = get("runtimeMemoryMeta"); if (!container || !meta) return; - const items = payload && Array.isArray(payload.items) ? payload.items : []; - const queryInfo = payload && payload.query && typeof payload.query === "object" - ? payload.query - : {}; + const items = + payload && Array.isArray(payload.items) ? payload.items : []; + const queryInfo = + payload && payload.query && typeof payload.query === "object" + ? payload.query + : {}; if (!Array.isArray(items) || items.length === 0) { meta.textContent = i18nFormat("runtime.total", { count: 0 }); container.innerHTML = `
${t("runtime.empty")}
`; @@ -579,7 +680,8 @@ const meta = get(metaId); const list = get(listId); if (!meta || !list) return; - const items = payload && Array.isArray(payload.items) ? payload.items : []; + const items = + payload && Array.isArray(payload.items) ? payload.items : []; const count = Number.isFinite(Number(payload && payload.count)) ? Number(payload.count) : items.length; @@ -600,36 +702,55 @@ "request_id", ]; - list.innerHTML = items.map((item, index) => { - const doc = escapeHtml(String((item && item.document) || "").trim()); - const md = item && typeof item.metadata === "object" && item.metadata - ? item.metadata - : {}; - const dist = formatNumeric(item && item.distance); - const rerank = formatNumeric(item && item.rerank_score); - const timestamp = escapeHtml(String(md.timestamp_local || "").trim()); - const headLabel = timestamp || `#${index + 1}`; - const tags = []; - if (dist) tags.push(`distance ${dist}`); - if (rerank) tags.push(`rerank ${rerank}`); - - const metaRows = preferredMetaKeys - .filter((key) => md[key] !== undefined && md[key] !== null && String(md[key]).trim() !== "") - .map((key) => { - const raw = md[key]; - const text = (raw && typeof raw === "object") - ? JSON.stringify(raw) - : String(raw); - return `${escapeHtml(key)}${escapeHtml(text)}`; - }) - .join(""); - - return `
+ list.innerHTML = items + .map((item, index) => { + const doc = escapeHtml( + String((item && item.document) || "").trim(), + ); + const md = + item && typeof item.metadata === "object" && item.metadata + ? item.metadata + : {}; + const dist = formatNumeric(item && item.distance); + const rerank = formatNumeric(item && item.rerank_score); + const timestamp = escapeHtml( + String(md.timestamp_local || "").trim(), + ); + const headLabel = timestamp || `#${index + 1}`; + const tags = []; + if (dist) + tags.push( + `distance ${dist}`, + ); + if (rerank) + tags.push( + `rerank ${rerank}`, + ); + + const metaRows = preferredMetaKeys + .filter( + (key) => + md[key] !== undefined && + md[key] !== null && + String(md[key]).trim() !== "", + ) + .map((key) => { + const raw = md[key]; + const text = + raw && typeof raw === "object" + ? JSON.stringify(raw) + : String(raw); + return `${escapeHtml(key)}${escapeHtml(text)}`; + }) + .join(""); + + return `
${headLabel}
${tags.join("")}
${doc || "--"}
${metaRows ? `
${metaRows}
` : ""}
`; - }).join(""); + }) + .join(""); } function renderProfileDetail(payload) { @@ -637,7 +758,11 @@ const container = get("runtimeProfileResult"); if (!meta || !container) return; if (!payload || typeof payload !== "object") { - setListMessage("runtimeProfileMeta", "runtimeProfileResult", t("runtime.empty")); + setListMessage( + "runtimeProfileMeta", + "runtimeProfileResult", + t("runtime.empty"), + ); return; } @@ -655,6 +780,96 @@
`; } + function renderBootstrapProbe(data) { + const el = get("managementBootstrapProbe"); + if (!el) return; + if (!data || data.error) { + el.innerHTML = `
${escapeHtml(data?.error || "--")}
`; + return; + } + + const configExists = !!data.config_exists; + const configValid = + data.config_valid === undefined ? null : !!data.config_valid; + const usingDefaultPassword = !!data.using_default_password; + const runtimeEnabled = !!data.runtime_enabled; + const runtimeReachable = + data.runtime_reachable === undefined + ? null + : !!data.runtime_reachable; + const authMode = + data.auth_mode || (state.authAccessToken ? "token" : "cookie"); + const advice = Array.isArray(data.advice) ? data.advice : []; + + const configStatus = configExists ? "ok" : "error"; + const validationStatus = + configValid === null ? "skipped" : configValid ? "ok" : "error"; + const authStatus = usingDefaultPassword ? "error" : "ok"; + const runtimeStatus = + runtimeReachable === null + ? runtimeEnabled + ? "skipped" + : "error" + : runtimeReachable + ? "ok" + : runtimeEnabled + ? "error" + : "skipped"; + + let html = `
${t("probes.section_bootstrap")}
`; + html += `
`; + html += probeItem( + t("probes.bootstrap_config"), + probeStatusBadge(configStatus), + ); + html += probeItem( + t("probes.bootstrap_validation"), + probeStatusBadge(validationStatus), + ); + html += probeItem( + t("probes.bootstrap_auth"), + `${probeStatusBadge(authStatus)} ${escapeHtml(String(authMode))}`, + ); + html += probeItem( + t("probes.bootstrap_runtime"), + probeStatusBadge(runtimeStatus), + ); + html += `
`; + + const summary = []; + if (configExists) summary.push(t("probes.bootstrap_config_exists")); + if (!configExists) summary.push(t("probes.bootstrap_config_missing")); + if (configValid === false && data.validation_error) + summary.push(String(data.validation_error)); + if (usingDefaultPassword) summary.push(t("auth.change_required")); + if (runtimeEnabled && runtimeReachable === false) + summary.push(t("probes.bootstrap_runtime_pending")); + if (!summary.length) summary.push(t("probes.bootstrap_ready")); + + html += `
${summary + .concat(advice) + .map( + (item) => + `
${escapeHtml(item)}
`, + ) + .join("")}
`; + html += `
`; + el.innerHTML = html; + } + + function buildBootstrapFallback(meta, errorMessage = "") { + return { + config_exists: !!state.configExists, + config_valid: null, + using_default_password: !!state.usingDefaultPassword, + auth_mode: state.authAccessToken ? "token" : "cookie", + runtime_enabled: !!(meta && meta.enabled), + runtime_reachable: false, + validation_error: "", + advice: errorMessage ? [String(errorMessage)] : [], + }; + } + function setProbeUnavailable(message) { const msg = String(message || RUNTIME_DISABLED_ERROR); renderInternalProbe({ error: msg }); @@ -670,9 +885,10 @@ } async function fetchRuntimeMeta() { - const res = await api("/api/runtime/meta"); - const data = await res.json(); - return data; + return fetchJsonOrThrow([ + "/api/v1/management/runtime/meta", + "/api/runtime/meta", + ]); } async function ensureRuntimeEnabled() { @@ -686,17 +902,38 @@ } async function fetchInternalProbe() { - const res = await api("/api/runtime/probes/internal"); - const data = await res.json(); + const data = await fetchJsonOrThrow([ + "/api/v1/management/runtime/probes/internal", + "/api/runtime/probes/internal", + ]); renderInternalProbe(data); } async function fetchExternalProbe() { - const res = await api("/api/runtime/probes/external"); - const data = await res.json(); + const data = await fetchJsonOrThrow([ + "/api/v1/management/runtime/probes/external", + "/api/runtime/probes/external", + ]); renderExternalProbe(data); } + async function fetchBootstrapProbe() { + try { + const data = await fetchJsonOrThrow([ + "/api/v1/management/probes/bootstrap", + "/api/v1/management/probes/capabilities", + ]); + renderBootstrapProbe(data); + } catch (error) { + const meta = await fetchRuntimeMeta().catch(() => ({ + enabled: false, + })); + renderBootstrapProbe( + buildBootstrapFallback(meta, error.message || error), + ); + } + } + async function searchMemory() { if (!(await ensureRuntimeEnabled())) { setMemoryUnavailable(t("runtime.disabled")); @@ -711,63 +948,135 @@ appendPositiveIntParam(params, "top_k", topK); appendQueryParam(params, "time_from", timeFrom); appendQueryParam(params, "time_to", timeTo); - const data = await fetchJsonOrThrow(`/api/runtime/memory?${params.toString()}`); + const data = await fetchJsonOrThrow( + `/api/runtime/memory?${params.toString()}`, + ); renderMemoryItems(data); } async function searchEvents() { if (!(await ensureRuntimeEnabled())) { - setListMessage("runtimeEventsMeta", "runtimeEventsResult", t("runtime.disabled")); + setListMessage( + "runtimeEventsMeta", + "runtimeEventsResult", + t("runtime.disabled"), + ); return; } const query = readInputValue("runtimeEventsQuery"); if (!query) { - setListMessage("runtimeEventsMeta", "runtimeEventsResult", "q is required"); + setListMessage( + "runtimeEventsMeta", + "runtimeEventsResult", + "q is required", + ); return; } const params = new URLSearchParams(); appendQueryParam(params, "q", query); - appendPositiveIntParam(params, "top_k", readInputValue("runtimeEventsTopK")); - appendQueryParam(params, "request_type", readInputValue("runtimeEventsRequestType")); - appendQueryParam(params, "target_user_id", readInputValue("runtimeEventsTargetUserId")); - appendQueryParam(params, "target_group_id", readInputValue("runtimeEventsTargetGroupId")); - appendQueryParam(params, "sender_id", readInputValue("runtimeEventsSenderId")); - appendQueryParam(params, "time_from", readInputValue("runtimeEventsTimeFrom")); - appendQueryParam(params, "time_to", readInputValue("runtimeEventsTimeTo")); - const data = await fetchJsonOrThrow(`/api/runtime/cognitive/events?${params.toString()}`); + appendPositiveIntParam( + params, + "top_k", + readInputValue("runtimeEventsTopK"), + ); + appendQueryParam( + params, + "request_type", + readInputValue("runtimeEventsRequestType"), + ); + appendQueryParam( + params, + "target_user_id", + readInputValue("runtimeEventsTargetUserId"), + ); + appendQueryParam( + params, + "target_group_id", + readInputValue("runtimeEventsTargetGroupId"), + ); + appendQueryParam( + params, + "sender_id", + readInputValue("runtimeEventsSenderId"), + ); + appendQueryParam( + params, + "time_from", + readInputValue("runtimeEventsTimeFrom"), + ); + appendQueryParam( + params, + "time_to", + readInputValue("runtimeEventsTimeTo"), + ); + const data = await fetchJsonOrThrow( + `/api/runtime/cognitive/events?${params.toString()}`, + ); renderCognitiveItems("runtimeEventsMeta", "runtimeEventsResult", data); } async function searchProfiles() { if (!(await ensureRuntimeEnabled())) { - setListMessage("runtimeProfilesMeta", "runtimeProfilesResult", t("runtime.disabled")); + setListMessage( + "runtimeProfilesMeta", + "runtimeProfilesResult", + t("runtime.disabled"), + ); return; } const query = readInputValue("runtimeProfilesQuery"); if (!query) { - setListMessage("runtimeProfilesMeta", "runtimeProfilesResult", "q is required"); + setListMessage( + "runtimeProfilesMeta", + "runtimeProfilesResult", + "q is required", + ); return; } const params = new URLSearchParams(); appendQueryParam(params, "q", query); - appendPositiveIntParam(params, "top_k", readInputValue("runtimeProfilesTopK")); - appendQueryParam(params, "entity_type", readInputValue("runtimeProfilesEntityType")); - const data = await fetchJsonOrThrow(`/api/runtime/cognitive/profiles?${params.toString()}`); - renderCognitiveItems("runtimeProfilesMeta", "runtimeProfilesResult", data); + appendPositiveIntParam( + params, + "top_k", + readInputValue("runtimeProfilesTopK"), + ); + appendQueryParam( + params, + "entity_type", + readInputValue("runtimeProfilesEntityType"), + ); + const data = await fetchJsonOrThrow( + `/api/runtime/cognitive/profiles?${params.toString()}`, + ); + renderCognitiveItems( + "runtimeProfilesMeta", + "runtimeProfilesResult", + data, + ); } async function fetchProfileByEntity() { if (!(await ensureRuntimeEnabled())) { - setListMessage("runtimeProfileMeta", "runtimeProfileResult", t("runtime.disabled")); + setListMessage( + "runtimeProfileMeta", + "runtimeProfileResult", + t("runtime.disabled"), + ); return; } const entityType = readInputValue("runtimeProfileEntityType"); const entityId = readInputValue("runtimeProfileEntityId"); if (!entityType || !entityId) { - setListMessage("runtimeProfileMeta", "runtimeProfileResult", "entity_type/entity_id are required"); + setListMessage( + "runtimeProfileMeta", + "runtimeProfileResult", + "entity_type/entity_id are required", + ); return; } - const data = await fetchJsonOrThrow(`/api/runtime/cognitive/profile/${encodeURIComponent(entityType)}/${encodeURIComponent(entityId)}`); + const data = await fetchJsonOrThrow( + `/api/runtime/cognitive/profile/${encodeURIComponent(entityType)}/${encodeURIComponent(entityId)}`, + ); renderProfileDetail(data); } @@ -782,7 +1091,7 @@ showToast( `${t("runtime.failed")}: ${appendRuntimeApiHint(error.message || error)}`, "error", - 5000 + 5000, ); } finally { setButtonLoading(button, false); @@ -829,7 +1138,9 @@ body: JSON.stringify({ message, stream: true }), }); - const contentType = (res.headers.get("Content-Type") || "").toLowerCase(); + const contentType = ( + res.headers.get("Content-Type") || "" + ).toLowerCase(); if (contentType.includes("text/event-stream") && res.body) { let replied = false; let streamError = ""; @@ -838,8 +1149,8 @@ if (event === "message") { const content = String( payload && (payload.content ?? payload.message) - ? payload.content ?? payload.message - : "" + ? (payload.content ?? payload.message) + : "", ).trim(); if (!content) return; appendChatMessage("bot", content); @@ -850,7 +1161,7 @@ streamError = String( payload && (payload.error || payload.message) ? payload.error || payload.message - : "stream error" + : "stream error", ); return; } @@ -877,9 +1188,12 @@ throw new Error(buildRequestError(res, data)); } - const messages = data && Array.isArray(data.messages) ? data.messages : []; + const messages = + data && Array.isArray(data.messages) ? data.messages : []; if (messages.length > 0) { - messages.forEach((msg) => appendChatMessage("bot", String(msg || ""))); + messages.forEach((msg) => + appendChatMessage("bot", String(msg || "")), + ); } else if (data && data.reply) { appendChatMessage("bot", String(data.reply)); } else { @@ -890,7 +1204,7 @@ showToast( `${t("runtime.failed")}: ${appendRuntimeApiHint(error.message || error)}`, "error", - 5000 + 5000, ); } finally { runtimeState.chatBusy = false; @@ -906,7 +1220,8 @@ try { for (const file of files) { - if (!file || !String(file.type || "").startsWith("image/")) continue; + if (!file || !String(file.type || "").startsWith("image/")) + continue; const dataUrl = await readFileAsDataUrl(file); const base64 = String(dataUrl).split(",", 2)[1] || ""; if (!base64) continue; @@ -921,7 +1236,7 @@ showToast( `${t("runtime.failed")}: ${appendRuntimeApiHint(error.message || error)}`, "error", - 5000 + 5000, ); } finally { if (input) input.value = ""; @@ -930,19 +1245,22 @@ async function refreshProbes() { try { + await fetchBootstrapProbe(); if (!(await ensureRuntimeEnabled())) { setProbeUnavailable(t("runtime.disabled")); runtimeState.probesLoaded = true; + runtimeState.bootstrapLoaded = true; return; } await Promise.all([fetchInternalProbe(), fetchExternalProbe()]); runtimeState.probesLoaded = true; + runtimeState.bootstrapLoaded = true; } catch (error) { showToast( `${t("runtime.failed")}: ${appendRuntimeApiHint(error.message || error)}`, "error", - 5000 + 5000, ); } } @@ -960,7 +1278,7 @@ showToast( `${t("runtime.failed")}: ${appendRuntimeApiHint(error.message || error)}`, "error", - 5000 + 5000, ); } } @@ -988,12 +1306,25 @@ if (probeRefresh) probeRefresh.addEventListener("click", refreshProbes); const memoryRefresh = get("btnMemoryRefresh"); - if (memoryRefresh) memoryRefresh.addEventListener("click", refreshMemory); - - const runMemorySearch = () => runQueryAction("memory", "btnRuntimeMemorySearch", searchMemory); - const runEventsSearch = () => runQueryAction("events", "btnRuntimeEventsSearch", searchEvents); - const runProfilesSearch = () => runQueryAction("profiles", "btnRuntimeProfilesSearch", searchProfiles); - const runProfileGet = () => runQueryAction("profileGet", "btnRuntimeProfileGet", fetchProfileByEntity); + if (memoryRefresh) + memoryRefresh.addEventListener("click", refreshMemory); + + const runMemorySearch = () => + runQueryAction("memory", "btnRuntimeMemorySearch", searchMemory); + const runEventsSearch = () => + runQueryAction("events", "btnRuntimeEventsSearch", searchEvents); + const runProfilesSearch = () => + runQueryAction( + "profiles", + "btnRuntimeProfilesSearch", + searchProfiles, + ); + const runProfileGet = () => + runQueryAction( + "profileGet", + "btnRuntimeProfileGet", + fetchProfileByEntity, + ); const memoryBtn = get("btnRuntimeMemorySearch"); if (memoryBtn) memoryBtn.addEventListener("click", runMemorySearch); @@ -1004,7 +1335,7 @@ "runtimeMemoryTimeFrom", "runtimeMemoryTimeTo", ], - runMemorySearch + runMemorySearch, ); const eventsBtn = get("btnRuntimeEventsSearch"); @@ -1019,18 +1350,24 @@ "runtimeEventsTimeFrom", "runtimeEventsTimeTo", ], - runEventsSearch + runEventsSearch, ); const profilesBtn = get("btnRuntimeProfilesSearch"); - if (profilesBtn) profilesBtn.addEventListener("click", runProfilesSearch); + if (profilesBtn) + profilesBtn.addEventListener("click", runProfilesSearch); bindEnterMany( - ["runtimeProfilesQuery", "runtimeProfilesTopK", "runtimeProfilesEntityType"], - runProfilesSearch + [ + "runtimeProfilesQuery", + "runtimeProfilesTopK", + "runtimeProfilesEntityType", + ], + runProfilesSearch, ); const profileGetBtn = get("btnRuntimeProfileGet"); - if (profileGetBtn) profileGetBtn.addEventListener("click", runProfileGet); + if (profileGetBtn) + profileGetBtn.addEventListener("click", runProfileGet); bindEnter("runtimeProfileEntityType", runProfileGet); bindEnter("runtimeProfileEntityId", runProfileGet); @@ -1061,7 +1398,10 @@ function startProbeTimer() { stopProbeTimer(); - runtimeState.probeTimer = setInterval(refreshProbes, PROBE_REFRESH_INTERVAL); + runtimeState.probeTimer = setInterval( + refreshProbes, + PROBE_REFRESH_INTERVAL, + ); } function stopProbeTimer() { @@ -1092,7 +1432,7 @@ showToast( `${t("runtime.failed")}: ${appendRuntimeApiHint(error.message || error)}`, "error", - 5000 + 5000, ); }); } diff --git a/src/Undefined/webui/static/js/state.js b/src/Undefined/webui/static/js/state.js index c4001e39..e5b04c0c 100644 --- a/src/Undefined/webui/static/js/state.js +++ b/src/Undefined/webui/static/js/state.js @@ -10,16 +10,173 @@ function readJsonScript(id, fallback) { } } +function normalizeBootstrapAuthPayload(payload) { + if (!payload || typeof payload !== "object") return null; + const source = + payload.tokens && typeof payload.tokens === "object" + ? payload.tokens + : payload; + const accessToken = String( + source.access_token || source.accessToken || "", + ).trim(); + const refreshToken = String( + source.refresh_token || source.refreshToken || "", + ).trim(); + const accessTokenExpiresAt = + Number.parseInt( + String( + source.access_token_expires_at || + source.accessTokenExpiresAt || + "0", + ), + 10, + ) || 0; + if (!accessToken) return null; + return { accessToken, refreshToken, accessTokenExpiresAt }; +} + +function decodeBase64UrlUtf8(value) { + const normalized = String(value || "") + .replace(/-/g, "+") + .replace(/_/g, "/"); + const padded = + normalized + "=".repeat((4 - (normalized.length % 4 || 4)) % 4); + const binary = window.atob(padded); + const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); + return new TextDecoder().decode(bytes); +} + +function readBootstrapAuthPayload() { + const hash = String(window.location.hash || "").replace(/^#/, ""); + if (!hash) return null; + const params = new URLSearchParams(hash); + const raw = params.get("auth"); + if (!raw) return null; + try { + return normalizeBootstrapAuthPayload( + JSON.parse(decodeBase64UrlUtf8(raw)), + ); + } catch (_error) { + return null; + } +} + +function readSessionStorage(key) { + try { + return window.sessionStorage.getItem(key) || ""; + } catch (_error) { + return ""; + } +} + +function readStorage(key) { + try { + return window.localStorage.getItem(key) || ""; + } catch (_error) { + return ""; + } +} + +function removeStorage(key) { + try { + window.localStorage.removeItem(key); + } catch (_error) { + // ignore storage failures in hardened browsers/private mode + } +} + +function writeSessionStorage(key, value) { + try { + if (value === null || value === undefined || value === "") { + window.sessionStorage.removeItem(key); + return true; + } + window.sessionStorage.setItem(key, String(value)); + return true; + } catch (_error) { + return false; + } +} + +function readAuthStorage(key) { + const sessionValue = readSessionStorage(key); + if (sessionValue) return sessionValue; + const legacyValue = readStorage(key); + if (!legacyValue) return ""; + if (writeSessionStorage(key, legacyValue)) { + removeStorage(key); + } + return legacyValue; +} + +function writeAuthStorage(key, value) { + if (writeSessionStorage(key, value)) { + removeStorage(key); + } +} + +function persistBootstrapAuthPayload(payload) { + if (!payload) return; + if (payload.accessToken) { + writeAuthStorage("undefined_auth_access_token", payload.accessToken); + } + if (payload.refreshToken) { + writeAuthStorage("undefined_auth_refresh_token", payload.refreshToken); + } + if (payload.accessTokenExpiresAt) { + writeAuthStorage( + "undefined_auth_access_expires_at", + String(payload.accessTokenExpiresAt), + ); + } +} + +function clearBootstrapAuthHash() { + if (!String(window.location.hash || "").includes("auth=")) return; + const nextUrl = `${window.location.pathname}${window.location.search}`; + window.history.replaceState(null, "", nextUrl); +} + +const bootstrapAuth = readBootstrapAuthPayload(); +if (bootstrapAuth) { + persistBootstrapAuthPayload(bootstrapAuth); + clearBootstrapAuthHash(); +} + const initialState = readJsonScript("initial-state", {}); const initialView = readJsonScript("initial-view", "landing"); const state = { - lang: (initialState && initialState.lang) || getCookie("undefined_lang") || "zh", - theme: "light", + lang: + (initialState && initialState.lang) || + getCookie("undefined_lang") || + "zh", + theme: (initialState && initialState.theme) || "light", authenticated: false, - usingDefaultPassword: !!(initialState && initialState.using_default_password), + launcherMode: !!(initialState && initialState.launcher_mode), + returnTo: (initialState && initialState.return_to) || "", + authAccessToken: + (bootstrapAuth && bootstrapAuth.accessToken) || + readAuthStorage("undefined_auth_access_token"), + authRefreshToken: + (bootstrapAuth && bootstrapAuth.refreshToken) || + readAuthStorage("undefined_auth_refresh_token"), + authAccessTokenExpiresAt: + Number( + (bootstrapAuth && bootstrapAuth.accessTokenExpiresAt) || + Number.parseInt( + readAuthStorage("undefined_auth_access_expires_at") || "0", + 10, + ) || + 0, + ) || 0, + authRefreshTimer: null, + usingDefaultPassword: !!( + initialState && initialState.using_default_password + ), configExists: !!(initialState && initialState.config_exists), - tab: "overview", + capabilities: null, + tab: (initialState && initialState.initial_tab) || "overview", view: initialView || "landing", config: {}, comments: {}, @@ -63,10 +220,16 @@ const THEME_COLORS = { dark: "#0f1112", }; -const LOG_LEVELS = window.LogsController ? window.LogsController.LOG_LEVELS : ["all"]; +const LOG_LEVELS = window.LogsController + ? window.LogsController.LOG_LEVELS + : ["all"]; -function get(id) { return document.getElementById(id); } -function t(key) { return I18N[state.lang][key] || key; } +function get(id) { + return document.getElementById(id); +} +function t(key) { + return I18N[state.lang][key] || key; +} function escapeHtml(value) { return String(value) @@ -91,12 +254,45 @@ function setButtonLoading(button, loading) { function getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); - if (parts.length === 2) return parts.pop().split(';').shift(); + if (parts.length === 2) return parts.pop().split(";").shift(); } function setCookie(name, value, days = 30) { const d = new Date(); - d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)); + d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000); const expires = `expires=${d.toUTCString()}`; document.cookie = `${name}=${value};${expires};path=/;SameSite=Lax`; } + +function clearStoredAuthTokens() { + state.authAccessToken = ""; + state.authRefreshToken = ""; + state.authAccessTokenExpiresAt = 0; + writeAuthStorage("undefined_auth_access_token", ""); + writeAuthStorage("undefined_auth_refresh_token", ""); + writeAuthStorage("undefined_auth_access_expires_at", ""); + if (state.authRefreshTimer) { + clearTimeout(state.authRefreshTimer); + state.authRefreshTimer = null; + } +} + +function storeAuthTokens({ + accessToken = "", + refreshToken = "", + accessTokenExpiresAt = 0, +} = {}) { + state.authAccessToken = accessToken || ""; + state.authRefreshToken = refreshToken || ""; + state.authAccessTokenExpiresAt = Number.isFinite( + Number(accessTokenExpiresAt), + ) + ? Number(accessTokenExpiresAt) + : 0; + writeAuthStorage("undefined_auth_access_token", state.authAccessToken); + writeAuthStorage("undefined_auth_refresh_token", state.authRefreshToken); + writeAuthStorage( + "undefined_auth_access_expires_at", + state.authAccessTokenExpiresAt || "", + ); +} diff --git a/src/Undefined/webui/static/js/ui.js b/src/Undefined/webui/static/js/ui.js index 17ab62f6..c42a4dd5 100644 --- a/src/Undefined/webui/static/js/ui.js +++ b/src/Undefined/webui/static/js/ui.js @@ -1,8 +1,8 @@ function updateI18N() { - document.querySelectorAll("[data-i18n]").forEach(el => { + document.querySelectorAll("[data-i18n]").forEach((el) => { el.innerText = t(el.getAttribute("data-i18n")); }); - document.querySelectorAll("[data-i18n-placeholder]").forEach(el => { + document.querySelectorAll("[data-i18n-placeholder]").forEach((el) => { el.placeholder = t(el.getAttribute("data-i18n-placeholder")); }); updateToggleLabels(); @@ -22,11 +22,12 @@ function updateI18N() { function updateToggleLabels() { const langLabel = state.lang === "zh" ? "English" : "中文"; - document.querySelectorAll('[data-action="toggle-lang"]').forEach(btn => { + document.querySelectorAll('[data-action="toggle-lang"]').forEach((btn) => { btn.innerText = langLabel; }); - const themeLabel = state.theme === "dark" ? t("theme.dark") : t("theme.light"); - document.querySelectorAll('[data-action="toggle-theme"]').forEach(btn => { + const themeLabel = + state.theme === "dark" ? t("theme.dark") : t("theme.light"); + document.querySelectorAll('[data-action="toggle-theme"]').forEach((btn) => { btn.innerText = themeLabel; }); } @@ -44,7 +45,7 @@ function updateLogFilterLabels() { if (!select) return; const current = select.value || state.logLevel; while (select.firstChild) select.removeChild(select.firstChild); - LOG_LEVELS.forEach(level => { + LOG_LEVELS.forEach((level) => { const option = document.createElement("option"); option.value = level; option.textContent = t(`logs.level.${level}`); @@ -62,7 +63,7 @@ function updateLogPauseLabel() { } function updateLogTabs() { - document.querySelectorAll(".log-tab").forEach(tab => { + document.querySelectorAll(".log-tab").forEach((tab) => { tab.classList.toggle("active", tab.dataset.logType === state.logType); }); } @@ -73,16 +74,19 @@ function updateLogFileSelect() { const files = state.logFiles[state.logType] || []; select.disabled = files.length === 0; while (select.firstChild) select.removeChild(select.firstChild); - files.forEach(file => { + files.forEach((file) => { const option = document.createElement("option"); option.value = file.name; - let label = file.current ? t("logs.file.current") : t("logs.file.history"); + let label = file.current + ? t("logs.file.current") + : t("logs.file.history"); if (state.logType === "all") label = t("logs.file.other"); option.textContent = `${file.name} (${label})`; select.appendChild(option); }); - if (!state.logFile || !files.find(file => file.name === state.logFile)) { - state.logFile = state.logFileCurrent || (files[0] && files[0].name) || ""; + if (!state.logFile || !files.find((file) => file.name === state.logFile)) { + state.logFile = + state.logFileCurrent || (files[0] && files[0].name) || ""; } select.value = state.logFile; updateLogStreamEligibility(); @@ -93,7 +97,8 @@ function updateLogStreamEligibility() { state.logStreamEnabled = false; return; } - state.logStreamEnabled = !state.logFile || state.logFile === state.logFileCurrent; + state.logStreamEnabled = + !state.logFile || state.logFile === state.logFileCurrent; } function updateConfigStateLabel() { @@ -124,22 +129,32 @@ function updateAuthPanels() { const landingLogin = get("landingLoginBox"); const landingReset = get("landingPasswordResetBox"); - if (landingLogin) landingLogin.style.display = showLanding && !usingDefault ? "block" : "none"; - if (landingReset) landingReset.style.display = showLanding && usingDefault ? "block" : "none"; + if (landingLogin) + landingLogin.style.display = + showLanding && !usingDefault ? "block" : "none"; + if (landingReset) + landingReset.style.display = + showLanding && usingDefault ? "block" : "none"; const appLogin = get("appLoginBox"); const appReset = get("appPasswordResetBox"); - if (appLogin) appLogin.style.display = showAppLogin && !usingDefault ? "block" : "none"; - if (appReset) appReset.style.display = showAppLogin && usingDefault ? "block" : "none"; + if (appLogin) + appLogin.style.display = + showAppLogin && !usingDefault ? "block" : "none"; + if (appReset) + appReset.style.display = + showAppLogin && usingDefault ? "block" : "none"; } function updateSectionToggleLabels() { - document.querySelectorAll(".config-card").forEach(card => { + document.querySelectorAll(".config-card").forEach((card) => { const section = card.dataset.section; const toggle = card.querySelector(".config-card-actions button"); if (!toggle || !section) return; const collapsed = !!state.configCollapsed[section]; - toggle.innerText = collapsed ? t("config.expand_section") : t("config.collapse_section"); + toggle.innerText = collapsed + ? t("config.expand_section") + : t("config.collapse_section"); toggle.setAttribute("aria-expanded", collapsed ? "false" : "true"); }); } @@ -181,10 +196,15 @@ function setConfigState(mode) { grid.style.display = "block"; return; } - const keyMap = { loading: "config.loading", error: "config.error", empty: "config.no_results" }; + const keyMap = { + loading: "config.loading", + error: "config.error", + empty: "config.no_results", + }; const key = keyMap[mode] || "common.error"; stateEl.dataset.i18nState = key; stateEl.innerText = t(key); stateEl.style.display = "block"; - grid.style.display = mode === "loading" || mode === "error" ? "none" : "block"; + grid.style.display = + mode === "loading" || mode === "error" ? "none" : "block"; } diff --git a/src/Undefined/webui/static/js/vendor/marked.min.js b/src/Undefined/webui/static/js/vendor/marked.min.js index b4e0d73b..ac5c93b4 100644 --- a/src/Undefined/webui/static/js/vendor/marked.min.js +++ b/src/Undefined/webui/static/js/vendor/marked.min.js @@ -8,62 +8,2446 @@ * DO NOT EDIT THIS FILE * The code in this file is generated from files in ./src/ */ -(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("marked",f)}else {g["marked"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports}; -"use strict";var H=Object.defineProperty;var be=Object.getOwnPropertyDescriptor;var Te=Object.getOwnPropertyNames;var we=Object.prototype.hasOwnProperty;var ye=(l,e)=>{for(var t in e)H(l,t,{get:e[t],enumerable:!0})},Re=(l,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Te(e))!we.call(l,s)&&s!==t&&H(l,s,{get:()=>e[s],enumerable:!(n=be(e,s))||n.enumerable});return l};var Se=l=>Re(H({},"__esModule",{value:!0}),l);var kt={};ye(kt,{Hooks:()=>L,Lexer:()=>x,Marked:()=>E,Parser:()=>b,Renderer:()=>$,TextRenderer:()=>_,Tokenizer:()=>S,defaults:()=>w,getDefaults:()=>z,lexer:()=>ht,marked:()=>k,options:()=>it,parse:()=>pt,parseInline:()=>ct,parser:()=>ut,setOptions:()=>ot,use:()=>lt,walkTokens:()=>at});module.exports=Se(kt);function z(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var w=z();function N(l){w=l}var I={exec:()=>null};function h(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(s,i)=>{let r=typeof i=="string"?i:i.source;return r=r.replace(m.caret,"$1"),t=t.replace(s,r),n},getRegex:()=>new RegExp(t,e)};return n}var m={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:l=>new RegExp(`^( {0,3}${l})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}#`),htmlBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}<(?:[a-z].*>|!--)`,"i")},$e=/^(?:[ \t]*(?:\n|$))+/,_e=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Le=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,O=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,ze=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,F=/(?:[*+-]|\d{1,9}[.)])/,ie=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,oe=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Me=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),Q=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,Pe=/^[^\n]+/,U=/(?!\s*\])(?:\\.|[^\[\]\\])+/,Ae=h(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",U).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Ee=h(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,F).getRegex(),v="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",K=/|$))/,Ce=h("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",K).replace("tag",v).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),le=h(Q).replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),Ie=h(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",le).getRegex(),X={blockquote:Ie,code:_e,def:Ae,fences:Le,heading:ze,hr:O,html:Ce,lheading:oe,list:Ee,newline:$e,paragraph:le,table:I,text:Pe},re=h("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),Oe={...X,lheading:Me,table:re,paragraph:h(Q).replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",re).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex()},Be={...X,html:h(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",K).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:I,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:h(Q).replace("hr",O).replace("heading",` *#{1,6} *[^ -]`).replace("lheading",oe).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},qe=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,ve=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,ae=/^( {2,}|\\)\n(?!\s*$)/,De=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g,ue=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,je=h(ue,"u").replace(/punct/g,D).getRegex(),Fe=h(ue,"u").replace(/punct/g,pe).getRegex(),he="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",Qe=h(he,"gu").replace(/notPunctSpace/g,ce).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Ue=h(he,"gu").replace(/notPunctSpace/g,He).replace(/punctSpace/g,Ge).replace(/punct/g,pe).getRegex(),Ke=h("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,ce).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Xe=h(/\\(punct)/,"gu").replace(/punct/g,D).getRegex(),We=h(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),Je=h(K).replace("(?:-->|$)","-->").getRegex(),Ve=h("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",Je).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),q=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Ye=h(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",q).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),ke=h(/^!?\[(label)\]\[(ref)\]/).replace("label",q).replace("ref",U).getRegex(),ge=h(/^!?\[(ref)\](?:\[\])?/).replace("ref",U).getRegex(),et=h("reflink|nolink(?!\\()","g").replace("reflink",ke).replace("nolink",ge).getRegex(),J={_backpedal:I,anyPunctuation:Xe,autolink:We,blockSkip:Ne,br:ae,code:ve,del:I,emStrongLDelim:je,emStrongRDelimAst:Qe,emStrongRDelimUnd:Ke,escape:qe,link:Ye,nolink:ge,punctuation:Ze,reflink:ke,reflinkSearch:et,tag:Ve,text:De,url:I},tt={...J,link:h(/^!?\[(label)\]\((.*?)\)/).replace("label",q).getRegex(),reflink:h(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",q).getRegex()},j={...J,emStrongRDelimAst:Ue,emStrongLDelim:Fe,url:h(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},fe=l=>st[l];function R(l,e){if(e){if(m.escapeTest.test(l))return l.replace(m.escapeReplace,fe)}else if(m.escapeTestNoEncode.test(l))return l.replace(m.escapeReplaceNoEncode,fe);return l}function V(l){try{l=encodeURI(l).replace(m.percentDecode,"%")}catch{return null}return l}function Y(l,e){let t=l.replace(m.findPipe,(i,r,o)=>{let a=!1,c=r;for(;--c>=0&&o[c]==="\\";)a=!a;return a?"|":" |"}),n=t.split(m.splitPipe),s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length0?-2:-1}function me(l,e,t,n,s){let i=e.href,r=e.title||null,o=l[1].replace(s.other.outputLinkReplace,"$1");n.state.inLink=!0;let a={type:l[0].charAt(0)==="!"?"image":"link",raw:t,href:i,title:r,text:o,tokens:n.inlineTokens(o)};return n.state.inLink=!1,a}function rt(l,e,t){let n=l.match(t.other.indentCodeCompensation);if(n===null)return e;let s=n[1];return e.split(` -`).map(i=>{let r=i.match(t.other.beginningSpace);if(r===null)return i;let[o]=r;return o.length>=s.length?i.slice(s.length):i}).join(` -`)}var S=class{options;rules;lexer;constructor(e){this.options=e||w}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:A(n,` -`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],s=rt(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:s}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let s=A(n,"#");(this.options.pedantic||!s||this.rules.other.endingSpaceChar.test(s))&&(n=s.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:A(t[0],` -`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=A(t[0],` -`).split(` -`),s="",i="",r=[];for(;n.length>0;){let o=!1,a=[],c;for(c=0;c1,i={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");let r=this.rules.other.listItemRegex(n),o=!1;for(;e;){let c=!1,p="",u="";if(!(t=r.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let d=t[2].split(` -`,1)[0].replace(this.rules.other.listReplaceTabs,Z=>" ".repeat(3*Z.length)),g=e.split(` -`,1)[0],T=!d.trim(),f=0;if(this.options.pedantic?(f=2,u=d.trimStart()):T?f=t[1].length+1:(f=t[2].search(this.rules.other.nonSpaceChar),f=f>4?1:f,u=d.slice(f),f+=t[1].length),T&&this.rules.other.blankLine.test(g)&&(p+=g+` -`,e=e.substring(g.length+1),c=!0),!c){let Z=this.rules.other.nextBulletRegex(f),te=this.rules.other.hrRegex(f),ne=this.rules.other.fencesBeginRegex(f),se=this.rules.other.headingBeginRegex(f),xe=this.rules.other.htmlBeginRegex(f);for(;e;){let G=e.split(` -`,1)[0],C;if(g=G,this.options.pedantic?(g=g.replace(this.rules.other.listReplaceNesting," "),C=g):C=g.replace(this.rules.other.tabCharGlobal," "),ne.test(g)||se.test(g)||xe.test(g)||Z.test(g)||te.test(g))break;if(C.search(this.rules.other.nonSpaceChar)>=f||!g.trim())u+=` -`+C.slice(f);else{if(T||d.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||ne.test(d)||se.test(d)||te.test(d))break;u+=` -`+g}!T&&!g.trim()&&(T=!0),p+=G+` -`,e=e.substring(G.length+1),d=C.slice(f)}}i.loose||(o?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(o=!0));let y=null,ee;this.options.gfm&&(y=this.rules.other.listIsTask.exec(u),y&&(ee=y[0]!=="[ ] ",u=u.replace(this.rules.other.listReplaceTask,""))),i.items.push({type:"list_item",raw:p,task:!!y,checked:ee,loose:!1,text:u,tokens:[]}),i.raw+=p}let a=i.items.at(-1);if(a)a.raw=a.raw.trimEnd(),a.text=a.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let c=0;cd.type==="space"),u=p.length>0&&p.some(d=>this.rules.other.anyLine.test(d.raw));i.loose=u}if(i.loose)for(let c=0;c({text:a,tokens:this.lexer.inline(a),header:!1,align:r.align[c]})));return r}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===` -`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let r=A(n.slice(0,-1),"\\");if((n.length-r.length)%2===0)return}else{let r=de(t[2],"()");if(r===-2)return;if(r>-1){let a=(t[0].indexOf("!")===0?5:4)+t[1].length+r;t[2]=t[2].substring(0,r),t[0]=t[0].substring(0,a).trim(),t[3]=""}}let s=t[2],i="";if(this.options.pedantic){let r=this.rules.other.pedanticHrefTitle.exec(s);r&&(s=r[1],i=r[3])}else i=t[3]?t[3].slice(1,-1):"";return s=s.trim(),this.rules.other.startAngleBracket.test(s)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?s=s.slice(1):s=s.slice(1,-1)),me(t,{href:s&&s.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let s=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[s.toLowerCase()];if(!i){let r=n[0].charAt(0);return{type:"text",raw:r,text:r}}return me(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s||s[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){let r=[...s[0]].length-1,o,a,c=r,p=0,u=s[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(u.lastIndex=0,t=t.slice(-1*e.length+r);(s=u.exec(t))!=null;){if(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o)continue;if(a=[...o].length,s[3]||s[4]){c+=a;continue}else if((s[5]||s[6])&&r%3&&!((r+a)%3)){p+=a;continue}if(c-=a,c>0)continue;a=Math.min(a,a+c+p);let d=[...s[0]][0].length,g=e.slice(0,r+s.index+d+a);if(Math.min(r,a)%2){let f=g.slice(1,-1);return{type:"em",raw:g,text:f,tokens:this.lexer.inlineTokens(f)}}let T=g.slice(2,-2);return{type:"strong",raw:g,text:T,tokens:this.lexer.inlineTokens(T)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),s=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return s&&i&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,s;return t[2]==="@"?(n=t[1],s="mailto:"+n):(n=t[1],s=n),{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,s;if(t[2]==="@")n=t[0],s="mailto:"+n;else{let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);n=t[0],t[1]==="www."?s="http://"+t[0]:s=t[0]}return{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}};var x=class l{tokens;options;state;tokenizer;inlineQueue;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||w,this.options.tokenizer=this.options.tokenizer||new S,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:m,block:B.normal,inline:P.normal};this.options.pedantic?(t.block=B.pedantic,t.inline=P.pedantic):this.options.gfm&&(t.block=B.gfm,this.options.breaks?t.inline=P.breaks:t.inline=P.gfm),this.tokenizer.rules=t}static get rules(){return{block:B,inline:P}}static lex(e,t){return new l(t).lex(e)}static lexInline(e,t){return new l(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,` -`),this.blockTokens(e,this.tokens);for(let t=0;t(s=r.call({lexer:this},e,t))?(e=e.substring(s.raw.length),t.push(s),!0):!1))continue;if(s=this.tokenizer.space(e)){e=e.substring(s.raw.length);let r=t.at(-1);s.raw.length===1&&r!==void 0?r.raw+=` -`:t.push(s);continue}if(s=this.tokenizer.code(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="paragraph"||r?.type==="text"?(r.raw+=` -`+s.raw,r.text+=` -`+s.text,this.inlineQueue.at(-1).src=r.text):t.push(s);continue}if(s=this.tokenizer.fences(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.heading(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.hr(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.blockquote(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.list(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.html(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.def(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="paragraph"||r?.type==="text"?(r.raw+=` -`+s.raw,r.text+=` -`+s.raw,this.inlineQueue.at(-1).src=r.text):this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title});continue}if(s=this.tokenizer.table(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.lheading(e)){e=e.substring(s.raw.length),t.push(s);continue}let i=e;if(this.options.extensions?.startBlock){let r=1/0,o=e.slice(1),a;this.options.extensions.startBlock.forEach(c=>{a=c.call({lexer:this},o),typeof a=="number"&&a>=0&&(r=Math.min(r,a))}),r<1/0&&r>=0&&(i=e.substring(0,r+1))}if(this.state.top&&(s=this.tokenizer.paragraph(i))){let r=t.at(-1);n&&r?.type==="paragraph"?(r.raw+=` -`+s.raw,r.text+=` -`+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s),n=i.length!==e.length,e=e.substring(s.raw.length);continue}if(s=this.tokenizer.text(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="text"?(r.raw+=` -`+s.raw,r.text+=` -`+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s);continue}if(e){let r="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(r);break}else throw new Error(r)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,s=null;if(this.tokens.links){let o=Object.keys(this.tokens.links);if(o.length>0)for(;(s=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)o.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(s=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,s.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(s=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);let i=!1,r="";for(;e;){i||(r=""),i=!1;let o;if(this.options.extensions?.inline?.some(c=>(o=c.call({lexer:this},e,t))?(e=e.substring(o.raw.length),t.push(o),!0):!1))continue;if(o=this.tokenizer.escape(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.tag(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.link(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(o.raw.length);let c=t.at(-1);o.type==="text"&&c?.type==="text"?(c.raw+=o.raw,c.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,r)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.codespan(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.br(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.del(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.autolink(e)){e=e.substring(o.raw.length),t.push(o);continue}if(!this.state.inLink&&(o=this.tokenizer.url(e))){e=e.substring(o.raw.length),t.push(o);continue}let a=e;if(this.options.extensions?.startInline){let c=1/0,p=e.slice(1),u;this.options.extensions.startInline.forEach(d=>{u=d.call({lexer:this},p),typeof u=="number"&&u>=0&&(c=Math.min(c,u))}),c<1/0&&c>=0&&(a=e.substring(0,c+1))}if(o=this.tokenizer.inlineText(a)){e=e.substring(o.raw.length),o.raw.slice(-1)!=="_"&&(r=o.raw.slice(-1)),i=!0;let c=t.at(-1);c?.type==="text"?(c.raw+=o.raw,c.text+=o.text):t.push(o);continue}if(e){let c="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(c);break}else throw new Error(c)}}return t}};var $=class{options;parser;constructor(e){this.options=e||w}space(e){return""}code({text:e,lang:t,escaped:n}){let s=(t||"").match(m.notSpaceStart)?.[0],i=e.replace(m.endingNewline,"")+` -`;return s?'
'+(n?i:R(i,!0))+`
-`:"
"+(n?i:R(i,!0))+`
-`}blockquote({tokens:e}){return`
+(function (g, f) { + if (typeof exports == "object" && typeof module < "u") { + module.exports = f(); + } else if ("function" == typeof define && define.amd) { + define("marked", f); + } else { + g["marked"] = f(); + } +})( + typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this, + function () { + var exports = {}; + var __exports = exports; + var module = { exports }; + ("use strict"); + var H = Object.defineProperty; + var be = Object.getOwnPropertyDescriptor; + var Te = Object.getOwnPropertyNames; + var we = Object.prototype.hasOwnProperty; + var ye = (l, e) => { + for (var t in e) H(l, t, { get: e[t], enumerable: !0 }); + }, + Re = (l, e, t, n) => { + if ((e && typeof e == "object") || typeof e == "function") + for (let s of Te(e)) + !we.call(l, s) && + s !== t && + H(l, s, { + get: () => e[s], + enumerable: !(n = be(e, s)) || n.enumerable, + }); + return l; + }; + var Se = (l) => Re(H({}, "__esModule", { value: !0 }), l); + var kt = {}; + ye(kt, { + Hooks: () => L, + Lexer: () => x, + Marked: () => E, + Parser: () => b, + Renderer: () => $, + TextRenderer: () => _, + Tokenizer: () => S, + defaults: () => w, + getDefaults: () => z, + lexer: () => ht, + marked: () => k, + options: () => it, + parse: () => pt, + parseInline: () => ct, + parser: () => ut, + setOptions: () => ot, + use: () => lt, + walkTokens: () => at, + }); + module.exports = Se(kt); + function z() { + return { + async: !1, + breaks: !1, + extensions: null, + gfm: !0, + hooks: null, + pedantic: !1, + renderer: null, + silent: !1, + tokenizer: null, + walkTokens: null, + }; + } + var w = z(); + function N(l) { + w = l; + } + var I = { exec: () => null }; + function h(l, e = "") { + let t = typeof l == "string" ? l : l.source, + n = { + replace: (s, i) => { + let r = typeof i == "string" ? i : i.source; + return ( + (r = r.replace(m.caret, "$1")), + (t = t.replace(s, r)), + n + ); + }, + getRegex: () => new RegExp(t, e), + }; + return n; + } + var m = { + codeRemoveIndent: /^(?: {1,4}| {0,3}\t)/gm, + outputLinkReplace: /\\([\[\]])/g, + indentCodeCompensation: /^(\s+)(?:```)/, + beginningSpace: /^\s+/, + endingHash: /#$/, + startingSpaceChar: /^ /, + endingSpaceChar: / $/, + nonSpaceChar: /[^ ]/, + newLineCharGlobal: /\n/g, + tabCharGlobal: /\t/g, + multipleSpaceGlobal: /\s+/g, + blankLine: /^[ \t]*$/, + doubleBlankLine: /\n[ \t]*\n[ \t]*$/, + blockquoteStart: /^ {0,3}>/, + blockquoteSetextReplace: /\n {0,3}((?:=+|-+) *)(?=\n|$)/g, + blockquoteSetextReplace2: /^ {0,3}>[ \t]?/gm, + listReplaceTabs: /^\t+/, + listReplaceNesting: /^ {1,4}(?=( {4})*[^ ])/g, + listIsTask: /^\[[ xX]\] /, + listReplaceTask: /^\[[ xX]\] +/, + anyLine: /\n.*\n/, + hrefBrackets: /^<(.*)>$/, + tableDelimiter: /[:|]/, + tableAlignChars: /^\||\| *$/g, + tableRowBlankLine: /\n[ \t]*$/, + tableAlignRight: /^ *-+: *$/, + tableAlignCenter: /^ *:-+: *$/, + tableAlignLeft: /^ *:-+ *$/, + startATag: /^/i, + startPreScriptTag: /^<(pre|code|kbd|script)(\s|>)/i, + endPreScriptTag: /^<\/(pre|code|kbd|script)(\s|>)/i, + startAngleBracket: /^$/, + pedanticHrefTitle: /^([^'"]*[^\s])\s+(['"])(.*)\2/, + unicodeAlphaNumeric: /[\p{L}\p{N}]/u, + escapeTest: /[&<>"']/, + escapeReplace: /[&<>"']/g, + escapeTestNoEncode: + /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/, + escapeReplaceNoEncode: + /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g, + unescapeTest: /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi, + caret: /(^|[^\[])\^/g, + percentDecode: /%25/g, + findPipe: /\|/g, + splitPipe: / \|/, + slashPipe: /\\\|/g, + carriageReturn: /\r\n|\r/g, + spaceLine: /^ +$/gm, + notSpaceStart: /^\S*/, + endingNewline: /\n$/, + listItemRegex: (l) => + new RegExp(`^( {0,3}${l})((?:[ ][^\\n]*)?(?:\\n|$))`), + nextBulletRegex: (l) => + new RegExp( + `^ {0,${Math.min(3, l - 1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`, + ), + hrRegex: (l) => + new RegExp( + `^ {0,${Math.min(3, l - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`, + ), + fencesBeginRegex: (l) => + new RegExp(`^ {0,${Math.min(3, l - 1)}}(?:\`\`\`|~~~)`), + headingBeginRegex: (l) => + new RegExp(`^ {0,${Math.min(3, l - 1)}}#`), + htmlBeginRegex: (l) => + new RegExp( + `^ {0,${Math.min(3, l - 1)}}<(?:[a-z].*>|!--)`, + "i", + ), + }, + $e = /^(?:[ \t]*(?:\n|$))+/, + _e = /^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/, + Le = + /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/, + O = + /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/, + ze = /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/, + F = /(?:[*+-]|\d{1,9}[.)])/, + ie = + /^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/, + oe = h(ie) + .replace(/bull/g, F) + .replace(/blockCode/g, /(?: {4}| {0,3}\t)/) + .replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) + .replace(/blockquote/g, / {0,3}>/) + .replace(/heading/g, / {0,3}#{1,6}/) + .replace(/html/g, / {0,3}<[^\n>]+>\n/) + .replace(/\|table/g, "") + .getRegex(), + Me = h(ie) + .replace(/bull/g, F) + .replace(/blockCode/g, /(?: {4}| {0,3}\t)/) + .replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) + .replace(/blockquote/g, / {0,3}>/) + .replace(/heading/g, / {0,3}#{1,6}/) + .replace(/html/g, / {0,3}<[^\n>]+>\n/) + .replace(/table/g, / {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/) + .getRegex(), + Q = + /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/, + Pe = /^[^\n]+/, + U = /(?!\s*\])(?:\\.|[^\[\]\\])+/, + Ae = h( + /^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/, + ) + .replace("label", U) + .replace( + "title", + /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/, + ) + .getRegex(), + Ee = h(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/) + .replace(/bull/g, F) + .getRegex(), + v = + "address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul", + K = /|$))/, + Ce = h( + "^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))", + "i", + ) + .replace("comment", K) + .replace("tag", v) + .replace( + "attribute", + / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/, + ) + .getRegex(), + le = h(Q) + .replace("hr", O) + .replace("heading", " {0,3}#{1,6}(?:\\s|$)") + .replace("|lheading", "") + .replace("|table", "") + .replace("blockquote", " {0,3}>") + .replace( + "fences", + " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n", + ) + .replace("list", " {0,3}(?:[*+-]|1[.)]) ") + .replace( + "html", + ")|<(?:script|pre|style|textarea|!--)", + ) + .replace("tag", v) + .getRegex(), + Ie = h(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/) + .replace("paragraph", le) + .getRegex(), + X = { + blockquote: Ie, + code: _e, + def: Ae, + fences: Le, + heading: ze, + hr: O, + html: Ce, + lheading: oe, + list: Ee, + newline: $e, + paragraph: le, + table: I, + text: Pe, + }, + re = h( + "^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)", + ) + .replace("hr", O) + .replace("heading", " {0,3}#{1,6}(?:\\s|$)") + .replace("blockquote", " {0,3}>") + .replace("code", "(?: {4}| {0,3} )[^\\n]") + .replace( + "fences", + " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n", + ) + .replace("list", " {0,3}(?:[*+-]|1[.)]) ") + .replace( + "html", + ")|<(?:script|pre|style|textarea|!--)", + ) + .replace("tag", v) + .getRegex(), + Oe = { + ...X, + lheading: Me, + table: re, + paragraph: h(Q) + .replace("hr", O) + .replace("heading", " {0,3}#{1,6}(?:\\s|$)") + .replace("|lheading", "") + .replace("table", re) + .replace("blockquote", " {0,3}>") + .replace( + "fences", + " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n", + ) + .replace("list", " {0,3}(?:[*+-]|1[.)]) ") + .replace( + "html", + ")|<(?:script|pre|style|textarea|!--)", + ) + .replace("tag", v) + .getRegex(), + }, + Be = { + ...X, + html: h( + `^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`, + ) + .replace("comment", K) + .replace( + /tag/g, + "(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b", + ) + .getRegex(), + def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, + heading: /^(#{1,6})(.*)(?:\n+|$)/, + fences: I, + lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, + paragraph: h(Q) + .replace("hr", O) + .replace( + "heading", + ` *#{1,6} *[^ +]`, + ) + .replace("lheading", oe) + .replace("|table", "") + .replace("blockquote", " {0,3}>") + .replace("|fences", "") + .replace("|list", "") + .replace("|html", "") + .replace("|tag", "") + .getRegex(), + }, + qe = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/, + ve = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/, + ae = /^( {2,}|\\)\n(?!\s*$)/, + De = + /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g, + ue = + /^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/, + je = h(ue, "u").replace(/punct/g, D).getRegex(), + Fe = h(ue, "u").replace(/punct/g, pe).getRegex(), + he = + "^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)", + Qe = h(he, "gu") + .replace(/notPunctSpace/g, ce) + .replace(/punctSpace/g, W) + .replace(/punct/g, D) + .getRegex(), + Ue = h(he, "gu") + .replace(/notPunctSpace/g, He) + .replace(/punctSpace/g, Ge) + .replace(/punct/g, pe) + .getRegex(), + Ke = h( + "^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)", + "gu", + ) + .replace(/notPunctSpace/g, ce) + .replace(/punctSpace/g, W) + .replace(/punct/g, D) + .getRegex(), + Xe = h(/\\(punct)/, "gu") + .replace(/punct/g, D) + .getRegex(), + We = h(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/) + .replace("scheme", /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/) + .replace( + "email", + /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/, + ) + .getRegex(), + Je = h(K).replace("(?:-->|$)", "-->").getRegex(), + Ve = h( + "^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^", + ) + .replace("comment", Je) + .replace( + "attribute", + /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/, + ) + .getRegex(), + q = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/, + Ye = h( + /^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/, + ) + .replace("label", q) + .replace("href", /<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/) + .replace( + "title", + /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/, + ) + .getRegex(), + ke = h(/^!?\[(label)\]\[(ref)\]/) + .replace("label", q) + .replace("ref", U) + .getRegex(), + ge = h(/^!?\[(ref)\](?:\[\])?/) + .replace("ref", U) + .getRegex(), + et = h("reflink|nolink(?!\\()", "g") + .replace("reflink", ke) + .replace("nolink", ge) + .getRegex(), + J = { + _backpedal: I, + anyPunctuation: Xe, + autolink: We, + blockSkip: Ne, + br: ae, + code: ve, + del: I, + emStrongLDelim: je, + emStrongRDelimAst: Qe, + emStrongRDelimUnd: Ke, + escape: qe, + link: Ye, + nolink: ge, + punctuation: Ze, + reflink: ke, + reflinkSearch: et, + tag: Ve, + text: De, + url: I, + }, + tt = { + ...J, + link: h(/^!?\[(label)\]\((.*?)\)/) + .replace("label", q) + .getRegex(), + reflink: h(/^!?\[(label)\]\s*\[([^\]]*)\]/) + .replace("label", q) + .getRegex(), + }, + j = { + ...J, + emStrongRDelimAst: Ue, + emStrongLDelim: Fe, + url: h( + /^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, + "i", + ) + .replace( + "email", + /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/, + ) + .getRegex(), + _backpedal: + /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, + del: /^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/, + text: /^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\": ">", + '"': """, + "'": "'", + }, + fe = (l) => st[l]; + function R(l, e) { + if (e) { + if (m.escapeTest.test(l)) return l.replace(m.escapeReplace, fe); + } else if (m.escapeTestNoEncode.test(l)) + return l.replace(m.escapeReplaceNoEncode, fe); + return l; + } + function V(l) { + try { + l = encodeURI(l).replace(m.percentDecode, "%"); + } catch { + return null; + } + return l; + } + function Y(l, e) { + let t = l.replace(m.findPipe, (i, r, o) => { + let a = !1, + c = r; + for (; --c >= 0 && o[c] === "\\"; ) a = !a; + return a ? "|" : " |"; + }), + n = t.split(m.splitPipe), + s = 0; + if ( + (n[0].trim() || n.shift(), + n.length > 0 && !n.at(-1)?.trim() && n.pop(), + e) + ) + if (n.length > e) n.splice(e); + else for (; n.length < e; ) n.push(""); + for (; s < n.length; s++) + n[s] = n[s].trim().replace(m.slashPipe, "|"); + return n; + } + function A(l, e, t) { + let n = l.length; + if (n === 0) return ""; + let s = 0; + for (; s < n; ) { + let i = l.charAt(n - s - 1); + if (i === e && !t) s++; + else if (i !== e && t) s++; + else break; + } + return l.slice(0, n - s); + } + function de(l, e) { + if (l.indexOf(e[1]) === -1) return -1; + let t = 0; + for (let n = 0; n < l.length; n++) + if (l[n] === "\\") n++; + else if (l[n] === e[0]) t++; + else if (l[n] === e[1] && (t--, t < 0)) return n; + return t > 0 ? -2 : -1; + } + function me(l, e, t, n, s) { + let i = e.href, + r = e.title || null, + o = l[1].replace(s.other.outputLinkReplace, "$1"); + n.state.inLink = !0; + let a = { + type: l[0].charAt(0) === "!" ? "image" : "link", + raw: t, + href: i, + title: r, + text: o, + tokens: n.inlineTokens(o), + }; + return (n.state.inLink = !1), a; + } + function rt(l, e, t) { + let n = l.match(t.other.indentCodeCompensation); + if (n === null) return e; + let s = n[1]; + return e + .split(` +`) + .map((i) => { + let r = i.match(t.other.beginningSpace); + if (r === null) return i; + let [o] = r; + return o.length >= s.length ? i.slice(s.length) : i; + }) + .join(` +`); + } + var S = class { + options; + rules; + lexer; + constructor(e) { + this.options = e || w; + } + space(e) { + let t = this.rules.block.newline.exec(e); + if (t && t[0].length > 0) return { type: "space", raw: t[0] }; + } + code(e) { + let t = this.rules.block.code.exec(e); + if (t) { + let n = t[0].replace(this.rules.other.codeRemoveIndent, ""); + return { + type: "code", + raw: t[0], + codeBlockStyle: "indented", + text: this.options.pedantic + ? n + : A( + n, + ` +`, + ), + }; + } + } + fences(e) { + let t = this.rules.block.fences.exec(e); + if (t) { + let n = t[0], + s = rt(n, t[3] || "", this.rules); + return { + type: "code", + raw: n, + lang: t[2] + ? t[2] + .trim() + .replace( + this.rules.inline.anyPunctuation, + "$1", + ) + : t[2], + text: s, + }; + } + } + heading(e) { + let t = this.rules.block.heading.exec(e); + if (t) { + let n = t[2].trim(); + if (this.rules.other.endingHash.test(n)) { + let s = A(n, "#"); + (this.options.pedantic || + !s || + this.rules.other.endingSpaceChar.test(s)) && + (n = s.trim()); + } + return { + type: "heading", + raw: t[0], + depth: t[1].length, + text: n, + tokens: this.lexer.inline(n), + }; + } + } + hr(e) { + let t = this.rules.block.hr.exec(e); + if (t) + return { + type: "hr", + raw: A( + t[0], + ` +`, + ), + }; + } + blockquote(e) { + let t = this.rules.block.blockquote.exec(e); + if (t) { + let n = A( + t[0], + ` +`, + ).split(` +`), + s = "", + i = "", + r = []; + for (; n.length > 0; ) { + let o = !1, + a = [], + c; + for (c = 0; c < n.length; c++) + if (this.rules.other.blockquoteStart.test(n[c])) + a.push(n[c]), (o = !0); + else if (!o) a.push(n[c]); + else break; + n = n.slice(c); + let p = a.join(` +`), + u = p + .replace( + this.rules.other.blockquoteSetextReplace, + ` + $1`, + ) + .replace( + this.rules.other.blockquoteSetextReplace2, + "", + ); + (s = s + ? `${s} +${p}` + : p), + (i = i + ? `${i} +${u}` + : u); + let d = this.lexer.state.top; + if ( + ((this.lexer.state.top = !0), + this.lexer.blockTokens(u, r, !0), + (this.lexer.state.top = d), + n.length === 0) + ) + break; + let g = r.at(-1); + if (g?.type === "code") break; + if (g?.type === "blockquote") { + let T = g, + f = + T.raw + + ` +` + + n.join(` +`), + y = this.blockquote(f); + (r[r.length - 1] = y), + (s = + s.substring(0, s.length - T.raw.length) + + y.raw), + (i = + i.substring(0, i.length - T.text.length) + + y.text); + break; + } else if (g?.type === "list") { + let T = g, + f = + T.raw + + ` +` + + n.join(` +`), + y = this.list(f); + (r[r.length - 1] = y), + (s = + s.substring(0, s.length - g.raw.length) + + y.raw), + (i = + i.substring(0, i.length - T.raw.length) + + y.raw), + (n = f.substring(r.at(-1).raw.length).split(` +`)); + continue; + } + } + return { type: "blockquote", raw: s, tokens: r, text: i }; + } + } + list(e) { + let t = this.rules.block.list.exec(e); + if (t) { + let n = t[1].trim(), + s = n.length > 1, + i = { + type: "list", + raw: "", + ordered: s, + start: s ? +n.slice(0, -1) : "", + loose: !1, + items: [], + }; + (n = s ? `\\d{1,9}\\${n.slice(-1)}` : `\\${n}`), + this.options.pedantic && (n = s ? n : "[*+-]"); + let r = this.rules.other.listItemRegex(n), + o = !1; + for (; e; ) { + let c = !1, + p = "", + u = ""; + if (!(t = r.exec(e)) || this.rules.block.hr.test(e)) + break; + (p = t[0]), (e = e.substring(p.length)); + let d = t[2] + .split( + ` +`, + 1, + )[0] + .replace( + this.rules.other.listReplaceTabs, + (Z) => " ".repeat(3 * Z.length), + ), + g = e.split( + ` +`, + 1, + )[0], + T = !d.trim(), + f = 0; + if ( + (this.options.pedantic + ? ((f = 2), (u = d.trimStart())) + : T + ? (f = t[1].length + 1) + : ((f = t[2].search( + this.rules.other.nonSpaceChar, + )), + (f = f > 4 ? 1 : f), + (u = d.slice(f)), + (f += t[1].length)), + T && + this.rules.other.blankLine.test(g) && + ((p += + g + + ` +`), + (e = e.substring(g.length + 1)), + (c = !0)), + !c) + ) { + let Z = this.rules.other.nextBulletRegex(f), + te = this.rules.other.hrRegex(f), + ne = this.rules.other.fencesBeginRegex(f), + se = this.rules.other.headingBeginRegex(f), + xe = this.rules.other.htmlBeginRegex(f); + for (; e; ) { + let G = e.split( + ` +`, + 1, + )[0], + C; + if ( + ((g = G), + this.options.pedantic + ? ((g = g.replace( + this.rules.other + .listReplaceNesting, + " ", + )), + (C = g)) + : (C = g.replace( + this.rules.other.tabCharGlobal, + " ", + )), + ne.test(g) || + se.test(g) || + xe.test(g) || + Z.test(g) || + te.test(g)) + ) + break; + if ( + C.search(this.rules.other.nonSpaceChar) >= + f || + !g.trim() + ) + u += + ` +` + C.slice(f); + else { + if ( + T || + d + .replace( + this.rules.other.tabCharGlobal, + " ", + ) + .search( + this.rules.other.nonSpaceChar, + ) >= 4 || + ne.test(d) || + se.test(d) || + te.test(d) + ) + break; + u += + ` +` + g; + } + !T && !g.trim() && (T = !0), + (p += + G + + ` +`), + (e = e.substring(G.length + 1)), + (d = C.slice(f)); + } + } + i.loose || + (o + ? (i.loose = !0) + : this.rules.other.doubleBlankLine.test(p) && + (o = !0)); + let y = null, + ee; + this.options.gfm && + ((y = this.rules.other.listIsTask.exec(u)), + y && + ((ee = y[0] !== "[ ] "), + (u = u.replace( + this.rules.other.listReplaceTask, + "", + )))), + i.items.push({ + type: "list_item", + raw: p, + task: !!y, + checked: ee, + loose: !1, + text: u, + tokens: [], + }), + (i.raw += p); + } + let a = i.items.at(-1); + if (a) + (a.raw = a.raw.trimEnd()), (a.text = a.text.trimEnd()); + else return; + i.raw = i.raw.trimEnd(); + for (let c = 0; c < i.items.length; c++) + if ( + ((this.lexer.state.top = !1), + (i.items[c].tokens = this.lexer.blockTokens( + i.items[c].text, + [], + )), + !i.loose) + ) { + let p = i.items[c].tokens.filter( + (d) => d.type === "space", + ), + u = + p.length > 0 && + p.some((d) => + this.rules.other.anyLine.test(d.raw), + ); + i.loose = u; + } + if (i.loose) + for (let c = 0; c < i.items.length; c++) + i.items[c].loose = !0; + return i; + } + } + html(e) { + let t = this.rules.block.html.exec(e); + if (t) + return { + type: "html", + block: !0, + raw: t[0], + pre: + t[1] === "pre" || + t[1] === "script" || + t[1] === "style", + text: t[0], + }; + } + def(e) { + let t = this.rules.block.def.exec(e); + if (t) { + let n = t[1] + .toLowerCase() + .replace(this.rules.other.multipleSpaceGlobal, " "), + s = t[2] + ? t[2] + .replace(this.rules.other.hrefBrackets, "$1") + .replace( + this.rules.inline.anyPunctuation, + "$1", + ) + : "", + i = t[3] + ? t[3] + .substring(1, t[3].length - 1) + .replace( + this.rules.inline.anyPunctuation, + "$1", + ) + : t[3]; + return { + type: "def", + tag: n, + raw: t[0], + href: s, + title: i, + }; + } + } + table(e) { + let t = this.rules.block.table.exec(e); + if (!t || !this.rules.other.tableDelimiter.test(t[2])) return; + let n = Y(t[1]), + s = t[2] + .replace(this.rules.other.tableAlignChars, "") + .split("|"), + i = t[3]?.trim() + ? t[3] + .replace(this.rules.other.tableRowBlankLine, "") + .split(` +`) + : [], + r = { + type: "table", + raw: t[0], + header: [], + align: [], + rows: [], + }; + if (n.length === s.length) { + for (let o of s) + this.rules.other.tableAlignRight.test(o) + ? r.align.push("right") + : this.rules.other.tableAlignCenter.test(o) + ? r.align.push("center") + : this.rules.other.tableAlignLeft.test(o) + ? r.align.push("left") + : r.align.push(null); + for (let o = 0; o < n.length; o++) + r.header.push({ + text: n[o], + tokens: this.lexer.inline(n[o]), + header: !0, + align: r.align[o], + }); + for (let o of i) + r.rows.push( + Y(o, r.header.length).map((a, c) => ({ + text: a, + tokens: this.lexer.inline(a), + header: !1, + align: r.align[c], + })), + ); + return r; + } + } + lheading(e) { + let t = this.rules.block.lheading.exec(e); + if (t) + return { + type: "heading", + raw: t[0], + depth: t[2].charAt(0) === "=" ? 1 : 2, + text: t[1], + tokens: this.lexer.inline(t[1]), + }; + } + paragraph(e) { + let t = this.rules.block.paragraph.exec(e); + if (t) { + let n = + t[1].charAt(t[1].length - 1) === + ` +` + ? t[1].slice(0, -1) + : t[1]; + return { + type: "paragraph", + raw: t[0], + text: n, + tokens: this.lexer.inline(n), + }; + } + } + text(e) { + let t = this.rules.block.text.exec(e); + if (t) + return { + type: "text", + raw: t[0], + text: t[0], + tokens: this.lexer.inline(t[0]), + }; + } + escape(e) { + let t = this.rules.inline.escape.exec(e); + if (t) return { type: "escape", raw: t[0], text: t[1] }; + } + tag(e) { + let t = this.rules.inline.tag.exec(e); + if (t) + return ( + !this.lexer.state.inLink && + this.rules.other.startATag.test(t[0]) + ? (this.lexer.state.inLink = !0) + : this.lexer.state.inLink && + this.rules.other.endATag.test(t[0]) && + (this.lexer.state.inLink = !1), + !this.lexer.state.inRawBlock && + this.rules.other.startPreScriptTag.test(t[0]) + ? (this.lexer.state.inRawBlock = !0) + : this.lexer.state.inRawBlock && + this.rules.other.endPreScriptTag.test(t[0]) && + (this.lexer.state.inRawBlock = !1), + { + type: "html", + raw: t[0], + inLink: this.lexer.state.inLink, + inRawBlock: this.lexer.state.inRawBlock, + block: !1, + text: t[0], + } + ); + } + link(e) { + let t = this.rules.inline.link.exec(e); + if (t) { + let n = t[2].trim(); + if ( + !this.options.pedantic && + this.rules.other.startAngleBracket.test(n) + ) { + if (!this.rules.other.endAngleBracket.test(n)) return; + let r = A(n.slice(0, -1), "\\"); + if ((n.length - r.length) % 2 === 0) return; + } else { + let r = de(t[2], "()"); + if (r === -2) return; + if (r > -1) { + let a = + (t[0].indexOf("!") === 0 ? 5 : 4) + + t[1].length + + r; + (t[2] = t[2].substring(0, r)), + (t[0] = t[0].substring(0, a).trim()), + (t[3] = ""); + } + } + let s = t[2], + i = ""; + if (this.options.pedantic) { + let r = this.rules.other.pedanticHrefTitle.exec(s); + r && ((s = r[1]), (i = r[3])); + } else i = t[3] ? t[3].slice(1, -1) : ""; + return ( + (s = s.trim()), + this.rules.other.startAngleBracket.test(s) && + (this.options.pedantic && + !this.rules.other.endAngleBracket.test(n) + ? (s = s.slice(1)) + : (s = s.slice(1, -1))), + me( + t, + { + href: + s && + s.replace( + this.rules.inline.anyPunctuation, + "$1", + ), + title: + i && + i.replace( + this.rules.inline.anyPunctuation, + "$1", + ), + }, + t[0], + this.lexer, + this.rules, + ) + ); + } + } + reflink(e, t) { + let n; + if ( + (n = this.rules.inline.reflink.exec(e)) || + (n = this.rules.inline.nolink.exec(e)) + ) { + let s = (n[2] || n[1]).replace( + this.rules.other.multipleSpaceGlobal, + " ", + ), + i = t[s.toLowerCase()]; + if (!i) { + let r = n[0].charAt(0); + return { type: "text", raw: r, text: r }; + } + return me(n, i, n[0], this.lexer, this.rules); + } + } + emStrong(e, t, n = "") { + let s = this.rules.inline.emStrongLDelim.exec(e); + if ( + !s || + (s[3] && n.match(this.rules.other.unicodeAlphaNumeric)) + ) + return; + if ( + !(s[1] || s[2] || "") || + !n || + this.rules.inline.punctuation.exec(n) + ) { + let r = [...s[0]].length - 1, + o, + a, + c = r, + p = 0, + u = + s[0][0] === "*" + ? this.rules.inline.emStrongRDelimAst + : this.rules.inline.emStrongRDelimUnd; + for ( + u.lastIndex = 0, t = t.slice(-1 * e.length + r); + (s = u.exec(t)) != null; + ) { + if ( + ((o = s[1] || s[2] || s[3] || s[4] || s[5] || s[6]), + !o) + ) + continue; + if (((a = [...o].length), s[3] || s[4])) { + c += a; + continue; + } else if ((s[5] || s[6]) && r % 3 && !((r + a) % 3)) { + p += a; + continue; + } + if (((c -= a), c > 0)) continue; + a = Math.min(a, a + c + p); + let d = [...s[0]][0].length, + g = e.slice(0, r + s.index + d + a); + if (Math.min(r, a) % 2) { + let f = g.slice(1, -1); + return { + type: "em", + raw: g, + text: f, + tokens: this.lexer.inlineTokens(f), + }; + } + let T = g.slice(2, -2); + return { + type: "strong", + raw: g, + text: T, + tokens: this.lexer.inlineTokens(T), + }; + } + } + } + codespan(e) { + let t = this.rules.inline.code.exec(e); + if (t) { + let n = t[2].replace( + this.rules.other.newLineCharGlobal, + " ", + ), + s = this.rules.other.nonSpaceChar.test(n), + i = + this.rules.other.startingSpaceChar.test(n) && + this.rules.other.endingSpaceChar.test(n); + return ( + s && i && (n = n.substring(1, n.length - 1)), + { type: "codespan", raw: t[0], text: n } + ); + } + } + br(e) { + let t = this.rules.inline.br.exec(e); + if (t) return { type: "br", raw: t[0] }; + } + del(e) { + let t = this.rules.inline.del.exec(e); + if (t) + return { + type: "del", + raw: t[0], + text: t[2], + tokens: this.lexer.inlineTokens(t[2]), + }; + } + autolink(e) { + let t = this.rules.inline.autolink.exec(e); + if (t) { + let n, s; + return ( + t[2] === "@" + ? ((n = t[1]), (s = "mailto:" + n)) + : ((n = t[1]), (s = n)), + { + type: "link", + raw: t[0], + text: n, + href: s, + tokens: [{ type: "text", raw: n, text: n }], + } + ); + } + } + url(e) { + let t; + if ((t = this.rules.inline.url.exec(e))) { + let n, s; + if (t[2] === "@") (n = t[0]), (s = "mailto:" + n); + else { + let i; + do + (i = t[0]), + (t[0] = + this.rules.inline._backpedal.exec( + t[0], + )?.[0] ?? ""); + while (i !== t[0]); + (n = t[0]), + t[1] === "www." + ? (s = "http://" + t[0]) + : (s = t[0]); + } + return { + type: "link", + raw: t[0], + text: n, + href: s, + tokens: [{ type: "text", raw: n, text: n }], + }; + } + } + inlineText(e) { + let t = this.rules.inline.text.exec(e); + if (t) { + let n = this.lexer.state.inRawBlock; + return { type: "text", raw: t[0], text: t[0], escaped: n }; + } + } + }; + var x = class l { + tokens; + options; + state; + tokenizer; + inlineQueue; + constructor(e) { + (this.tokens = []), + (this.tokens.links = Object.create(null)), + (this.options = e || w), + (this.options.tokenizer = + this.options.tokenizer || new S()), + (this.tokenizer = this.options.tokenizer), + (this.tokenizer.options = this.options), + (this.tokenizer.lexer = this), + (this.inlineQueue = []), + (this.state = { inLink: !1, inRawBlock: !1, top: !0 }); + let t = { other: m, block: B.normal, inline: P.normal }; + this.options.pedantic + ? ((t.block = B.pedantic), (t.inline = P.pedantic)) + : this.options.gfm && + ((t.block = B.gfm), + this.options.breaks + ? (t.inline = P.breaks) + : (t.inline = P.gfm)), + (this.tokenizer.rules = t); + } + static get rules() { + return { block: B, inline: P }; + } + static lex(e, t) { + return new l(t).lex(e); + } + static lexInline(e, t) { + return new l(t).inlineTokens(e); + } + lex(e) { + (e = e.replace( + m.carriageReturn, + ` +`, + )), + this.blockTokens(e, this.tokens); + for (let t = 0; t < this.inlineQueue.length; t++) { + let n = this.inlineQueue[t]; + this.inlineTokens(n.src, n.tokens); + } + return (this.inlineQueue = []), this.tokens; + } + blockTokens(e, t = [], n = !1) { + for ( + this.options.pedantic && + (e = e + .replace(m.tabCharGlobal, " ") + .replace(m.spaceLine, "")); + e; + ) { + let s; + if ( + this.options.extensions?.block?.some((r) => + (s = r.call({ lexer: this }, e, t)) + ? ((e = e.substring(s.raw.length)), + t.push(s), + !0) + : !1, + ) + ) + continue; + if ((s = this.tokenizer.space(e))) { + e = e.substring(s.raw.length); + let r = t.at(-1); + s.raw.length === 1 && r !== void 0 + ? (r.raw += ` +`) + : t.push(s); + continue; + } + if ((s = this.tokenizer.code(e))) { + e = e.substring(s.raw.length); + let r = t.at(-1); + r?.type === "paragraph" || r?.type === "text" + ? ((r.raw += + ` +` + s.raw), + (r.text += + ` +` + s.text), + (this.inlineQueue.at(-1).src = r.text)) + : t.push(s); + continue; + } + if ((s = this.tokenizer.fences(e))) { + (e = e.substring(s.raw.length)), t.push(s); + continue; + } + if ((s = this.tokenizer.heading(e))) { + (e = e.substring(s.raw.length)), t.push(s); + continue; + } + if ((s = this.tokenizer.hr(e))) { + (e = e.substring(s.raw.length)), t.push(s); + continue; + } + if ((s = this.tokenizer.blockquote(e))) { + (e = e.substring(s.raw.length)), t.push(s); + continue; + } + if ((s = this.tokenizer.list(e))) { + (e = e.substring(s.raw.length)), t.push(s); + continue; + } + if ((s = this.tokenizer.html(e))) { + (e = e.substring(s.raw.length)), t.push(s); + continue; + } + if ((s = this.tokenizer.def(e))) { + e = e.substring(s.raw.length); + let r = t.at(-1); + r?.type === "paragraph" || r?.type === "text" + ? ((r.raw += + ` +` + s.raw), + (r.text += + ` +` + s.raw), + (this.inlineQueue.at(-1).src = r.text)) + : this.tokens.links[s.tag] || + (this.tokens.links[s.tag] = { + href: s.href, + title: s.title, + }); + continue; + } + if ((s = this.tokenizer.table(e))) { + (e = e.substring(s.raw.length)), t.push(s); + continue; + } + if ((s = this.tokenizer.lheading(e))) { + (e = e.substring(s.raw.length)), t.push(s); + continue; + } + let i = e; + if (this.options.extensions?.startBlock) { + let r = 1 / 0, + o = e.slice(1), + a; + this.options.extensions.startBlock.forEach((c) => { + (a = c.call({ lexer: this }, o)), + typeof a == "number" && + a >= 0 && + (r = Math.min(r, a)); + }), + r < 1 / 0 && r >= 0 && (i = e.substring(0, r + 1)); + } + if (this.state.top && (s = this.tokenizer.paragraph(i))) { + let r = t.at(-1); + n && r?.type === "paragraph" + ? ((r.raw += + ` +` + s.raw), + (r.text += + ` +` + s.text), + this.inlineQueue.pop(), + (this.inlineQueue.at(-1).src = r.text)) + : t.push(s), + (n = i.length !== e.length), + (e = e.substring(s.raw.length)); + continue; + } + if ((s = this.tokenizer.text(e))) { + e = e.substring(s.raw.length); + let r = t.at(-1); + r?.type === "text" + ? ((r.raw += + ` +` + s.raw), + (r.text += + ` +` + s.text), + this.inlineQueue.pop(), + (this.inlineQueue.at(-1).src = r.text)) + : t.push(s); + continue; + } + if (e) { + let r = "Infinite loop on byte: " + e.charCodeAt(0); + if (this.options.silent) { + console.error(r); + break; + } else throw new Error(r); + } + } + return (this.state.top = !0), t; + } + inline(e, t = []) { + return this.inlineQueue.push({ src: e, tokens: t }), t; + } + inlineTokens(e, t = []) { + let n = e, + s = null; + if (this.tokens.links) { + let o = Object.keys(this.tokens.links); + if (o.length > 0) + for ( + ; + (s = + this.tokenizer.rules.inline.reflinkSearch.exec( + n, + )) != null; + ) + o.includes( + s[0].slice(s[0].lastIndexOf("[") + 1, -1), + ) && + (n = + n.slice(0, s.index) + + "[" + + "a".repeat(s[0].length - 2) + + "]" + + n.slice( + this.tokenizer.rules.inline + .reflinkSearch.lastIndex, + )); + } + for ( + ; + (s = this.tokenizer.rules.inline.anyPunctuation.exec(n)) != + null; + ) + n = + n.slice(0, s.index) + + "++" + + n.slice( + this.tokenizer.rules.inline.anyPunctuation + .lastIndex, + ); + for ( + ; + (s = this.tokenizer.rules.inline.blockSkip.exec(n)) != null; + ) + n = + n.slice(0, s.index) + + "[" + + "a".repeat(s[0].length - 2) + + "]" + + n.slice( + this.tokenizer.rules.inline.blockSkip.lastIndex, + ); + let i = !1, + r = ""; + for (; e; ) { + i || (r = ""), (i = !1); + let o; + if ( + this.options.extensions?.inline?.some((c) => + (o = c.call({ lexer: this }, e, t)) + ? ((e = e.substring(o.raw.length)), + t.push(o), + !0) + : !1, + ) + ) + continue; + if ((o = this.tokenizer.escape(e))) { + (e = e.substring(o.raw.length)), t.push(o); + continue; + } + if ((o = this.tokenizer.tag(e))) { + (e = e.substring(o.raw.length)), t.push(o); + continue; + } + if ((o = this.tokenizer.link(e))) { + (e = e.substring(o.raw.length)), t.push(o); + continue; + } + if ((o = this.tokenizer.reflink(e, this.tokens.links))) { + e = e.substring(o.raw.length); + let c = t.at(-1); + o.type === "text" && c?.type === "text" + ? ((c.raw += o.raw), (c.text += o.text)) + : t.push(o); + continue; + } + if ((o = this.tokenizer.emStrong(e, n, r))) { + (e = e.substring(o.raw.length)), t.push(o); + continue; + } + if ((o = this.tokenizer.codespan(e))) { + (e = e.substring(o.raw.length)), t.push(o); + continue; + } + if ((o = this.tokenizer.br(e))) { + (e = e.substring(o.raw.length)), t.push(o); + continue; + } + if ((o = this.tokenizer.del(e))) { + (e = e.substring(o.raw.length)), t.push(o); + continue; + } + if ((o = this.tokenizer.autolink(e))) { + (e = e.substring(o.raw.length)), t.push(o); + continue; + } + if (!this.state.inLink && (o = this.tokenizer.url(e))) { + (e = e.substring(o.raw.length)), t.push(o); + continue; + } + let a = e; + if (this.options.extensions?.startInline) { + let c = 1 / 0, + p = e.slice(1), + u; + this.options.extensions.startInline.forEach((d) => { + (u = d.call({ lexer: this }, p)), + typeof u == "number" && + u >= 0 && + (c = Math.min(c, u)); + }), + c < 1 / 0 && c >= 0 && (a = e.substring(0, c + 1)); + } + if ((o = this.tokenizer.inlineText(a))) { + (e = e.substring(o.raw.length)), + o.raw.slice(-1) !== "_" && (r = o.raw.slice(-1)), + (i = !0); + let c = t.at(-1); + c?.type === "text" + ? ((c.raw += o.raw), (c.text += o.text)) + : t.push(o); + continue; + } + if (e) { + let c = "Infinite loop on byte: " + e.charCodeAt(0); + if (this.options.silent) { + console.error(c); + break; + } else throw new Error(c); + } + } + return t; + } + }; + var $ = class { + options; + parser; + constructor(e) { + this.options = e || w; + } + space(e) { + return ""; + } + code({ text: e, lang: t, escaped: n }) { + let s = (t || "").match(m.notSpaceStart)?.[0], + i = + e.replace(m.endingNewline, "") + + ` +`; + return s + ? '
' +
+                          (n ? i : R(i, !0)) +
+                          `
+` + : "
" +
+                          (n ? i : R(i, !0)) +
+                          `
+`; + } + blockquote({ tokens: e }) { + return `
${this.parser.parse(e)}
-`}html({text:e}){return e}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)} -`}hr(e){return`
-`}list(e){let t=e.ordered,n=e.start,s="";for(let o=0;o -`+s+" -`}listitem(e){let t="";if(e.task){let n=this.checkbox({checked:!!e.checked});e.loose?e.tokens[0]?.type==="paragraph"?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&e.tokens[0].tokens[0].type==="text"&&(e.tokens[0].tokens[0].text=n+" "+R(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • -`}checkbox({checked:e}){return"'}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    -`}table(e){let t="",n="";for(let i=0;i${s}`),` +`; + } + html({ text: e }) { + return e; + } + heading({ tokens: e, depth: t }) { + return `${this.parser.parseInline(e)} +`; + } + hr(e) { + return `
    +`; + } + list(e) { + let t = e.ordered, + n = e.start, + s = ""; + for (let o = 0; o < e.items.length; o++) { + let a = e.items[o]; + s += this.listitem(a); + } + let i = t ? "ol" : "ul", + r = t && n !== 1 ? ' start="' + n + '"' : ""; + return ( + "<" + + i + + r + + `> +` + + s + + " +` + ); + } + listitem(e) { + let t = ""; + if (e.task) { + let n = this.checkbox({ checked: !!e.checked }); + e.loose + ? e.tokens[0]?.type === "paragraph" + ? ((e.tokens[0].text = n + " " + e.tokens[0].text), + e.tokens[0].tokens && + e.tokens[0].tokens.length > 0 && + e.tokens[0].tokens[0].type === "text" && + ((e.tokens[0].tokens[0].text = + n + " " + R(e.tokens[0].tokens[0].text)), + (e.tokens[0].tokens[0].escaped = !0))) + : e.tokens.unshift({ + type: "text", + raw: n + " ", + text: n + " ", + escaped: !0, + }) + : (t += n + " "); + } + return ( + (t += this.parser.parse(e.tokens, !!e.loose)), + `
  • ${t}
  • +` + ); + } + checkbox({ checked: e }) { + return ( + "' + ); + } + paragraph({ tokens: e }) { + return `

    ${this.parser.parseInline(e)}

    +`; + } + table(e) { + let t = "", + n = ""; + for (let i = 0; i < e.header.length; i++) + n += this.tablecell(e.header[i]); + t += this.tablerow({ text: n }); + let s = ""; + for (let i = 0; i < e.rows.length; i++) { + let r = e.rows[i]; + n = ""; + for (let o = 0; o < r.length; o++) + n += this.tablecell(r[o]); + s += this.tablerow({ text: n }); + } + return ( + s && (s = `${s}`), + `
    -`+t+` -`+s+`
    -`}tablerow({text:e}){return` +` + + t + + ` +` + + s + + ` +` + ); + } + tablerow({ text: e }) { + return ` ${e} -`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+` -`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${R(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){let s=this.parser.parseInline(n),i=V(e);if(i===null)return s;e=i;let r='
    ",r}image({href:e,title:t,text:n,tokens:s}){s&&(n=this.parser.parseInline(s,this.parser.textRenderer));let i=V(e);if(i===null)return R(n);e=i;let r=`${n}{let o=i[r].flat(1/0);n=n.concat(this.walkTokens(o,t))}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let s={...n};if(s.async=this.defaults.async||s.async||!1,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let r=t.renderers[i.name];r?t.renderers[i.name]=function(...o){let a=i.renderer.apply(this,o);return a===!1&&(a=r.apply(this,o)),a}:t.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let r=t[i.level];r?r.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),s.extensions=t),n.renderer){let i=this.defaults.renderer||new $(this.defaults);for(let r in n.renderer){if(!(r in i))throw new Error(`renderer '${r}' does not exist`);if(["options","parser"].includes(r))continue;let o=r,a=n.renderer[o],c=i[o];i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u||""}}s.renderer=i}if(n.tokenizer){let i=this.defaults.tokenizer||new S(this.defaults);for(let r in n.tokenizer){if(!(r in i))throw new Error(`tokenizer '${r}' does not exist`);if(["options","rules","lexer"].includes(r))continue;let o=r,a=n.tokenizer[o],c=i[o];i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u}}s.tokenizer=i}if(n.hooks){let i=this.defaults.hooks||new L;for(let r in n.hooks){if(!(r in i))throw new Error(`hook '${r}' does not exist`);if(["options","block"].includes(r))continue;let o=r,a=n.hooks[o],c=i[o];L.passThroughHooks.has(r)?i[o]=p=>{if(this.defaults.async)return Promise.resolve(a.call(i,p)).then(d=>c.call(i,d));let u=a.call(i,p);return c.call(i,u)}:i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u}}s.hooks=i}if(n.walkTokens){let i=this.defaults.walkTokens,r=n.walkTokens;s.walkTokens=function(o){let a=[];return a.push(r.call(this,o)),i&&(a=a.concat(i.call(this,o))),a}}this.defaults={...this.defaults,...s}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return x.lex(e,t??this.defaults)}parser(e,t){return b.parse(e,t??this.defaults)}parseMarkdown(e){return(n,s)=>{let i={...s},r={...this.defaults,...i},o=this.onError(!!r.silent,!!r.async);if(this.defaults.async===!0&&i.async===!1)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);let a=r.hooks?r.hooks.provideLexer():e?x.lex:x.lexInline,c=r.hooks?r.hooks.provideParser():e?b.parse:b.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(n):n).then(p=>a(p,r)).then(p=>r.hooks?r.hooks.processAllTokens(p):p).then(p=>r.walkTokens?Promise.all(this.walkTokens(p,r.walkTokens)).then(()=>p):p).then(p=>c(p,r)).then(p=>r.hooks?r.hooks.postprocess(p):p).catch(o);try{r.hooks&&(n=r.hooks.preprocess(n));let p=a(n,r);r.hooks&&(p=r.hooks.processAllTokens(p)),r.walkTokens&&this.walkTokens(p,r.walkTokens);let u=c(p,r);return r.hooks&&(u=r.hooks.postprocess(u)),u}catch(p){return o(p)}}}onError(e,t){return n=>{if(n.message+=` -Please report this to https://github.com/markedjs/marked.`,e){let s="

    An error occurred:

    "+R(n.message+"",!0)+"
    ";return t?Promise.resolve(s):s}if(t)return Promise.reject(n);throw n}}};var M=new E;function k(l,e){return M.parse(l,e)}k.options=k.setOptions=function(l){return M.setOptions(l),k.defaults=M.defaults,N(k.defaults),k};k.getDefaults=z;k.defaults=w;k.use=function(...l){return M.use(...l),k.defaults=M.defaults,N(k.defaults),k};k.walkTokens=function(l,e){return M.walkTokens(l,e)};k.parseInline=M.parseInline;k.Parser=b;k.parser=b.parse;k.Renderer=$;k.TextRenderer=_;k.Lexer=x;k.lexer=x.lex;k.Tokenizer=S;k.Hooks=L;k.parse=k;var it=k.options,ot=k.setOptions,lt=k.use,at=k.walkTokens,ct=k.parseInline,pt=k,ut=b.parse,ht=x.lex; +`; + } + tablecell(e) { + let t = this.parser.parseInline(e.tokens), + n = e.header ? "th" : "td"; + return ( + (e.align ? `<${n} align="${e.align}">` : `<${n}>`) + + t + + ` +` + ); + } + strong({ tokens: e }) { + return `${this.parser.parseInline(e)}`; + } + em({ tokens: e }) { + return `${this.parser.parseInline(e)}`; + } + codespan({ text: e }) { + return `${R(e, !0)}`; + } + br(e) { + return "
    "; + } + del({ tokens: e }) { + return `${this.parser.parseInline(e)}`; + } + link({ href: e, title: t, tokens: n }) { + let s = this.parser.parseInline(n), + i = V(e); + if (i === null) return s; + e = i; + let r = '
    "), + r + ); + } + image({ href: e, title: t, text: n, tokens: s }) { + s && (n = this.parser.parseInline(s, this.parser.textRenderer)); + let i = V(e); + if (i === null) return R(n); + e = i; + let r = `${n} { + let o = i[r].flat(1 / 0); + n = n.concat(this.walkTokens(o, t)); + }) + : i.tokens && + (n = n.concat(this.walkTokens(i.tokens, t))); + } + } + return n; + } + use(...e) { + let t = this.defaults.extensions || { + renderers: {}, + childTokens: {}, + }; + return ( + e.forEach((n) => { + let s = { ...n }; + if ( + ((s.async = this.defaults.async || s.async || !1), + n.extensions && + (n.extensions.forEach((i) => { + if (!i.name) + throw new Error( + "extension name required", + ); + if ("renderer" in i) { + let r = t.renderers[i.name]; + r + ? (t.renderers[i.name] = function ( + ...o + ) { + let a = i.renderer.apply( + this, + o, + ); + return ( + a === !1 && + (a = r.apply( + this, + o, + )), + a + ); + }) + : (t.renderers[i.name] = + i.renderer); + } + if ("tokenizer" in i) { + if ( + !i.level || + (i.level !== "block" && + i.level !== "inline") + ) + throw new Error( + "extension level must be 'block' or 'inline'", + ); + let r = t[i.level]; + r + ? r.unshift(i.tokenizer) + : (t[i.level] = [i.tokenizer]), + i.start && + (i.level === "block" + ? t.startBlock + ? t.startBlock.push( + i.start, + ) + : (t.startBlock = [ + i.start, + ]) + : i.level === "inline" && + (t.startInline + ? t.startInline.push( + i.start, + ) + : (t.startInline = [ + i.start, + ]))); + } + "childTokens" in i && + i.childTokens && + (t.childTokens[i.name] = i.childTokens); + }), + (s.extensions = t)), + n.renderer) + ) { + let i = + this.defaults.renderer || new $(this.defaults); + for (let r in n.renderer) { + if (!(r in i)) + throw new Error( + `renderer '${r}' does not exist`, + ); + if (["options", "parser"].includes(r)) continue; + let o = r, + a = n.renderer[o], + c = i[o]; + i[o] = (...p) => { + let u = a.apply(i, p); + return ( + u === !1 && (u = c.apply(i, p)), u || "" + ); + }; + } + s.renderer = i; + } + if (n.tokenizer) { + let i = + this.defaults.tokenizer || new S(this.defaults); + for (let r in n.tokenizer) { + if (!(r in i)) + throw new Error( + `tokenizer '${r}' does not exist`, + ); + if (["options", "rules", "lexer"].includes(r)) + continue; + let o = r, + a = n.tokenizer[o], + c = i[o]; + i[o] = (...p) => { + let u = a.apply(i, p); + return u === !1 && (u = c.apply(i, p)), u; + }; + } + s.tokenizer = i; + } + if (n.hooks) { + let i = this.defaults.hooks || new L(); + for (let r in n.hooks) { + if (!(r in i)) + throw new Error( + `hook '${r}' does not exist`, + ); + if (["options", "block"].includes(r)) continue; + let o = r, + a = n.hooks[o], + c = i[o]; + L.passThroughHooks.has(r) + ? (i[o] = (p) => { + if (this.defaults.async) + return Promise.resolve( + a.call(i, p), + ).then((d) => c.call(i, d)); + let u = a.call(i, p); + return c.call(i, u); + }) + : (i[o] = (...p) => { + let u = a.apply(i, p); + return ( + u === !1 && (u = c.apply(i, p)), u + ); + }); + } + s.hooks = i; + } + if (n.walkTokens) { + let i = this.defaults.walkTokens, + r = n.walkTokens; + s.walkTokens = function (o) { + let a = []; + return ( + a.push(r.call(this, o)), + i && (a = a.concat(i.call(this, o))), + a + ); + }; + } + this.defaults = { ...this.defaults, ...s }; + }), + this + ); + } + setOptions(e) { + return (this.defaults = { ...this.defaults, ...e }), this; + } + lexer(e, t) { + return x.lex(e, t ?? this.defaults); + } + parser(e, t) { + return b.parse(e, t ?? this.defaults); + } + parseMarkdown(e) { + return (n, s) => { + let i = { ...s }, + r = { ...this.defaults, ...i }, + o = this.onError(!!r.silent, !!r.async); + if (this.defaults.async === !0 && i.async === !1) + return o( + new Error( + "marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.", + ), + ); + if (typeof n > "u" || n === null) + return o( + new Error( + "marked(): input parameter is undefined or null", + ), + ); + if (typeof n != "string") + return o( + new Error( + "marked(): input parameter is of type " + + Object.prototype.toString.call(n) + + ", string expected", + ), + ); + r.hooks && ((r.hooks.options = r), (r.hooks.block = e)); + let a = r.hooks + ? r.hooks.provideLexer() + : e + ? x.lex + : x.lexInline, + c = r.hooks + ? r.hooks.provideParser() + : e + ? b.parse + : b.parseInline; + if (r.async) + return Promise.resolve( + r.hooks ? r.hooks.preprocess(n) : n, + ) + .then((p) => a(p, r)) + .then((p) => + r.hooks ? r.hooks.processAllTokens(p) : p, + ) + .then((p) => + r.walkTokens + ? Promise.all( + this.walkTokens(p, r.walkTokens), + ).then(() => p) + : p, + ) + .then((p) => c(p, r)) + .then((p) => (r.hooks ? r.hooks.postprocess(p) : p)) + .catch(o); + try { + r.hooks && (n = r.hooks.preprocess(n)); + let p = a(n, r); + r.hooks && (p = r.hooks.processAllTokens(p)), + r.walkTokens && this.walkTokens(p, r.walkTokens); + let u = c(p, r); + return r.hooks && (u = r.hooks.postprocess(u)), u; + } catch (p) { + return o(p); + } + }; + } + onError(e, t) { + return (n) => { + if ( + ((n.message += ` +Please report this to https://github.com/markedjs/marked.`), + e) + ) { + let s = + "

    An error occurred:

    " +
    +                            R(n.message + "", !0) +
    +                            "
    "; + return t ? Promise.resolve(s) : s; + } + if (t) return Promise.reject(n); + throw n; + }; + } + }; + var M = new E(); + function k(l, e) { + return M.parse(l, e); + } + k.options = k.setOptions = function (l) { + return M.setOptions(l), (k.defaults = M.defaults), N(k.defaults), k; + }; + k.getDefaults = z; + k.defaults = w; + k.use = function (...l) { + return M.use(...l), (k.defaults = M.defaults), N(k.defaults), k; + }; + k.walkTokens = function (l, e) { + return M.walkTokens(l, e); + }; + k.parseInline = M.parseInline; + k.Parser = b; + k.parser = b.parse; + k.Renderer = $; + k.TextRenderer = _; + k.Lexer = x; + k.lexer = x.lex; + k.Tokenizer = S; + k.Hooks = L; + k.parse = k; + var it = k.options, + ot = k.setOptions, + lt = k.use, + at = k.walkTokens, + ct = k.parseInline, + pt = k, + ut = b.parse, + ht = x.lex; -if(__exports != exports)module.exports = exports;return module.exports})); + if (__exports != exports) module.exports = exports; + return module.exports; + }, +); diff --git a/src/Undefined/webui/templates/index.html b/src/Undefined/webui/templates/index.html index 98ad198b..d2752cb7 100644 --- a/src/Undefined/webui/templates/index.html +++ b/src/Undefined/webui/templates/index.html @@ -366,6 +366,10 @@

    探针

    +
    +
    引导与管理探针
    +
    --
    +
    内部探针
    --
    diff --git a/src/Undefined/webui/utils/__init__.py b/src/Undefined/webui/utils/__init__.py index 9e4e2f1c..959893b8 100644 --- a/src/Undefined/webui/utils/__init__.py +++ b/src/Undefined/webui/utils/__init__.py @@ -4,6 +4,7 @@ load_default_data, validate_toml, validate_required_config, + load_bootstrap_probe_data, tail_file, CONFIG_EXAMPLE_PATH, TomlData, @@ -32,6 +33,7 @@ "load_default_data", "validate_toml", "validate_required_config", + "load_bootstrap_probe_data", "tail_file", "CONFIG_EXAMPLE_PATH", "TomlData", diff --git a/src/Undefined/webui/utils/config_io.py b/src/Undefined/webui/utils/config_io.py index 5632f0b9..ebf824ec 100644 --- a/src/Undefined/webui/utils/config_io.py +++ b/src/Undefined/webui/utils/config_io.py @@ -5,6 +5,7 @@ from pathlib import Path from typing import Any +from Undefined.config import load_webui_settings from Undefined.config.loader import CONFIG_PATH, Config from Undefined.utils.resources import resolve_resource_path @@ -89,6 +90,45 @@ def validate_required_config() -> tuple[bool, str]: return False, str(exc) +def load_bootstrap_probe_data() -> dict[str, Any]: + source = read_config_source() + content = str(source.get("content") or "") + exists = bool(source.get("exists")) + toml_valid, toml_message = validate_toml(content) + strict_valid = False + strict_message = "" + if toml_valid: + strict_valid, strict_message = validate_required_config() + + danger_defaults: list[str] = [] + parsed: dict[str, Any] = {} + if toml_valid and content.strip(): + try: + loaded = tomllib.loads(content) + if isinstance(loaded, dict): + parsed = loaded + except Exception: + parsed = {} + + webui_settings = load_webui_settings() + if webui_settings.using_default_password: + danger_defaults.append("webui.password") + api_auth_key = str(parsed.get("api", {}).get("auth_key") or "").strip() + if api_auth_key == "changeme": + danger_defaults.append("api.auth_key") + + return { + "config_exists": exists, + "config_source": source.get("source", ""), + "toml_valid": toml_valid, + "toml_message": toml_message, + "config_valid": strict_valid, + "validation_error": "" if strict_valid else strict_message, + "using_default_password": webui_settings.using_default_password, + "danger_defaults": danger_defaults, + } + + def load_default_data() -> TomlData: example_path = _resolve_config_example_path() if example_path is None or not example_path.exists(): diff --git a/tests/test_llm_request_params.py b/tests/test_llm_request_params.py index f68c46ed..3c2cb7b5 100644 --- a/tests/test_llm_request_params.py +++ b/tests/test_llm_request_params.py @@ -1,11 +1,14 @@ from __future__ import annotations +from types import SimpleNamespace from typing import Any, cast +from unittest.mock import AsyncMock import httpx import pytest from openai import AsyncOpenAI, BadRequestError +from Undefined.ai.client import AIClient from Undefined.ai.llm import ModelRequester, _encode_tool_name_for_api from Undefined.ai.transports.openai_transport import normalize_responses_result from Undefined.ai.parsing import extract_choices_content @@ -456,6 +459,86 @@ async def test_responses_transport_state_uses_previous_response_id_and_tool_outp } +@pytest.mark.asyncio +async def test_ai_client_request_model_prefetch_keeps_transport_count_from_caller_messages( + monkeypatch: pytest.MonkeyPatch, +) -> None: + client = object.__new__(AIClient) + requester = AsyncMock(return_value={"choices": []}) + prefetch_mock = AsyncMock( + return_value=( + [ + {"role": "system", "content": "【预先工具结果】\n- lookup: done"}, + {"role": "user", "content": "hello"}, + ], + [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "lookup weather", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + } + ], + ) + ) + setattr(client, "_requester", SimpleNamespace(request=requester)) + setattr( + client, + "tool_manager", + SimpleNamespace(maybe_merge_agent_tools=lambda _call_type, tools: tools), + ) + monkeypatch.setattr(client, "_maybe_prefetch_tools", prefetch_mock) + + cfg = ChatModelConfig( + api_url="https://api.openai.com/v1", + api_key="sk-test", + model_name="gpt-test", + max_tokens=512, + api_mode="responses", + ) + caller_messages: list[dict[str, Any]] = [{"role": "user", "content": "hello"}] + tools = [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "lookup weather", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + } + ] + + await AIClient.request_model( + client, + model_config=cfg, + messages=caller_messages, + max_tokens=128, + call_type="chat", + tools=tools, + ) + + prefetch_mock.assert_awaited_once_with(caller_messages, tools, "chat") + assert requester.await_count == 1 + assert requester.await_args is not None + assert requester.await_args.kwargs["messages"] == [ + {"role": "system", "content": "【预先工具结果】\n- lookup: done"}, + {"role": "user", "content": "hello"}, + ] + assert requester.await_args.kwargs["message_count_for_transport"] == len( + caller_messages + ) + + @pytest.mark.asyncio async def test_responses_transport_state_uses_prefetched_message_count() -> None: requester = ModelRequester( diff --git a/tests/test_webui_management_api.py b/tests/test_webui_management_api.py new file mode 100644 index 00000000..26d3b1c6 --- /dev/null +++ b/tests/test_webui_management_api.py @@ -0,0 +1,277 @@ +from __future__ import annotations + +import json +from types import SimpleNamespace +from typing import Any, cast + +from aiohttp import web + +from Undefined.api import app as runtime_api_app +from Undefined.webui import app as webui_app +from Undefined.webui.app import create_app +from Undefined.webui.core import SessionStore +from Undefined.webui.routes import _auth, _index, _shared, _system +from Undefined.webui.routes._shared import ( + REDIRECT_TO_CONFIG_ONCE_APP_KEY, + SESSION_COOKIE, + SESSION_STORE_APP_KEY, + SETTINGS_APP_KEY, +) + + +class DummyRequest(SimpleNamespace): + async def json(self) -> dict[str, object]: + return dict(getattr(self, "_json", {})) + + +def _request( + *, + json_body: dict[str, object] | None = None, + headers: dict[str, str] | None = None, + cookies: dict[str, str] | None = None, + query: dict[str, str] | None = None, + settings: object | None = None, + session_store: SessionStore | None = None, +) -> DummyRequest: + return DummyRequest( + _json=json_body or {}, + headers=headers or {}, + cookies=cookies or {}, + query=query or {}, + app={ + SETTINGS_APP_KEY: settings + or SimpleNamespace( + url="127.0.0.1", + port=8787, + password="secret", + using_default_password=False, + config_exists=True, + ), + SESSION_STORE_APP_KEY: session_store or SessionStore(), + REDIRECT_TO_CONFIG_ONCE_APP_KEY: False, + }, + remote="127.0.0.1", + transport=None, + ) + + +def _json_payload(response: web.StreamResponse) -> dict[str, object]: + payload_text = cast(web.Response, response).text + assert payload_text is not None + return cast(dict[str, object], json.loads(payload_text)) + + +def test_session_store_issues_and_refreshes_auth_tokens() -> None: + session_store = SessionStore() + + first = session_store.issue_auth_tokens() + access_token = cast(str, first["access_token"]) + refresh_token = cast(str, first["refresh_token"]) + assert session_store.is_valid(access_token, allowed_kinds={"access"}) + assert session_store.is_valid(refresh_token, allowed_kinds={"refresh"}) + + second = session_store.refresh_auth_tokens(refresh_token) + assert second is not None + assert second["access_token"] != first["access_token"] + assert second["refresh_token"] != first["refresh_token"] + assert not session_store.is_valid(refresh_token, allowed_kinds={"refresh"}) + + +async def test_login_handler_returns_cookie_and_bearer_tokens() -> None: + session_store = SessionStore() + request = _request(json_body={"password": "secret"}, session_store=session_store) + + response = await _auth.login_handler(cast(web.Request, cast(Any, request))) + payload = _json_payload(response) + access_token = payload["access_token"] + refresh_token = payload["refresh_token"] + + assert payload["success"] is True + assert isinstance(access_token, str) + assert isinstance(refresh_token, str) + assert response.cookies[SESSION_COOKIE].value + assert session_store.is_valid(access_token, allowed_kinds={"access"}) + assert session_store.is_valid(refresh_token, allowed_kinds={"refresh"}) + + +async def test_refresh_handler_rotates_access_token() -> None: + session_store = SessionStore() + tokens = session_store.issue_auth_tokens() + request = _request( + json_body={"refresh_token": tokens["refresh_token"]}, + session_store=session_store, + ) + + response = await _auth.refresh_handler(cast(web.Request, cast(Any, request))) + payload = _json_payload(response) + + assert payload["success"] is True + assert payload["access_token"] != tokens["access_token"] + assert payload["refresh_token"] != tokens["refresh_token"] + assert session_store.is_valid( + cast(str, payload["access_token"]), allowed_kinds={"access"} + ) + + +async def test_bootstrap_probe_handler_reports_management_state( + monkeypatch: Any, +) -> None: + session_store = SessionStore() + tokens = session_store.issue_auth_tokens() + request = _request( + headers={"Authorization": f"Bearer {tokens['access_token']}"}, + session_store=session_store, + ) + monkeypatch.setattr( + _system, + "load_bootstrap_probe_data", + lambda: { + "config_exists": True, + "config_source": "config.toml", + "toml_valid": True, + "toml_message": "OK", + "config_valid": False, + "validation_error": "missing onebot.ws_url", + "using_default_password": False, + "danger_defaults": [], + }, + ) + + async def _fake_runtime() -> tuple[bool, bool, str]: + return True, False, "connection refused" + + monkeypatch.setattr(_system, "_runtime_health_status", _fake_runtime) + + response = await _system.bootstrap_probe_handler( + cast(web.Request, cast(Any, request)) + ) + payload = _json_payload(response) + + assert payload["config_exists"] is True + assert payload["runtime_enabled"] is True + assert payload["runtime_reachable"] is False + assert payload["auth_mode"] == "token" + assert payload["advice"] + + +def test_create_app_registers_management_routes() -> None: + app = create_app() + routes = { + (route.method, cast(Any, route.resource).canonical) + for route in app.router.routes() + if getattr(route, "resource", None) is not None + and hasattr(route.resource, "canonical") + } + assert ("POST", "/api/v1/management/auth/login") in routes + assert ("POST", "/api/v1/management/auth/refresh") in routes + assert ("GET", "/api/v1/management/probes/bootstrap") in routes + assert ("GET", "/api/v1/management/runtime/meta") in routes + assert ("POST", "/api/v1/management/config/validate") in routes + assert ("POST", "/api/v1/management/bot/start") in routes + + +async def test_index_handler_applies_launcher_mode_and_initial_view() -> None: + request = _request( + query={ + "lang": "en", + "theme": "dark", + "view": "app", + "tab": "logs", + "client": "native", + } + ) + + response = await _index.index_handler(cast(web.Request, cast(Any, request))) + payload_text = cast(web.Response, response).text + + assert payload_text is not None + assert '"lang": "en"' in payload_text + assert '"theme": "dark"' in payload_text + assert '"initial_tab": "logs"' in payload_text + assert '"launcher_mode": true' in payload_text + assert ( + '' + in payload_text + ) + + +def test_webui_cors_only_allows_trusted_origins(monkeypatch: Any) -> None: + monkeypatch.setattr( + webui_app, + "load_webui_settings", + lambda: SimpleNamespace(url="127.0.0.1", port=8787), + ) + trusted_request = cast( + web.Request, + cast(Any, DummyRequest(headers={"Origin": "http://127.0.0.1:8787"})), + ) + trusted_response = web.Response(status=200) + webui_app._apply_cors_headers(trusted_request, trusted_response) + assert trusted_response.headers.get("Access-Control-Allow-Origin") == ( + "http://127.0.0.1:8787" + ) + assert trusted_response.headers.get("Access-Control-Allow-Credentials") == "true" + + loopback_request = cast( + web.Request, + cast(Any, DummyRequest(headers={"Origin": "http://localhost:1420"})), + ) + loopback_response = web.Response(status=200) + webui_app._apply_cors_headers(loopback_request, loopback_response) + assert loopback_response.headers.get("Access-Control-Allow-Origin") == ( + "http://localhost:1420" + ) + + untrusted_request = cast( + web.Request, + cast(Any, DummyRequest(headers={"Origin": "https://evil.example"})), + ) + untrusted_response = web.Response(status=200) + webui_app._apply_cors_headers(untrusted_request, untrusted_response) + assert "Access-Control-Allow-Origin" not in untrusted_response.headers + assert "Access-Control-Allow-Credentials" not in untrusted_response.headers + + +def test_runtime_api_cors_only_allows_trusted_origins(monkeypatch: Any) -> None: + monkeypatch.setattr( + runtime_api_app, + "load_webui_settings", + lambda: SimpleNamespace(url="127.0.0.1", port=8787), + ) + trusted_request = cast( + web.Request, + cast(Any, DummyRequest(headers={"Origin": "tauri://localhost"})), + ) + trusted_response = web.Response(status=200) + runtime_api_app._apply_cors_headers(trusted_request, trusted_response) + assert trusted_response.headers.get("Access-Control-Allow-Origin") == ( + "tauri://localhost" + ) + assert trusted_response.headers.get("Access-Control-Allow-Credentials") == "true" + + loopback_request = cast( + web.Request, + cast(Any, DummyRequest(headers={"Origin": "http://localhost:1420"})), + ) + loopback_response = web.Response(status=200) + runtime_api_app._apply_cors_headers(loopback_request, loopback_response) + assert loopback_response.headers.get("Access-Control-Allow-Origin") == ( + "http://localhost:1420" + ) + + untrusted_request = cast( + web.Request, + cast(Any, DummyRequest(headers={"Origin": "https://evil.example"})), + ) + untrusted_response = web.Response(status=200) + runtime_api_app._apply_cors_headers(untrusted_request, untrusted_response) + assert "Access-Control-Allow-Origin" not in untrusted_response.headers + assert "Access-Control-Allow-Credentials" not in untrusted_response.headers + + +def test_get_refresh_token_prefers_cookie_when_payload_missing() -> None: + request = cast( + web.Request, + cast(Any, DummyRequest(cookies={_shared.TOKEN_COOKIE: "refresh-cookie"})), + ) + assert _shared.get_refresh_token(request, payload={}) == "refresh-cookie" diff --git a/uv.lock b/uv.lock index e66861fa..d83788e0 100644 --- a/uv.lock +++ b/uv.lock @@ -4671,7 +4671,7 @@ wheels = [ [[package]] name = "undefined-bot" -version = "3.1.2" +version = "3.2.0" source = { editable = "." } dependencies = [ { name = "aiofiles" },