From b5b286c44f6a1456bef1db4ab904cecb92ff15a3 Mon Sep 17 00:00:00 2001
From: kronecker <2026692687@qq.com>
Date: Sun, 4 Oct 2026 15:46:49 +0800
Subject: [PATCH 1/8] chore(workspace): bootstrap platform, judge and frontend
---
.editorconfig | 15 +
.gitattributes | 4 +
.gitignore | 35 +
AGENTS.md | 22 +
Makefile | 82 +
README.md | 71 +
backend/.env.example | 5 +
backend/.python-version | 1 +
backend/README.md | 25 +
backend/alembic.ini | 37 +
backend/migrations/env.py | 52 +
backend/migrations/script.py.mako | 24 +
backend/migrations/versions/.gitkeep | 0
backend/pyproject.toml | 46 +
backend/src/kf_oj/__init__.py | 3 +
backend/src/kf_oj/api/__init__.py | 1 +
backend/src/kf_oj/api/app.py | 36 +
backend/src/kf_oj/api/v1/__init__.py | 1 +
backend/src/kf_oj/api/v1/router.py | 12 +
backend/src/kf_oj/contracts/__init__.py | 1 +
backend/src/kf_oj/contracts/export.py | 41 +
backend/src/kf_oj/contracts/health.py | 11 +
backend/src/kf_oj/contracts/judge.py | 14 +
backend/src/kf_oj/core/__init__.py | 1 +
backend/src/kf_oj/core/logging.py | 22 +
backend/src/kf_oj/core/settings.py | 13 +
backend/src/kf_oj/infra/__init__.py | 1 +
backend/src/kf_oj/infra/artifacts/__init__.py | 1 +
backend/src/kf_oj/infra/db/__init__.py | 1 +
backend/src/kf_oj/infra/db/base.py | 14 +
backend/src/kf_oj/infra/db/session.py | 13 +
backend/src/kf_oj/infra/messaging/__init__.py | 1 +
backend/src/kf_oj/modules/__init__.py | 1 +
backend/src/kf_oj/modules/community/README.md | 6 +
.../src/kf_oj/modules/community/__init__.py | 1 +
backend/src/kf_oj/modules/identity/README.md | 5 +
.../src/kf_oj/modules/identity/__init__.py | 1 +
backend/src/kf_oj/modules/learning/README.md | 7 +
.../src/kf_oj/modules/learning/__init__.py | 1 +
.../src/kf_oj/modules/operations/README.md | 5 +
.../src/kf_oj/modules/operations/__init__.py | 1 +
backend/src/kf_oj/modules/problems/README.md | 5 +
.../src/kf_oj/modules/problems/__init__.py | 1 +
backend/src/kf_oj/modules/skills/README.md | 7 +
backend/src/kf_oj/modules/skills/__init__.py | 1 +
.../src/kf_oj/modules/submissions/README.md | 6 +
.../src/kf_oj/modules/submissions/__init__.py | 1 +
backend/src/kf_oj/runtime/__init__.py | 5 +
backend/src/kf_oj/workers/__init__.py | 1 +
backend/src/kf_oj/workers/bootstrap.py | 25 +
backend/src/kf_oj/workers/learning.py | 9 +
backend/src/kf_oj/workers/outbox.py | 9 +
backend/tests/test_contracts.py | 26 +
backend/tests/test_health.py | 33 +
backend/uv.lock | 677 +++++
contracts/README.md | 14 +
contracts/openapi/platform.v1.json | 62 +
contracts/schemas/judge-task.v1.schema.json | 30 +
deploy/.env.example | 10 +
deploy/README.md | 21 +
deploy/compose.dev.yml | 40 +
experiments/README.md | 7 +
frontend/.env.example | 3 +
frontend/.node-version | 1 +
frontend/.npmrc | 2 +
frontend/.prettierignore | 4 +
frontend/.prettierrc.json | 6 +
frontend/README.md | 24 +
frontend/eslint.config.js | 21 +
frontend/index.html | 13 +
frontend/package.json | 42 +
frontend/pnpm-lock.yaml | 2356 +++++++++++++++++
frontend/pnpm-workspace.yaml | 4 +
frontend/src/App.vue | 9 +
frontend/src/api/client.ts | 6 +
frontend/src/api/generated/schema.d.ts | 75 +
frontend/src/api/health.ts | 9 +
frontend/src/composables/usePlatformHealth.ts | 28 +
frontend/src/layouts/AppLayout.vue | 12 +
frontend/src/main.ts | 8 +
frontend/src/router/index.ts | 13 +
frontend/src/styles/base.css | 105 +
frontend/src/views/HomeView.vue | 36 +
frontend/src/views/NotFoundView.vue | 13 +
frontend/tsconfig.json | 19 +
frontend/vite.config.ts | 17 +
judge/Cargo.lock | 911 +++++++
judge/Cargo.toml | 35 +
judge/README.md | 29 +
judge/crates/contracts/Cargo.toml | 16 +
judge/crates/contracts/src/lib.rs | 43 +
judge/crates/contracts/tests/wire_examples.rs | 29 +
judge/crates/coordinator/Cargo.toml | 18 +
judge/crates/coordinator/src/api.rs | 21 +
judge/crates/coordinator/src/lib.rs | 6 +
judge/crates/coordinator/src/main.rs | 31 +
judge/crates/engine/Cargo.toml | 12 +
judge/crates/engine/src/lib.rs | 6 +
judge/crates/sandbox/Cargo.toml | 15 +
judge/crates/sandbox/src/lib.rs | 6 +
judge/crates/sandbox/src/main.rs | 38 +
judge/crates/sandbox/src/probe.rs | 49 +
.../crates/sandbox/tests/refuse_execution.rs | 14 +
judge/crates/worker/Cargo.toml | 16 +
judge/crates/worker/src/main.rs | 28 +
judge/rust-toolchain.toml | 4 +
scripts/check_architecture.py | 109 +
skills/README.md | 7 +
tests/README.md | 7 +
.../contracts/judge-task.invalid-extra.json | 6 +
.../contracts/judge-task.invalid-id.json | 5 +
.../contracts/judge-task.invalid-kind.json | 5 +
.../contracts/judge-task.invalid-version.json | 5 +
.../fixtures/contracts/judge-task.valid.json | 5 +
114 files changed, 5926 insertions(+)
create mode 100644 .editorconfig
create mode 100644 .gitattributes
create mode 100644 .gitignore
create mode 100644 AGENTS.md
create mode 100644 Makefile
create mode 100644 backend/.env.example
create mode 100644 backend/.python-version
create mode 100644 backend/README.md
create mode 100644 backend/alembic.ini
create mode 100644 backend/migrations/env.py
create mode 100644 backend/migrations/script.py.mako
create mode 100644 backend/migrations/versions/.gitkeep
create mode 100644 backend/pyproject.toml
create mode 100644 backend/src/kf_oj/__init__.py
create mode 100644 backend/src/kf_oj/api/__init__.py
create mode 100644 backend/src/kf_oj/api/app.py
create mode 100644 backend/src/kf_oj/api/v1/__init__.py
create mode 100644 backend/src/kf_oj/api/v1/router.py
create mode 100644 backend/src/kf_oj/contracts/__init__.py
create mode 100644 backend/src/kf_oj/contracts/export.py
create mode 100644 backend/src/kf_oj/contracts/health.py
create mode 100644 backend/src/kf_oj/contracts/judge.py
create mode 100644 backend/src/kf_oj/core/__init__.py
create mode 100644 backend/src/kf_oj/core/logging.py
create mode 100644 backend/src/kf_oj/core/settings.py
create mode 100644 backend/src/kf_oj/infra/__init__.py
create mode 100644 backend/src/kf_oj/infra/artifacts/__init__.py
create mode 100644 backend/src/kf_oj/infra/db/__init__.py
create mode 100644 backend/src/kf_oj/infra/db/base.py
create mode 100644 backend/src/kf_oj/infra/db/session.py
create mode 100644 backend/src/kf_oj/infra/messaging/__init__.py
create mode 100644 backend/src/kf_oj/modules/__init__.py
create mode 100644 backend/src/kf_oj/modules/community/README.md
create mode 100644 backend/src/kf_oj/modules/community/__init__.py
create mode 100644 backend/src/kf_oj/modules/identity/README.md
create mode 100644 backend/src/kf_oj/modules/identity/__init__.py
create mode 100644 backend/src/kf_oj/modules/learning/README.md
create mode 100644 backend/src/kf_oj/modules/learning/__init__.py
create mode 100644 backend/src/kf_oj/modules/operations/README.md
create mode 100644 backend/src/kf_oj/modules/operations/__init__.py
create mode 100644 backend/src/kf_oj/modules/problems/README.md
create mode 100644 backend/src/kf_oj/modules/problems/__init__.py
create mode 100644 backend/src/kf_oj/modules/skills/README.md
create mode 100644 backend/src/kf_oj/modules/skills/__init__.py
create mode 100644 backend/src/kf_oj/modules/submissions/README.md
create mode 100644 backend/src/kf_oj/modules/submissions/__init__.py
create mode 100644 backend/src/kf_oj/runtime/__init__.py
create mode 100644 backend/src/kf_oj/workers/__init__.py
create mode 100644 backend/src/kf_oj/workers/bootstrap.py
create mode 100644 backend/src/kf_oj/workers/learning.py
create mode 100644 backend/src/kf_oj/workers/outbox.py
create mode 100644 backend/tests/test_contracts.py
create mode 100644 backend/tests/test_health.py
create mode 100644 backend/uv.lock
create mode 100644 contracts/README.md
create mode 100644 contracts/openapi/platform.v1.json
create mode 100644 contracts/schemas/judge-task.v1.schema.json
create mode 100644 deploy/.env.example
create mode 100644 deploy/README.md
create mode 100644 deploy/compose.dev.yml
create mode 100644 experiments/README.md
create mode 100644 frontend/.env.example
create mode 100644 frontend/.node-version
create mode 100644 frontend/.npmrc
create mode 100644 frontend/.prettierignore
create mode 100644 frontend/.prettierrc.json
create mode 100644 frontend/README.md
create mode 100644 frontend/eslint.config.js
create mode 100644 frontend/index.html
create mode 100644 frontend/package.json
create mode 100644 frontend/pnpm-lock.yaml
create mode 100644 frontend/pnpm-workspace.yaml
create mode 100644 frontend/src/App.vue
create mode 100644 frontend/src/api/client.ts
create mode 100644 frontend/src/api/generated/schema.d.ts
create mode 100644 frontend/src/api/health.ts
create mode 100644 frontend/src/composables/usePlatformHealth.ts
create mode 100644 frontend/src/layouts/AppLayout.vue
create mode 100644 frontend/src/main.ts
create mode 100644 frontend/src/router/index.ts
create mode 100644 frontend/src/styles/base.css
create mode 100644 frontend/src/views/HomeView.vue
create mode 100644 frontend/src/views/NotFoundView.vue
create mode 100644 frontend/tsconfig.json
create mode 100644 frontend/vite.config.ts
create mode 100644 judge/Cargo.lock
create mode 100644 judge/Cargo.toml
create mode 100644 judge/README.md
create mode 100644 judge/crates/contracts/Cargo.toml
create mode 100644 judge/crates/contracts/src/lib.rs
create mode 100644 judge/crates/contracts/tests/wire_examples.rs
create mode 100644 judge/crates/coordinator/Cargo.toml
create mode 100644 judge/crates/coordinator/src/api.rs
create mode 100644 judge/crates/coordinator/src/lib.rs
create mode 100644 judge/crates/coordinator/src/main.rs
create mode 100644 judge/crates/engine/Cargo.toml
create mode 100644 judge/crates/engine/src/lib.rs
create mode 100644 judge/crates/sandbox/Cargo.toml
create mode 100644 judge/crates/sandbox/src/lib.rs
create mode 100644 judge/crates/sandbox/src/main.rs
create mode 100644 judge/crates/sandbox/src/probe.rs
create mode 100644 judge/crates/sandbox/tests/refuse_execution.rs
create mode 100644 judge/crates/worker/Cargo.toml
create mode 100644 judge/crates/worker/src/main.rs
create mode 100644 judge/rust-toolchain.toml
create mode 100644 scripts/check_architecture.py
create mode 100644 skills/README.md
create mode 100644 tests/README.md
create mode 100644 tests/fixtures/contracts/judge-task.invalid-extra.json
create mode 100644 tests/fixtures/contracts/judge-task.invalid-id.json
create mode 100644 tests/fixtures/contracts/judge-task.invalid-kind.json
create mode 100644 tests/fixtures/contracts/judge-task.invalid-version.json
create mode 100644 tests/fixtures/contracts/judge-task.valid.json
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..a70a029
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 2
+
+[*.{py,rs,toml}]
+indent_size = 4
+
+[Makefile]
+indent_style = tab
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..f47922c
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,4 @@
+* text=auto eol=lf
+*.png binary
+*.jpg binary
+*.zip binary
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8ad3879
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,35 @@
+# In-progress planning and engineering documents stay local; never publish to Gitea.
+/docs
+
+# Local configuration and credentials
+.env
+.env.*
+!.env.example
+!*.env.example
+*.pem
+*.key
+
+# Tool caches and generated build output
+.cache/
+.tools/
+.venv/
+__pycache__/
+*.py[cod]
+.pytest_cache/
+.ruff_cache/
+.coverage
+htmlcov/
+node_modules/
+dist/
+target/
+*.egg-info/
+
+# Local data and editor state
+.Trash-*/
+.idea/
+.vscode/
+.DS_Store
+*.log
+deploy/data/
+experiments/results/
+!experiments/results/.gitkeep
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..43ad8a9
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,22 @@
+# KF-OnlineJudge 工程约定
+
+按现有模块边界扩展。本地存在 `docs/ENGINEERING.md` 和 `docs/DEVELOPMENT.md` 时先阅读;开发文档不在远端仓库中。
+
+- 本机 `docs` 使用共享目录链接,Windows 和 CachyOS 使用同一份共享文档;实际目标由 `readlink docs` 确认。不创建另一份同步副本,不把源码、依赖或构建产物写回共享目录。
+- `docs/` 中的开发文档仅保留本地,不加入 Git 提交,不推送到 Gitea;不得用 `git add -f` 绕过忽略规则。
+- 分支采用 `feat/`、`fix/`、`chore/`、`perf/`、`test/` 等工程前缀,提交采用 Conventional Commits;不添加工具名称前缀或工具署名。
+- 本仓库提交身份使用 `kronecker <2026692687@qq.com>`。
+- 后端固定 Python 3.13.16,由 uv 管理;不要改用系统 Python 3.14 或用 uvx 运行项目服务。
+- 在本机原生文件系统中开发,使用仓库根目录 Makefile;Python 环境为 `backend/.venv`,Rust 构建产物为 `judge/target`。
+- 平台为模块化单体;业务域之间通过 `public.py` 调用,不直接操作其他模块的 ORM/repository。
+- Runtime 通过注入端口调用业务工具;不导入业务模块或具体基础设施实现。
+- Rust crate 依赖方向与 Python 分层由 `scripts/check_architecture.py` 检查。新增边界须同步更新架构说明与检查规则。
+- 跨服务协议变更先更新模型和共享样例,运行 `make contracts`,保留 Python/Rust 的兼容验证。
+- 数据库迁移只由后端 Alembic 管理;协调器控制执行状态,判题 Worker 不直接写评测数据库。
+- 沙箱未验证隔离前拒绝执行;不得用直接运行宿主命令作为临时判题实现。
+- 只建立当期需要的接口和实现;未实现能力明确报告,不提供模拟成绩或虚假任务成功状态。
+- 使用项目锁文件;新增依赖说明职责。凭据、私有代码和隐藏测试数据不进入仓库。
+
+按变更运行 `make check-backend`、`make check-judge`、`make check-frontend` 和 `make architecture`。
+跨工程骨架或契约改动运行完整 `make check`,记录实际通过项和未验证的能力。
+沙箱的权限、隔离、超限和破坏性测试单独在规定执行环境验证。
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..441d684
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,82 @@
+SHELL := /bin/bash
+CARGO ?= $(if $(wildcard $(HOME)/.cargo/bin/cargo),$(HOME)/.cargo/bin/cargo,cargo)
+UV ?= uv
+PNPM ?= pnpm
+
+.PHONY: help install check architecture check-backend check-frontend check-judge contracts format api frontend coordinator sandbox-probe worker-check infra-config infra-up infra-down
+
+help:
+ @echo 'install 安装锁定依赖并编译 Rust workspace'
+ @echo 'check 架构、契约、静态检查、测试与前端构建'
+ @echo 'contracts 更新 JSON Schema、OpenAPI 与前端类型'
+ @echo 'format 格式化 Python、Rust 与前端源码'
+ @echo 'api / frontend 启动平台 API / 前端开发服务'
+ @echo 'coordinator 启动协调器 HTTP 骨架'
+ @echo 'sandbox-probe 只读探测当前 Linux 环境'
+ @echo 'worker-check 验证三个后台进程入口,不消费任务'
+ @echo 'infra-config 检查本地 PostgreSQL / RabbitMQ 配置'
+ @echo 'infra-up/down 启动/停止本地开发依赖(保留数据卷)'
+
+install:
+ cd backend && $(UV) sync --locked
+ cd frontend && $(PNPM) install --frozen-lockfile
+ cd judge && $(CARGO) build --workspace --locked
+
+check: architecture check-backend check-judge check-frontend infra-config
+
+architecture:
+ python3 scripts/check_architecture.py
+
+check-backend:
+ cd backend && $(UV) run --locked ruff check . ../scripts
+ cd backend && $(UV) run --locked ruff format --check . ../scripts
+ cd backend && $(UV) run --locked pytest
+ cd backend && $(UV) run --locked python -m kf_oj.contracts.export --check
+
+check-judge:
+ cd judge && $(CARGO) fmt --all -- --check
+ cd judge && $(CARGO) clippy --workspace --all-targets --locked -- -D warnings
+ cd judge && $(CARGO) test --workspace --locked
+
+check-frontend:
+ cd frontend && $(PNPM) check:api
+ cd frontend && $(PNPM) lint
+ cd frontend && $(PNPM) format:check
+ cd frontend && $(PNPM) typecheck
+ cd frontend && $(PNPM) build
+
+contracts:
+ cd backend && $(UV) run --locked python -m kf_oj.contracts.export
+ cd frontend && $(PNPM) generate:api
+
+format:
+ cd backend && $(UV) run --locked ruff check --fix . ../scripts
+ cd backend && $(UV) run --locked ruff format . ../scripts
+ cd judge && $(CARGO) fmt --all
+ cd frontend && $(PNPM) format
+
+api:
+ cd backend && $(UV) run --locked uvicorn kf_oj.api.app:create_app --factory --reload --host 127.0.0.1 --port 8000
+
+frontend:
+ cd frontend && $(PNPM) dev
+
+coordinator:
+ cd judge && $(CARGO) run --locked -p kf-coordinator
+
+sandbox-probe:
+ cd judge && $(CARGO) run --locked -p kf-sandbox -- probe
+
+worker-check:
+ cd backend && $(UV) run --locked kf-learning-worker --check
+ cd backend && $(UV) run --locked kf-outbox-publisher --check
+ cd judge && $(CARGO) run --locked -p kf-worker -- --check
+
+infra-config:
+ docker compose --env-file deploy/.env.example -f deploy/compose.dev.yml config --quiet
+
+infra-up:
+ docker compose --env-file deploy/.env -f deploy/compose.dev.yml up -d --wait
+
+infra-down:
+ docker compose --env-file deploy/.env -f deploy/compose.dev.yml down
diff --git a/README.md b/README.md
index 661bdde..6d0019a 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,73 @@
# KF-OnlineJudge
+以 Rust 自研判题与 Linux 沙箱、以 Python/FastAPI 提供平台服务的在线评测系统。
+前端使用 Vue 3、TypeScript、Element Plus 和 Vite。
+
+当前版本为 **0.0.1 工程骨架**:平台 API 和协调器提供存活接口,前端可检查实际 API 连接。
+沙箱隔离执行、判题、账号、题库、提交与学习业务尚未实现。
+
+## 工程划分
+
+| 目录 | 职责 |
+| --- | --- |
+| `backend/` | Python 模块化单体、平台 API、学习 Worker、待投递事件发布器及唯一数据库迁移入口 |
+| `judge/` | Rust workspace:协议、沙箱、判题编排、判题 Worker 和协调器 |
+| `frontend/` | 页面、路由、请求生命周期与生成的 API 类型 |
+| `contracts/` | 版本化 JSON Schema 与 OpenAPI 快照 |
+| `deploy/` | 本地 PostgreSQL / RabbitMQ 配置 |
+| `scripts/` | Python 导入边界与 Rust crate 依赖检查 |
+| `tests/` | 跨语言共享协议样例 |
+| `skills/`、`experiments/` | 后续内置技能与可复现实验的扩展位置 |
+
+Python 业务域分为 `identity`、`problems`、`submissions`、`community`、`learning`、`skills`
+和 `operations`。业务域之间通过公开应用接口协作,不直接访问其他域的持久化实现。
+`runtime` 管理执行机制,通过注入端口调用业务能力。
+
+Rust 分为 `kf-contracts`、`kf-sandbox`、`kf-engine`、`kf-worker`、`kf-coordinator` 五个 crate。
+协调器拥有执行状态写入权,判题 Worker 不直接写评测数据库。
+沙箱尚未验证隔离能力,当前执行入口明确拒绝运行程序。
+
+## 安装与检查
+
+工具链基线:Python 3.13.16 + uv、Rust 1.99.0、Node 26.10.0 + pnpm 11.28.4。
+Python 和 Rust 版本由工程配置固定,三套工程分别提交依赖锁文件。
+开发与构建使用 Linux 原生文件系统。
+
+```bash
+make install
+make check
+```
+
+`make check` 包含架构边界、契约同步、静态检查、测试、前端生产构建和 Compose 配置检查。
+检查无需启动数据库或队列,也不执行破坏性沙箱测试。
+`make format` 格式化源码,`make contracts` 更新 Schema、OpenAPI 和前端类型。
+
+## 启动骨架
+
+分别在终端运行:
+
+```bash
+make api # 平台 API:127.0.0.1:8000
+make frontend # Vue 开发服务:127.0.0.1:5173
+make coordinator # 协调器:127.0.0.1:8100
+```
+
+平台存活接口为 `/api/v1/health/live`,开发 API 文档为 `/api/docs`。
+前端开发服务将 `/api` 代理到平台;生产部署需另行配置反向代理与 SPA 路由回退。
+协调器存活接口为 `/health/live`。
+
+```bash
+make worker-check # 三个后台入口仅检查配置,不消费任务
+make sandbox-probe # 只读观察 Linux 环境,不创建隔离资源
+```
+
+本地数据库与队列的启动步骤见 [部署说明](deploy/README.md)。
+业务表和数据库 revision 尚未建立;迁移统一由后端 Alembic 管理。
+模型服务与第三方插件尚未接入。
+
+开发中的规划文档仅在本地维护,不包含在远端仓库中。
+后续逐步实现沙箱、单机判题、分布式恢复、并行优化和学习功能。
+
+## 许可证
+
+[MIT](LICENSE)。引用或复用其他项目代码时记录来源并遵守对应许可证。
diff --git a/backend/.env.example b/backend/.env.example
new file mode 100644
index 0000000..faad37a
--- /dev/null
+++ b/backend/.env.example
@@ -0,0 +1,5 @@
+KF_OJ_ENVIRONMENT=development
+KF_OJ_LOG_LEVEL=INFO
+# Optional until database-backed use cases are implemented.
+KF_OJ_DATABASE_URL=postgresql+asyncpg://kf_oj:local-dev-only@127.0.0.1:5432/kf_oj
+KF_OJ_AMQP_URL=amqp://kf_oj:local-dev-only@127.0.0.1:5672/kf_oj
diff --git a/backend/.python-version b/backend/.python-version
new file mode 100644
index 0000000..d540408
--- /dev/null
+++ b/backend/.python-version
@@ -0,0 +1 @@
+3.13.16
diff --git a/backend/README.md b/backend/README.md
new file mode 100644
index 0000000..27d4b56
--- /dev/null
+++ b/backend/README.md
@@ -0,0 +1,25 @@
+# Python 平台工程
+
+固定 Python 3.13.16,使用 uv 管理项目与锁文件。API、待投递事件发布器和学习 Worker
+共用一个 Python 包、一个依赖锁文件,分别运行独立进程。
+
+业务模块位于 `src/kf_oj/modules/`;HTTP 在 `api/`;通用适配器在 `infra/`;
+Agent 执行机制在 `runtime/`;进程组合入口在 `workers/`。
+业务域之间通过公开应用接口调用,不直接访问其他域的 repository/models。
+Runtime 通过注入端口调用业务能力;导入边界由根目录的架构检查脚本验证。
+
+```bash
+# 从仓库根目录调用。
+make install
+make api
+make worker-check
+```
+
+API 当前只有 `/api/v1/health/live`,不连接数据库或消息队列。
+Worker 目前仅支持 `--check`,默认拒绝进入尚未实现的任务循环。
+日志、配置和迁移入口已建立;业务表、登录、判题投递和模型接入尚未实现。
+
+Python 虚拟环境位于 `backend/.venv`,可在 `backend/` 直接使用 `uv sync --locked` 和 `uv run --locked`。
+数据库由 Alembic 唯一管理;业务模型落地后在 `migrations/env.py` 中显式登记元数据。
+不要用 API 启动时的 `create_all` 代替迁移,也不要让 Rust 维护另一套迁移历史。
+配置示例在 `.env.example`;执行迁移前复制为 `.env` 并设置实际数据库连接。
diff --git a/backend/alembic.ini b/backend/alembic.ini
new file mode 100644
index 0000000..e7c98f8
--- /dev/null
+++ b/backend/alembic.ini
@@ -0,0 +1,37 @@
+[alembic]
+script_location = %(here)s/migrations
+prepend_sys_path = .
+
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/backend/migrations/env.py b/backend/migrations/env.py
new file mode 100644
index 0000000..c105c05
--- /dev/null
+++ b/backend/migrations/env.py
@@ -0,0 +1,52 @@
+import asyncio
+from logging.config import fileConfig
+
+from alembic import context
+from sqlalchemy import Connection
+
+from kf_oj.core.settings import Settings
+from kf_oj.infra.db.base import Base
+from kf_oj.infra.db.session import build_engine
+
+config = context.config
+if config.config_file_name is not None:
+ fileConfig(config.config_file_name)
+
+# Register each business module's ORM models here when its schema is implemented.
+target_metadata = Base.metadata
+
+
+def run_offline() -> None:
+ settings = Settings()
+ if settings.database_url is None:
+ raise ValueError("KF_OJ_DATABASE_URL is required for migrations")
+ context.configure(
+ url=settings.database_url.get_secret_value(),
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ compare_type=True,
+ )
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def migrate(connection: Connection) -> None:
+ context.configure(connection=connection, target_metadata=target_metadata, compare_type=True)
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+async def run_online() -> None:
+ engine = build_engine(Settings())
+ try:
+ async with engine.connect() as connection:
+ await connection.run_sync(migrate)
+ finally:
+ await engine.dispose()
+
+
+if context.is_offline_mode():
+ run_offline()
+else:
+ asyncio.run(run_online())
diff --git a/backend/migrations/script.py.mako b/backend/migrations/script.py.mako
new file mode 100644
index 0000000..49a7df2
--- /dev/null
+++ b/backend/migrations/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+"""
+from collections.abc import Sequence
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+revision: str = ${repr(up_revision)}
+down_revision: str | None = ${repr(down_revision)}
+branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
+depends_on: str | Sequence[str] | None = ${repr(depends_on)}
+
+
+def upgrade() -> None:
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade() -> None:
+ ${downgrades if downgrades else "pass"}
diff --git a/backend/migrations/versions/.gitkeep b/backend/migrations/versions/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
new file mode 100644
index 0000000..949761d
--- /dev/null
+++ b/backend/pyproject.toml
@@ -0,0 +1,46 @@
+[build-system]
+requires = ["hatchling>=1.27,<2"]
+build-backend = "hatchling.build"
+
+[project]
+name = "kf-oj-platform"
+version = "0.0.1"
+description = "KF-OnlineJudge platform and learning workers"
+readme = "README.md"
+requires-python = ">=3.13,<3.14"
+dependencies = [
+ "fastapi>=0.115,<1",
+ "pydantic>=2.10,<3",
+ "pydantic-settings>=2.7,<3",
+ "uvicorn>=0.34,<1",
+ "sqlalchemy[asyncio]>=2.0,<3",
+ "asyncpg>=0.30,<1",
+ "alembic>=1.15,<2",
+ "aio-pika>=9.5,<10",
+]
+
+[project.scripts]
+kf-learning-worker = "kf_oj.workers.learning:main"
+kf-outbox-publisher = "kf_oj.workers.outbox:main"
+
+[dependency-groups]
+dev = [
+ "ruff>=0.11,<1",
+ "pytest>=8.3,<10",
+ "httpx>=0.28,<1",
+ "jsonschema>=4.23,<5",
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/kf_oj"]
+
+[tool.ruff]
+target-version = "py313"
+line-length = 100
+
+[tool.ruff.lint]
+select = ["E", "F", "I", "B", "UP"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+addopts = "-ra"
diff --git a/backend/src/kf_oj/__init__.py b/backend/src/kf_oj/__init__.py
new file mode 100644
index 0000000..fb3a09f
--- /dev/null
+++ b/backend/src/kf_oj/__init__.py
@@ -0,0 +1,3 @@
+"""KF-OnlineJudge platform, application modules and learning workers."""
+
+__version__ = "0.0.1"
diff --git a/backend/src/kf_oj/api/__init__.py b/backend/src/kf_oj/api/__init__.py
new file mode 100644
index 0000000..b6f9df6
--- /dev/null
+++ b/backend/src/kf_oj/api/__init__.py
@@ -0,0 +1 @@
+"""HTTP transport and application composition."""
diff --git a/backend/src/kf_oj/api/app.py b/backend/src/kf_oj/api/app.py
new file mode 100644
index 0000000..0d6d81c
--- /dev/null
+++ b/backend/src/kf_oj/api/app.py
@@ -0,0 +1,36 @@
+import logging
+from contextlib import asynccontextmanager
+
+from fastapi import FastAPI
+
+from kf_oj import __version__
+from kf_oj.api.v1.router import router
+from kf_oj.core.logging import configure_logging
+from kf_oj.core.settings import Settings
+
+
+def create_app(settings: Settings | None = None) -> FastAPI:
+ settings = settings or Settings()
+
+ @asynccontextmanager
+ async def lifespan(_: FastAPI):
+ configure_logging(settings.log_level)
+ logger = logging.getLogger("kf_oj.api")
+ logger.info("Platform API started")
+ try:
+ yield
+ finally:
+ logger.info("Platform API stopped")
+
+ app = FastAPI(
+ title="KF-OnlineJudge Platform API",
+ version=__version__,
+ description="工程骨架。当前只提供进程存活检查,业务接口逐项实现。",
+ docs_url="/api/docs" if settings.environment != "production" else None,
+ redoc_url=None,
+ openapi_url="/api/openapi.json" if settings.environment != "production" else None,
+ lifespan=lifespan,
+ )
+ app.state.settings = settings
+ app.include_router(router, prefix="/api/v1")
+ return app
diff --git a/backend/src/kf_oj/api/v1/__init__.py b/backend/src/kf_oj/api/v1/__init__.py
new file mode 100644
index 0000000..27a6722
--- /dev/null
+++ b/backend/src/kf_oj/api/v1/__init__.py
@@ -0,0 +1 @@
+"""Platform API version 1."""
diff --git a/backend/src/kf_oj/api/v1/router.py b/backend/src/kf_oj/api/v1/router.py
new file mode 100644
index 0000000..8407ad9
--- /dev/null
+++ b/backend/src/kf_oj/api/v1/router.py
@@ -0,0 +1,12 @@
+from fastapi import APIRouter
+
+from kf_oj import __version__
+from kf_oj.contracts.health import HealthResponse
+
+router = APIRouter()
+
+
+@router.get("/health/live", response_model=HealthResponse, tags=["health"], operation_id="liveness")
+async def liveness() -> HealthResponse:
+ """Check process liveness only; does not attest DB, queue or judge readiness."""
+ return HealthResponse(status="ok", service="platform", version=__version__)
diff --git a/backend/src/kf_oj/contracts/__init__.py b/backend/src/kf_oj/contracts/__init__.py
new file mode 100644
index 0000000..2a9d45c
--- /dev/null
+++ b/backend/src/kf_oj/contracts/__init__.py
@@ -0,0 +1 @@
+"""Versioned transport models; never import business or infrastructure code."""
diff --git a/backend/src/kf_oj/contracts/export.py b/backend/src/kf_oj/contracts/export.py
new file mode 100644
index 0000000..6102016
--- /dev/null
+++ b/backend/src/kf_oj/contracts/export.py
@@ -0,0 +1,41 @@
+"""Export deterministic public contracts without opening DB or queue connections."""
+
+import argparse
+import json
+from pathlib import Path
+
+from kf_oj.api.app import create_app
+from kf_oj.contracts.judge import JudgeTaskNotification
+from kf_oj.core.settings import Settings
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--check", action="store_true", help="Fail if checked-in files are stale")
+ args = parser.parse_args()
+ root = Path(__file__).resolve().parents[4]
+ schema = JudgeTaskNotification.model_json_schema()
+ schema["$schema"] = "https://json-schema.org/draft/2020-12/schema"
+ schema["$id"] = "urn:kf-oj:judge-task:v1"
+ outputs = {
+ root / "contracts/schemas/judge-task.v1.schema.json": schema,
+ root / "contracts/openapi/platform.v1.json": create_app(
+ Settings(_env_file=None, environment="test")
+ ).openapi(),
+ }
+ stale = []
+ for path, document in outputs.items():
+ content = json.dumps(document, indent=2, sort_keys=True, ensure_ascii=False) + "\n"
+ if args.check:
+ if not path.exists() or path.read_text(encoding="utf-8") != content:
+ stale.append(str(path.relative_to(root)))
+ else:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(content, encoding="utf-8")
+ print(f"Exported {path.relative_to(root)}")
+ if stale:
+ parser.exit(1, "Stale contracts: " + ", ".join(stale) + "; run make contracts\n")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/backend/src/kf_oj/contracts/health.py b/backend/src/kf_oj/contracts/health.py
new file mode 100644
index 0000000..2077005
--- /dev/null
+++ b/backend/src/kf_oj/contracts/health.py
@@ -0,0 +1,11 @@
+from typing import Literal
+
+from pydantic import BaseModel, ConfigDict
+
+
+class HealthResponse(BaseModel):
+ model_config = ConfigDict(extra="forbid")
+
+ status: Literal["ok"]
+ service: Literal["platform"]
+ version: str
diff --git a/backend/src/kf_oj/contracts/judge.py b/backend/src/kf_oj/contracts/judge.py
new file mode 100644
index 0000000..07b1149
--- /dev/null
+++ b/backend/src/kf_oj/contracts/judge.py
@@ -0,0 +1,14 @@
+from typing import Literal
+from uuid import UUID
+
+from pydantic import BaseModel, ConfigDict
+
+
+class JudgeTaskNotification(BaseModel):
+ """A wake-up notification, not execution authority or the source of task state."""
+
+ model_config = ConfigDict(extra="forbid", frozen=True)
+
+ schema_version: Literal[1]
+ kind: Literal["judge.evaluation"]
+ evaluation_id: UUID
diff --git a/backend/src/kf_oj/core/__init__.py b/backend/src/kf_oj/core/__init__.py
new file mode 100644
index 0000000..1801d85
--- /dev/null
+++ b/backend/src/kf_oj/core/__init__.py
@@ -0,0 +1 @@
+"""Cross-cutting configuration and logging; no business or transport dependencies."""
diff --git a/backend/src/kf_oj/core/logging.py b/backend/src/kf_oj/core/logging.py
new file mode 100644
index 0000000..ae5f0e1
--- /dev/null
+++ b/backend/src/kf_oj/core/logging.py
@@ -0,0 +1,22 @@
+import json
+import logging
+from datetime import UTC, datetime
+
+
+class JsonFormatter(logging.Formatter):
+ def format(self, record: logging.LogRecord) -> str:
+ payload = {
+ "timestamp": datetime.fromtimestamp(record.created, UTC).isoformat(),
+ "level": record.levelname,
+ "logger": record.name,
+ "message": record.getMessage(),
+ }
+ if record.exc_info:
+ payload["exception"] = self.formatException(record.exc_info)
+ return json.dumps(payload, ensure_ascii=False)
+
+
+def configure_logging(level: str) -> None:
+ handler = logging.StreamHandler()
+ handler.setFormatter(JsonFormatter())
+ logging.basicConfig(level=level, handlers=[handler], force=True)
diff --git a/backend/src/kf_oj/core/settings.py b/backend/src/kf_oj/core/settings.py
new file mode 100644
index 0000000..f5fb278
--- /dev/null
+++ b/backend/src/kf_oj/core/settings.py
@@ -0,0 +1,13 @@
+from typing import Literal
+
+from pydantic import SecretStr
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+
+class Settings(BaseSettings):
+ model_config = SettingsConfigDict(env_prefix="KF_OJ_", env_file=".env", extra="ignore")
+
+ environment: Literal["development", "test", "production"] = "development"
+ log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
+ database_url: SecretStr | None = None
+ amqp_url: SecretStr | None = None
diff --git a/backend/src/kf_oj/infra/__init__.py b/backend/src/kf_oj/infra/__init__.py
new file mode 100644
index 0000000..fb6c8be
--- /dev/null
+++ b/backend/src/kf_oj/infra/__init__.py
@@ -0,0 +1 @@
+"""Shared database, messaging and artifact adapters; no HTTP or business orchestration."""
diff --git a/backend/src/kf_oj/infra/artifacts/__init__.py b/backend/src/kf_oj/infra/artifacts/__init__.py
new file mode 100644
index 0000000..38d4a81
--- /dev/null
+++ b/backend/src/kf_oj/infra/artifacts/__init__.py
@@ -0,0 +1 @@
+"""Reserved for authenticated artifact access and digest verification."""
diff --git a/backend/src/kf_oj/infra/db/__init__.py b/backend/src/kf_oj/infra/db/__init__.py
new file mode 100644
index 0000000..f36de67
--- /dev/null
+++ b/backend/src/kf_oj/infra/db/__init__.py
@@ -0,0 +1 @@
+"""Database primitives; all schema migrations live in backend/migrations."""
diff --git a/backend/src/kf_oj/infra/db/base.py b/backend/src/kf_oj/infra/db/base.py
new file mode 100644
index 0000000..932a4b8
--- /dev/null
+++ b/backend/src/kf_oj/infra/db/base.py
@@ -0,0 +1,14 @@
+from sqlalchemy import MetaData
+from sqlalchemy.orm import DeclarativeBase
+
+
+class Base(DeclarativeBase):
+ metadata = MetaData(
+ naming_convention={
+ "ix": "ix_%(table_name)s_%(column_0_name)s",
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s",
+ }
+ )
diff --git a/backend/src/kf_oj/infra/db/session.py b/backend/src/kf_oj/infra/db/session.py
new file mode 100644
index 0000000..02806dd
--- /dev/null
+++ b/backend/src/kf_oj/infra/db/session.py
@@ -0,0 +1,13 @@
+from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
+
+from kf_oj.core.settings import Settings
+
+
+def build_engine(settings: Settings) -> AsyncEngine:
+ if settings.database_url is None:
+ raise ValueError("KF_OJ_DATABASE_URL is required for database-backed operations")
+ return create_async_engine(settings.database_url.get_secret_value(), pool_pre_ping=True)
+
+
+def build_session_factory(engine: AsyncEngine) -> async_sessionmaker:
+ return async_sessionmaker(engine, expire_on_commit=False)
diff --git a/backend/src/kf_oj/infra/messaging/__init__.py b/backend/src/kf_oj/infra/messaging/__init__.py
new file mode 100644
index 0000000..a7f11f0
--- /dev/null
+++ b/backend/src/kf_oj/infra/messaging/__init__.py
@@ -0,0 +1 @@
+"""Reserved for AMQP publishing/consuming adapters, confirms and retry transport."""
diff --git a/backend/src/kf_oj/modules/__init__.py b/backend/src/kf_oj/modules/__init__.py
new file mode 100644
index 0000000..204b989
--- /dev/null
+++ b/backend/src/kf_oj/modules/__init__.py
@@ -0,0 +1 @@
+"""Business modules; cross-module calls go through explicit public application interfaces."""
diff --git a/backend/src/kf_oj/modules/community/README.md b/backend/src/kf_oj/modules/community/README.md
new file mode 100644
index 0000000..374c288
--- /dev/null
+++ b/backend/src/kf_oj/modules/community/README.md
@@ -0,0 +1,6 @@
+# community
+
+负责题目讨论、题解、评论、作者归属、可见性及内容修改记录。
+题目和账号信息通过对应模块的公开接口查询,不直接操作其 ORM 表。
+技能社区的技能版本与发布语义归 `skills` 管理,本模块只提供可复用的内容/反馈能力。
+当前未实现社区接口。
diff --git a/backend/src/kf_oj/modules/community/__init__.py b/backend/src/kf_oj/modules/community/__init__.py
new file mode 100644
index 0000000..f2b7616
--- /dev/null
+++ b/backend/src/kf_oj/modules/community/__init__.py
@@ -0,0 +1 @@
+"""Problem discussions, solutions, comments and content visibility (PF-04)."""
diff --git a/backend/src/kf_oj/modules/identity/README.md b/backend/src/kf_oj/modules/identity/README.md
new file mode 100644
index 0000000..299e021
--- /dev/null
+++ b/backend/src/kf_oj/modules/identity/README.md
@@ -0,0 +1,5 @@
+# identity
+
+负责账号、登录凭证、会话与角色;向其他模块提供经过验证的用户身份和授权依据。
+后续 `public.py` 暴露身份查询接口,`service.py` 编排用例,`repository.py` 封装持久化。
+用户身份不能来自前端自报字段或模型参数。目前仅建立模块边界,未实现登录。
diff --git a/backend/src/kf_oj/modules/identity/__init__.py b/backend/src/kf_oj/modules/identity/__init__.py
new file mode 100644
index 0000000..03de82b
--- /dev/null
+++ b/backend/src/kf_oj/modules/identity/__init__.py
@@ -0,0 +1 @@
+"""Accounts, credentials, roles and trusted user identity (PF-02)."""
diff --git a/backend/src/kf_oj/modules/learning/README.md b/backend/src/kf_oj/modules/learning/README.md
new file mode 100644
index 0000000..43d3f90
--- /dev/null
+++ b/backend/src/kf_oj/modules/learning/README.md
@@ -0,0 +1,7 @@
+# learning
+
+负责学习证据、统计与代码差异、题库筛选、分层答疑、计划校验和反馈。
+读取其他业务域的授权公开接口,通过组合层向 `runtime` 注入受控业务工具。
+规则推荐属于业务模块;步骤执行、预算与检查点属于 `runtime`。
+基础设施重试不能算成学生失败;模型输出必须校验后才能保存业务结果。
+当前未接入模型,也未创建训练计划表。
diff --git a/backend/src/kf_oj/modules/learning/__init__.py b/backend/src/kf_oj/modules/learning/__init__.py
new file mode 100644
index 0000000..a270d0f
--- /dev/null
+++ b/backend/src/kf_oj/modules/learning/__init__.py
@@ -0,0 +1 @@
+"""Evidence, recommendations, tutoring, training plans and feedback (AI-01..05)."""
diff --git a/backend/src/kf_oj/modules/operations/README.md b/backend/src/kf_oj/modules/operations/README.md
new file mode 100644
index 0000000..9681a78
--- /dev/null
+++ b/backend/src/kf_oj/modules/operations/README.md
@@ -0,0 +1,5 @@
+# operations
+
+负责平台侧节点、任务积压、重试原因和审计查询的管理视图。
+对判题状态的操作必须调用协调器协议;观测模块不成为第二个状态写入入口。
+当前只有目录边界,节点登记、指标聚合和管理员接口待实现。
diff --git a/backend/src/kf_oj/modules/operations/__init__.py b/backend/src/kf_oj/modules/operations/__init__.py
new file mode 100644
index 0000000..e096dbd
--- /dev/null
+++ b/backend/src/kf_oj/modules/operations/__init__.py
@@ -0,0 +1 @@
+"""Read-only node/task observability and administrator views (PF-05)."""
diff --git a/backend/src/kf_oj/modules/problems/README.md b/backend/src/kf_oj/modules/problems/README.md
new file mode 100644
index 0000000..9b62478
--- /dev/null
+++ b/backend/src/kf_oj/modules/problems/README.md
@@ -0,0 +1,5 @@
+# problems
+
+负责题目逻辑身份、不可变版本、导入、标签、限制、发布状态与测试工件元数据。
+跨模块访问通过后续 `public.py`;大文件存取通过 `infra/artifacts` 的适配器。
+隐藏数据与公开样例分别授权。当前未创建题库表或导入接口。
diff --git a/backend/src/kf_oj/modules/problems/__init__.py b/backend/src/kf_oj/modules/problems/__init__.py
new file mode 100644
index 0000000..0fc7c32
--- /dev/null
+++ b/backend/src/kf_oj/modules/problems/__init__.py
@@ -0,0 +1 @@
+"""Problem catalog, immutable versions, import and publication (PF-02)."""
diff --git a/backend/src/kf_oj/modules/skills/README.md b/backend/src/kf_oj/modules/skills/README.md
new file mode 100644
index 0000000..a398830
--- /dev/null
+++ b/backend/src/kf_oj/modules/skills/README.md
@@ -0,0 +1,7 @@
+# skills
+
+负责声明式技能包、不可变版本、个人绑定和参数、启停及后续分享生命周期。
+包内容与用户私有学习档案分开存储;工具声明不能增加后端授权。
+`runtime` 只接收已经校验的固定版本和参数,不导入本模块实现。
+仓库根目录 `skills/` 保存未来平台维护的静态内置内容;用户绑定属于数据库业务记录。
+当前未建立包解析器,不执行任何插件脚本。
diff --git a/backend/src/kf_oj/modules/skills/__init__.py b/backend/src/kf_oj/modules/skills/__init__.py
new file mode 100644
index 0000000..044794b
--- /dev/null
+++ b/backend/src/kf_oj/modules/skills/__init__.py
@@ -0,0 +1 @@
+"""Skill definitions, immutable versions, user bindings and publication (SP-01..04)."""
diff --git a/backend/src/kf_oj/modules/submissions/README.md b/backend/src/kf_oj/modules/submissions/README.md
new file mode 100644
index 0000000..28d657c
--- /dev/null
+++ b/backend/src/kf_oj/modules/submissions/README.md
@@ -0,0 +1,6 @@
+# submissions
+
+负责提交幂等请求、不可变源码引用、创建初始评测、历史查询与学生可见结果。
+创建提交、评测和待投递事件必须处于同一事务。
+协调器拥有领取后的执行状态与最终结果写入权;本模块不能绕过协议覆盖终态。
+后续 `public.py` 提供授权后的学习证据入口。当前未实现提交接口。
diff --git a/backend/src/kf_oj/modules/submissions/__init__.py b/backend/src/kf_oj/modules/submissions/__init__.py
new file mode 100644
index 0000000..9672f09
--- /dev/null
+++ b/backend/src/kf_oj/modules/submissions/__init__.py
@@ -0,0 +1 @@
+"""Source snapshots, submission creation, evaluation references and queries (PF-03)."""
diff --git a/backend/src/kf_oj/runtime/__init__.py b/backend/src/kf_oj/runtime/__init__.py
new file mode 100644
index 0000000..293509b
--- /dev/null
+++ b/backend/src/kf_oj/runtime/__init__.py
@@ -0,0 +1,5 @@
+"""Reserved for bounded Agent execution, tool ports, budgets and checkpoints.
+
+Application tools are injected by the learning composition layer. This package
+does not import business modules, database adapters or any model SDK.
+"""
diff --git a/backend/src/kf_oj/workers/__init__.py b/backend/src/kf_oj/workers/__init__.py
new file mode 100644
index 0000000..26f6223
--- /dev/null
+++ b/backend/src/kf_oj/workers/__init__.py
@@ -0,0 +1 @@
+"""Independent process composition roots; task loops are implemented per milestone."""
diff --git a/backend/src/kf_oj/workers/bootstrap.py b/backend/src/kf_oj/workers/bootstrap.py
new file mode 100644
index 0000000..c9e9480
--- /dev/null
+++ b/backend/src/kf_oj/workers/bootstrap.py
@@ -0,0 +1,25 @@
+import argparse
+import json
+
+from kf_oj import __version__
+from kf_oj.core.settings import Settings
+
+
+def check_entrypoint(service: str) -> None:
+ parser = argparse.ArgumentParser(description=f"{service} process entrypoint")
+ parser.add_argument("--check", action="store_true", help="Validate configuration and exit")
+ args = parser.parse_args()
+ settings = Settings()
+ if not args.check:
+ parser.exit(2, f"{service}: task loop is not implemented; use --check\n")
+ print(
+ json.dumps(
+ {
+ "service": service,
+ "version": __version__,
+ "environment": settings.environment,
+ "status": "scaffold",
+ "consuming_tasks": False,
+ }
+ )
+ )
diff --git a/backend/src/kf_oj/workers/learning.py b/backend/src/kf_oj/workers/learning.py
new file mode 100644
index 0000000..a9fd012
--- /dev/null
+++ b/backend/src/kf_oj/workers/learning.py
@@ -0,0 +1,9 @@
+from kf_oj.workers.bootstrap import check_entrypoint
+
+
+def main() -> None:
+ check_entrypoint("learning-worker")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/backend/src/kf_oj/workers/outbox.py b/backend/src/kf_oj/workers/outbox.py
new file mode 100644
index 0000000..1e38949
--- /dev/null
+++ b/backend/src/kf_oj/workers/outbox.py
@@ -0,0 +1,9 @@
+from kf_oj.workers.bootstrap import check_entrypoint
+
+
+def main() -> None:
+ check_entrypoint("outbox-publisher")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/backend/tests/test_contracts.py b/backend/tests/test_contracts.py
new file mode 100644
index 0000000..250ab53
--- /dev/null
+++ b/backend/tests/test_contracts.py
@@ -0,0 +1,26 @@
+import json
+from pathlib import Path
+
+import pytest
+from jsonschema import Draft202012Validator, FormatChecker
+from pydantic import ValidationError
+
+from kf_oj.contracts.judge import JudgeTaskNotification
+
+ROOT = Path(__file__).resolve().parents[2]
+FIXTURES = ROOT / "tests/fixtures/contracts"
+SCHEMA = json.loads((ROOT / "contracts/schemas/judge-task.v1.schema.json").read_text())
+
+
+@pytest.mark.parametrize("path", sorted(FIXTURES.glob("judge-task.*.json")), ids=lambda p: p.stem)
+def test_shared_wire_examples(path: Path) -> None:
+ payload = json.loads(path.read_text())
+ validator = Draft202012Validator(SCHEMA, format_checker=FormatChecker())
+ if ".invalid-" in path.name:
+ with pytest.raises(ValidationError):
+ JudgeTaskNotification.model_validate(payload)
+ assert not validator.is_valid(payload)
+ else:
+ notification = JudgeTaskNotification.model_validate(payload)
+ validator.validate(payload)
+ assert notification.model_dump(mode="json") == payload
diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py
new file mode 100644
index 0000000..76a1d16
--- /dev/null
+++ b/backend/tests/test_health.py
@@ -0,0 +1,33 @@
+import asyncio
+
+import httpx
+
+from kf_oj.api.app import create_app
+from kf_oj.core.settings import Settings
+
+
+def test_liveness_without_database_or_queue() -> None:
+ settings = Settings(_env_file=None, environment="test", database_url=None, amqp_url=None)
+
+ async def request():
+ async with httpx.AsyncClient(
+ transport=httpx.ASGITransport(app=create_app(settings)), base_url="http://test"
+ ) as client:
+ return await client.get("/api/v1/health/live")
+
+ response = asyncio.run(request())
+ assert response.status_code == 200
+ assert response.json() == {"status": "ok", "service": "platform", "version": "0.0.1"}
+
+
+def test_production_disables_api_documentation() -> None:
+ app = create_app(Settings(_env_file=None, environment="production"))
+
+ async def request():
+ async with httpx.AsyncClient(
+ transport=httpx.ASGITransport(app=app), base_url="http://test"
+ ) as client:
+ for path in ("/api/docs", "/api/openapi.json"):
+ assert (await client.get(path)).status_code == 404
+
+ asyncio.run(request())
diff --git a/backend/uv.lock b/backend/uv.lock
new file mode 100644
index 0000000..34143de
--- /dev/null
+++ b/backend/uv.lock
@@ -0,0 +1,677 @@
+version = 1
+revision = 5
+requires-python = "==3.13.*"
+
+[[package]]
+name = "aio-pika"
+version = "9.6.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiormq" },
+ { name = "yarl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/63/56354526f2e6e915c93bee6e4dedb35888fe82d6bc1a19f35f5a77e795ff/aio_pika-9.6.2.tar.gz", hash = "sha256:c49e9246080dc8ffa1bb0e4aca407bf3d8ad78c3ee3a93df88b68fe65d7a49b9", size = 70851, upload-time = "2026-03-22T19:03:20.878Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/05/256fa313f48bed075056d13593b92ce804be05d75f4f312be24edb82860a/aio_pika-9.6.2-py3-none-any.whl", hash = "sha256:2a5478af920d169795071c9c09c7542cd8cdece60438cf7804533dcbcce93b7f", size = 56269, upload-time = "2026-03-22T19:03:19.558Z" },
+]
+
+[[package]]
+name = "aiormq"
+version = "6.9.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pamqp" },
+ { name = "yarl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6c/0e/db90154d52d399108903fe603e5110a533c42065180265dd003788264080/aiormq-6.9.4.tar.gz", hash = "sha256:0e7c01b662804e1cc7ace9a17794e8c1192a27fc2afa96162362a6e61ae8e8ef", size = 49232, upload-time = "2026-03-23T09:18:19.493Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6c/48/1ce3773f392f02ceda37aee168fade9d725483a9592c202d06044cd093ff/aiormq-6.9.4-py3-none-any.whl", hash = "sha256:726a8586695e863fba68cf88842065ab12348c9438dcebdfc9d0bddaf6083277", size = 32166, upload-time = "2026-03-23T09:18:17.523Z" },
+]
+
+[[package]]
+name = "alembic"
+version = "1.20.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mako" },
+ { name = "sqlalchemy" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ed/aa/02910bdb8e2f1444f6654d5b296cd827d126f82209050ee7b1000f92ac4b/alembic-1.20.0.tar.gz", hash = "sha256:db505480647bc60386c5369402f4a57a506b7539c9e9ef5e270d45cbbe4939bf", size = 2093272, upload-time = "2026-09-11T19:09:11.126Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/27/78a89b55b0904d222183164e079b4ca56208e94eff1d35ad1f1ad5be9b06/alembic-1.20.0-py3-none-any.whl", hash = "sha256:77eb101048d95f982c0353e9233404889dcd7a6fc244c107836c0e2fc9cf7d9d", size = 268719, upload-time = "2026-09-11T19:09:12.88Z" },
+]
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.15.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
+]
+
+[[package]]
+name = "asyncpg"
+version = "0.31.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" },
+ { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" },
+ { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" },
+ { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" },
+ { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "26.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.7.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "fastapi"
+version = "0.142.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "opentelemetry-api" },
+ { name = "pydantic" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/56/4f/f7c30a73127e0a8bbffe788369b8359e530b01ae06e2757936fa35bc5e6d/fastapi-0.142.2.tar.gz", hash = "sha256:06366626f2e70576367714d9ab2fe8472e6c8456dba69b399f9f797ab5e92570", size = 466721, upload-time = "2026-09-30T09:51:11.277Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/b6/78aaf9141fb46742928c113f3cf6ef2259d538cb02b604b7656c1dc9883c/fastapi-0.142.2-py3-none-any.whl", hash = "sha256:bd5f4d81f1e93a88bcd77caf4dfe3c2dbffc3805407a0007e9a114c18b3a670b", size = 144409, upload-time = "2026-09-30T09:51:09.8Z" },
+]
+
+[[package]]
+name = "greenlet"
+version = "3.5.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3e/6e/0091f175ccd02b02bc8811bbcbcc6ac2e980be116e3b2f7a736ca322bf84/greenlet-3.5.6.tar.gz", hash = "sha256:8e67c43bdfc88d5fee6db0d3e40175b362fc95fb85f0412d233b9b203c53a575", size = 207653, upload-time = "2026-09-14T15:42:51.806Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/a1/e720a38852366c589e1a46cf570b886507ad2cf591050c203365638baab0/greenlet-3.5.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f96f0e30b5a95c7631b12bfe214cbc90ec8fe8cfa36920596c10514a65743519", size = 294627, upload-time = "2026-09-14T14:24:40.102Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/c3/58187858df41354a11e6a55b421e7af9059798abdab3a384cc51b8567c38/greenlet-3.5.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c75116c9de79949de23006e2d9b35ee82874c594fcf5c0311b439acaa14b8441", size = 614356, upload-time = "2026-09-14T15:12:03.399Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/b9/3a7e67d5f05c9760b1ad411fa52264bd69cc08e22a2ebfb4018b90628ced/greenlet-3.5.6-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cad5782f93f7f738b62c6527b6f32a60694d924029f299a8b524758cfa53d815", size = 626756, upload-time = "2026-09-14T15:20:44.269Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/7c/40400455f5b5a65bb83e94fde66d1be9e5ec518638113f8083ace746c309/greenlet-3.5.6-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a93ee7c6e8fd0f8a83525a51bd777be57ee17787e91d805bd8d6faf9dcada18e", size = 632632, upload-time = "2026-09-14T15:25:07.813Z" },
+ { url = "https://files.pythonhosted.org/packages/85/cb/ab0c123c514ed4e94c0dc9ee2e86362633e6b998cfc05de7fc9ac2eb9690/greenlet-3.5.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f98e8215e172f567ce80eeaed9107fb4d32b6c44f26983d9b8334658136a205a", size = 623779, upload-time = "2026-09-14T14:36:01.104Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/67/1f35cff30a6c51c3f23b63d4afcc7313ab4f97490ba3676fa78178984b27/greenlet-3.5.6-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7f731ebac68ea06d628658295cb2d217b10186329fcf9a3b6a149045059bf92e", size = 434933, upload-time = "2026-09-14T15:28:38.858Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/26/fda8a5a06e7073333ccb038133c5893b9e0c4fe29d5992a17e83c241bc6e/greenlet-3.5.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df19e2d0b1620039af5102563fbd96e8938c7f5c3f5828528d641d9fc585525e", size = 1584930, upload-time = "2026-09-14T15:10:08.234Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/37/50f8813163148d6234e08b23dcad6a9e37f01d148c8ec976e4c44ea2d918/greenlet-3.5.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06c0e933290fba8ffe53ead4ae1b8044b0e9754b75cebf381aa2bc3e50d82fac", size = 1647590, upload-time = "2026-09-14T14:35:51.173Z" },
+ { url = "https://files.pythonhosted.org/packages/86/da/b7669b09586365654083a62bd0724cf06cb74bd5085a15cdd161271f992f/greenlet-3.5.6-cp313-cp313-win_amd64.whl", hash = "sha256:5b602b4201b965a8354d74e232364a66ff243dd142e350d035f46169bb36e13d", size = 324086, upload-time = "2026-09-14T14:23:48.428Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/5d/c9663cfe84a2a9e0aa96f066f5b0594c227ea4c647511e087e2e11d4ac0a/greenlet-3.5.6-cp313-cp313-win_arm64.whl", hash = "sha256:876077e7ebb8c84ed068e2b23d4c62ebb010d60df84b9591af1be2f39010ffb2", size = 308211, upload-time = "2026-09-14T14:28:01.634Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "kf-oj-platform"
+version = "0.0.1"
+source = { editable = "." }
+dependencies = [
+ { name = "aio-pika" },
+ { name = "alembic" },
+ { name = "asyncpg" },
+ { name = "fastapi" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "sqlalchemy", extra = ["asyncio"] },
+ { name = "uvicorn" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "httpx" },
+ { name = "jsonschema" },
+ { name = "pytest" },
+ { name = "ruff" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "aio-pika", specifier = ">=9.5,<10" },
+ { name = "alembic", specifier = ">=1.15,<2" },
+ { name = "asyncpg", specifier = ">=0.30,<1" },
+ { name = "fastapi", specifier = ">=0.115,<1" },
+ { name = "pydantic", specifier = ">=2.10,<3" },
+ { name = "pydantic-settings", specifier = ">=2.7,<3" },
+ { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0,<3" },
+ { name = "uvicorn", specifier = ">=0.34,<1" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "httpx", specifier = ">=0.28,<1" },
+ { name = "jsonschema", specifier = ">=4.23,<5" },
+ { name = "pytest", specifier = ">=8.3,<10" },
+ { name = "ruff", specifier = ">=0.11,<1" },
+]
+
+[[package]]
+name = "mako"
+version = "1.4.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5a/09/e07c4b5579a79f4b16f8d4f29f6c54514ac787c4ad506b8c4f28a0e6b0bf/mako-1.4.3.tar.gz", hash = "sha256:cd6537fe88d5fec315c55c2f8529bc4ce7a9a352ad7db3eeaa6a66e2dd4ec37a", size = 412799, upload-time = "2026-09-22T20:54:31.509Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6d/a0/053d6af3e8f871e0073b4a36732d9e65be77a72e5434c31b94f6af78a6bb/mako-1.4.3-py3-none-any.whl", hash = "sha256:723296007c870bfd6b3f0c3230dba7198096e5269297ebf5e4eff9e7ffa39d4f", size = 80164, upload-time = "2026-09-22T20:54:33.128Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6d/18/4bc5ba32499e87bb2b0ef5b3a9bb9c00a131fa961ddf0be548cb550f548b/markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1", size = 12786, upload-time = "2026-10-02T23:05:08.709Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/6f/17f0c099bf25f3e31e63cc19244d9f6af861a9a4ab778c203997903cfdd0/markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1", size = 12510, upload-time = "2026-10-02T23:05:09.93Z" },
+ { url = "https://files.pythonhosted.org/packages/11/af/1a141081b905036ee904ec4bd945e1f70b4e1b32d33c4e59e8cf1d58b247/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96", size = 11583, upload-time = "2026-10-02T23:05:10.884Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/0a/a89385ae590232622a03e091805cff12f24fabe6c11e0e8bae096cece81c/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148", size = 11801, upload-time = "2026-10-02T23:05:11.913Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/85/ea548dc013962eb73653124bc595635fbf9e0fa41d1f181a967ccb784dfb/markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e", size = 11443, upload-time = "2026-10-02T23:05:12.887Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/72/15f2e5ec9cf2eb00d5cdfe968d94e4156a7bd7303832c3f3b2c403a36839/markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248", size = 11665, upload-time = "2026-10-02T23:05:13.829Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/e0/4030bea613677e333c8a2c901fd405055f657f9d06acba5b7357984b6ef7/markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72", size = 12061, upload-time = "2026-10-02T23:05:14.807Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/a5/28b76a7449eb702966b88bef599e2360b411fbb3afeee8fe560939be06ec/markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2", size = 24395, upload-time = "2026-10-02T23:05:15.909Z" },
+ { url = "https://files.pythonhosted.org/packages/07/6c/21232811afc3a063b5e934b1ae2efda52f46154ec382f585149c020e61fe/markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85", size = 27183, upload-time = "2026-10-02T23:05:16.976Z" },
+ { url = "https://files.pythonhosted.org/packages/14/38/6ccdfa5b59049cb36fb80cbc80aee9cf1fc9bb77d1335ad435f2070b08cf/markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde", size = 24772, upload-time = "2026-10-02T23:05:18.209Z" },
+ { url = "https://files.pythonhosted.org/packages/63/e0/cec6865dfe88cb48fedd4b20aed6af5158e41092adcbf3e028bcc6ec2108/markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6", size = 23017, upload-time = "2026-10-02T23:05:19.286Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/76/6ed4940bb7648a9aac457c14f870cfdd5105f139a0fb1f29cd61fafa47d1/markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f", size = 22036, upload-time = "2026-10-02T23:05:20.352Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/4f/ed476226d4fe46a09090a36025bf319296810028df55eb12f1253b540f3a/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39", size = 23786, upload-time = "2026-10-02T23:05:21.576Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/35/66ff30450e35ef5fba9ebc930c9411747e537fd9447b65e44f5007e2b84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee", size = 22672, upload-time = "2026-10-02T23:05:22.922Z" },
+ { url = "https://files.pythonhosted.org/packages/32/0b/72f45ce4b4efcbca4b80cf1b06703eff0be8d37e82abb78f66c85a7ead1e/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2", size = 24107, upload-time = "2026-10-02T23:05:24.175Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/03/71776e5fdcba04614b384cc102e8a4198208579d896fd1394cb7cb9aa900/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46", size = 21615, upload-time = "2026-10-02T23:05:25.215Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/5f/801ce02a02e7aee0f784b1ec7843026178f6adeb9c93ac67eb1992a9a84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17", size = 23076, upload-time = "2026-10-02T23:05:26.423Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/85/c43776625428f3bb4a61e8633940400e3efe6409e3c6f5bff26de5e45618/markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0", size = 14118, upload-time = "2026-10-02T23:05:27.716Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/36/163da64de88a13db79214ef75fa041be7fa13bdb42261cf5b7484de14bfb/markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5", size = 14313, upload-time = "2026-10-02T23:05:28.749Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/a8/9b662783ffaa1149221432a923cee562f78b9cbbb8baa3df9b3753e63e1e/markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc", size = 14189, upload-time = "2026-10-02T23:05:29.917Z" },
+]
+
+[[package]]
+name = "multidict"
+version = "7.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c4/64/642465a4827331a98ba4ae29f97658be25d1bdcb868872a2f78f7482b507/multidict-7.0.0.tar.gz", hash = "sha256:a7fcd089a0af2e0ef053c0d39c22c9ebf2434dddcb91034fd2f59ec99623788e", size = 280561, upload-time = "2026-09-26T19:36:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/6e/c388fccb88dd6cb41ce2454e420c869fb0629ce10bfe12e4cad578ba940d/multidict-7.0.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:15d9ad70615ce1b37c124445145f071ba4eaa88cb21b49a6b81727cc85aefc37", size = 92310, upload-time = "2026-09-26T19:32:24.366Z" },
+ { url = "https://files.pythonhosted.org/packages/58/2f/33505a5c498613d53ab1d87b3c9425f6757afc7e48c2409f7aa001d0cdc1/multidict-7.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:313e336c75a53447cd943d93235c4702428b9740cb681e7174e398ba1f3f2918", size = 85114, upload-time = "2026-09-26T19:32:25.975Z" },
+ { url = "https://files.pythonhosted.org/packages/76/f5/27b19597821cb80d082b649e5e6e01033aed5e0eebdbbc6a644eb49515a7/multidict-7.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a4838959217e2652ef7dce1d01bb370aed3e5b694eb7a8258fa1cc50beef08ca", size = 86952, upload-time = "2026-09-26T19:32:27.429Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/58/a5e44d5453c8bb295979d242cfd3beee06f6ab9f6c1c06a13c2191b792b1/multidict-7.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bd6520538e19fe104c3b74cc64767d6f8959dd2c9b8d59da62d83641ea646deb", size = 147356, upload-time = "2026-09-26T19:32:28.816Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/c0/4da412a7267ac85759300a078f3bdad986e18a30c1ead8b74d2e3b335a2b/multidict-7.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e9bf055becb08ff797d5cf80b760b726b8babb39046564d2b8481bbe6504bce", size = 88022, upload-time = "2026-09-26T19:32:30.44Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/46/bebeb7df10298d390942e61bc7fc48472183ee4f5a0b7c285d93369b6874/multidict-7.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8e443cab381f8b6d24950982ae508600d464412cbdcf78e263cf8cd9b8372d68", size = 87046, upload-time = "2026-09-26T19:32:31.881Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/d3/38b254c21cb8e9a65f8751ad8ba0a6f9baad75e3b6d3e559d1d05db26482/multidict-7.0.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e45bb7b9c850f216c0081dab8c6862fdec6b8be1c800b2a1aa4d1e56dcfacb54", size = 488729, upload-time = "2026-09-26T19:32:33.338Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/28/19d66c476cc44fffdeacd5227a59046f94612075008069286bdbb9d3fa35/multidict-7.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2f742000b47d806e5798f5ef0cdb2ed43d822fc913331a33ed1a40515495a7", size = 496708, upload-time = "2026-09-26T19:32:35.286Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/66/3bb2d2cdf67742b002985906785b0db1584ca2e67ddb3677f6e9614321d6/multidict-7.0.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70e28954ae5ae3c1806aa6d85bbb26ef7ab73017228eb236a3f245e24720bb34", size = 458261, upload-time = "2026-09-26T19:32:37.027Z" },
+ { url = "https://files.pythonhosted.org/packages/af/5c/c8ca15c191f957e8d86ec65148ed1e8ca189f456e66153e23c4ae610649a/multidict-7.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c41ccfa34b152146ccf05e8da753a85f8e67437477f8a00783aa427a132abd31", size = 511932, upload-time = "2026-09-26T19:32:39.025Z" },
+ { url = "https://files.pythonhosted.org/packages/14/ff/0665b2c3d5e42165a96fffc4c34c3d5fa66d0819d82b9bc2977b9a6aa1ed/multidict-7.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9a1de5f1da3ae056b45b6599a53dba6c86879bdfa5c3f15d7dde99f6cfbc03c6", size = 529152, upload-time = "2026-09-26T19:32:41.068Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/f7/967875e5d1f908613cfcd5d9d078011168acb88a128028b81a0de43e8aa6/multidict-7.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:094852ebc620a10ee0e11f36a68e016d73596c0f5ed8351a9a52fcff7d449ad6", size = 506337, upload-time = "2026-09-26T19:32:42.839Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/f9/56f04cf560bf736c23ba9cacc1ae3fcd2bbb591ba4f166c4bc08f5fead61/multidict-7.0.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03cc4027103bb61feac20336a15fced1a6a369fed6a61c516353921207e4578c", size = 448144, upload-time = "2026-09-26T19:32:44.556Z" },
+ { url = "https://files.pythonhosted.org/packages/07/ea/ff7978bb38f2e2908cf7fc80061175cbfacdcada248dc48fb79a10c0e92b/multidict-7.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0fef8622896443fd0bdf39586391c2162ac05004ba4c9918827dfd2f79da750c", size = 485535, upload-time = "2026-09-26T19:32:46.439Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/6c/1e9a6457587da0a31b3ef31ca0a40624877ca29408c8a7385260a3d45cd9/multidict-7.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1847d33ca198aba3bf5fad036a7e0376cca325e7ec89f26362f9aa48994d8146", size = 471078, upload-time = "2026-09-26T19:32:48.166Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/12/ee11505f0ae23a344544010485db982290c0e6105caefd74ce3830c315ca/multidict-7.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b5c93f582f8a973ea803c46097729e34969683bc99f99fc00321b1f76317e81", size = 490764, upload-time = "2026-09-26T19:32:49.861Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/8d/1e954f7cbb1f2080c2712b6a4cb6ce196037c06c686ae030003465b65ad7/multidict-7.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7c717376f801dd1ee102b292e0a4ca6037e948a8710316e64e698fe39eeb1307", size = 500752, upload-time = "2026-09-26T19:32:51.756Z" },
+ { url = "https://files.pythonhosted.org/packages/da/54/0f3899f51364f502219192b8c03da123c8618e9709499194ecd8e4143cc9/multidict-7.0.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c6d6e73b9f7b70558de5624b036815464a7ead4671fe2c3a4b3ce444c240cba6", size = 444706, upload-time = "2026-09-26T19:32:53.515Z" },
+ { url = "https://files.pythonhosted.org/packages/54/03/8edc3cb05a35185a4910582a50aa4dd342cceda3b26b2053dbd004adc9e8/multidict-7.0.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:18f627c69d041ae17d8fb45dbc63e192a2afc3ba062195441b5a50e93c50ae68", size = 511149, upload-time = "2026-09-26T19:32:55.281Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b4/4234b76f97b2966d1de2f3507f5dc9af4072e2ed16cfb476855490e34ae9/multidict-7.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8477e14353097077e1011464794d11e1d7559e821ab7c612cf7c06e7826ee1ba", size = 501451, upload-time = "2026-09-26T19:32:57.073Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/a9/fe7965b74cb3cf822d3e88063f13e3eff2600c36022f69c603b5b52f3e21/multidict-7.0.0-cp313-cp313-win32.whl", hash = "sha256:2efca6de4fbefc12f2aaac2d5e1c176a3864d22250246b44fe3823e3634f06f8", size = 76414, upload-time = "2026-09-26T19:32:58.84Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/c7/a4960e81ba8a3a9698011d1ac92d4d11ce8c9bb0ed6d235de76c49cae3d1/multidict-7.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:188a7f89a1935a138237088bff22400bacd25d80fb0d3a8a26fab25961df2239", size = 87005, upload-time = "2026-09-26T19:33:00.576Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/66/7f6b0898c895f2b39ebaa3fc34d32537d263aa5c74809ee747b5e705da80/multidict-7.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:debb701a0436c96f2717c6156428a2c8bce2559236e120a674d503aeed678aa3", size = 85466, upload-time = "2026-09-26T19:33:02.072Z" },
+ { url = "https://files.pythonhosted.org/packages/46/a4/a3e9e72ed0f99587e9e96c3de72b743e1385e5e2b15fb583d8dfc05ffffb/multidict-7.0.0-py3-none-any.whl", hash = "sha256:1ba6a45450243bd8953223adff7183fce5e3f9f3556a03bd17a674caaa046215", size = 26592, upload-time = "2026-09-26T19:36:09.764Z" },
+]
+
+[[package]]
+name = "opentelemetry-api"
+version = "1.45.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1f/dc/e12c1fe1ed8a7b7149777127b1a0e12ce5bd5a81d97408bedc2128c260f5/opentelemetry_api-1.45.0.tar.gz", hash = "sha256:711ede81773c8025c2c03dac0450bc89f3d30aea6eabcc815c570d4e35a963f7", size = 72115, upload-time = "2026-09-25T12:32:28.399Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/b9/040d1a1c7836922828e6480cd2366bb8fe0ebf75b413d2bb51a9b0e7f78f/opentelemetry_api-1.45.0-py3-none-any.whl", hash = "sha256:80e068aba7cd56c8b58512d6a36f8d25cb1dfaa0c0a4cc1c938ccf9f362d9cb3", size = 60020, upload-time = "2026-09-25T12:32:03.192Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
+]
+
+[[package]]
+name = "pamqp"
+version = "3.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fb/62/35bbd3d3021e008606cd0a9532db7850c65741bbf69ac8a3a0d8cfeb7934/pamqp-3.3.0.tar.gz", hash = "sha256:40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b", size = 30993, upload-time = "2024-01-12T20:37:25.085Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ac/8d/c1e93296e109a320e508e38118cf7d1fc2a4d1c2ec64de78565b3c445eb5/pamqp-3.3.0-py2.py3-none-any.whl", hash = "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0", size = 33848, upload-time = "2024-01-12T20:37:21.359Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "propcache"
+version = "0.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b3/9a/9fbf4e4ec0c2d7f1c32519fff782ef467859b8faa9fbc5331a96f6395d43/propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558", size = 61545, upload-time = "2026-09-16T00:17:14.386Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/4c/3b1365d58a667689e067e13d055fcd92bdf8d9a2fca3d9201b47ed5b3631/propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba", size = 85290, upload-time = "2026-09-16T00:14:52.342Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/61/5f9c29c3aa67c30238c4eadf95149b1d983a48f69b86b0cff927a7d6df13/propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0", size = 50027, upload-time = "2026-09-16T00:14:53.67Z" },
+ { url = "https://files.pythonhosted.org/packages/25/7d/c1ab1ef09e9d4d835be5d58c0a32a1e1de8397abaa4e502a9d4141328cad/propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111", size = 51425, upload-time = "2026-09-16T00:14:54.826Z" },
+ { url = "https://files.pythonhosted.org/packages/73/36/0093091ebb270fcd1bc1f6e095f93b2e0ed7f1011c28837dc2dbe5f96b99/propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c", size = 233595, upload-time = "2026-09-16T00:14:56.09Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8f/0de9d4c8e05ce0be71b436919a216bd7fc5cc6e2691c0602295efb22b9ed/propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867", size = 240318, upload-time = "2026-09-16T00:14:57.674Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/71/2b35e91455209b85ee98f7859583e0814fab57d3af0f2381aaee34c37304/propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9", size = 246649, upload-time = "2026-09-16T00:14:59.352Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/74/08e6c1faf26ee2732023a3828787ba535557122774f4a386b1f715cbd8e0/propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572", size = 234316, upload-time = "2026-09-16T00:15:00.696Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/9a/08385733c9321c9bb78039d3ff31045e4fca962d9665023c4eb70f998819/propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8", size = 204666, upload-time = "2026-09-16T00:15:02.019Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/f4/e87bc7629af9a14a752b218764a78742d73c2c563ac58315da6841f0cbe4/propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894", size = 225900, upload-time = "2026-09-16T00:15:03.394Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/6d/11014938d3fe9bea2ea2dcf930f26ed565bfb2f5be3c756362ea48c92636/propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9", size = 219988, upload-time = "2026-09-16T00:15:04.811Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/72/fbf17c589f92c0b3bbf6709a425661f8ef2ed0d46b38985a7d7b5a0f6b91/propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47", size = 233611, upload-time = "2026-09-16T00:15:06.498Z" },
+ { url = "https://files.pythonhosted.org/packages/55/7e/dbd637572a279692e5518d117274a9331bf5faac59f191d30e82521a3ec7/propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c", size = 204333, upload-time = "2026-09-16T00:15:07.961Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/5a/f99c92068f1e0f5c886899ce0e4a619db376ca98c5279d93f95bd86906af/propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1", size = 235177, upload-time = "2026-09-16T00:15:09.334Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/28/95456fabd2daf6be89049a13fbf03341756014d2959c83d12957d4c49694/propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c", size = 228982, upload-time = "2026-09-16T00:15:10.729Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/bb/df90f62c9cf7c93ea235f6f9405143bba802914607317266dd81fc8d737e/propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25", size = 42611, upload-time = "2026-09-16T00:15:12.111Z" },
+ { url = "https://files.pythonhosted.org/packages/01/bc/e0a7b84af04ec02d73a48aa71f091e1e4a2107e3074b7ce12195b66901f4/propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77", size = 45342, upload-time = "2026-09-16T00:15:13.519Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/70/50b031cafe72a5c1878b903ee87303f71313345566bf3d6ec202e5ddc9ec/propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177", size = 42408, upload-time = "2026-09-16T00:15:14.788Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/cd/785c64ed382f3f04201870267b02783f63b4678c2acfddc177a3ebcc2727/propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468", size = 16338, upload-time = "2026-09-16T00:17:13.106Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.13.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.46.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" },
+ { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" },
+ { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" },
+ { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" },
+ { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" },
+ { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.15.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.21.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "2026.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" },
+ { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" },
+ { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" },
+ { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" },
+ { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" },
+ { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.16.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" },
+ { url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" },
+ { url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" },
+ { url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" },
+ { url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" },
+ { url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" },
+]
+
+[[package]]
+name = "sqlalchemy"
+version = "2.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/02/4b/81d972a46c9f1d978af1795e2abc988855c711453552cb45d75f6e4abbf5/sqlalchemy-2.1.3.tar.gz", hash = "sha256:ade5281df06038c6394f532590d592d3381ee5d11b9e0006a2570aef41145118", size = 10525778, upload-time = "2026-10-02T22:57:00.308Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1a/71/68c29931fb2c5178241f7d0c63dbdd800ad65685eac70d7f2117145b5e3c/sqlalchemy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:260bdd8baa1dc631d76d14b26ad009ebbb665b4469666afef8dcaa1f964016cb", size = 2457479, upload-time = "2026-10-03T02:39:19.741Z" },
+ { url = "https://files.pythonhosted.org/packages/94/21/5dbd91816b2cdb367bca3195f300ffeca916762e088d6924fc33a18f50ad/sqlalchemy-2.1.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fc01a8047d6a4f4e997441994f07af79fdd470fc80669751e32f91153d6b491", size = 4582199, upload-time = "2026-10-02T23:37:44.508Z" },
+ { url = "https://files.pythonhosted.org/packages/32/d1/79962ce06d6cd34516def113f5170184fa9401ebddc59fd6cb18f6e919f3/sqlalchemy-2.1.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b1b3ff10eba0f9290142375e667351c99d24ee7fcff6e0f85bd8be9d39b50a17", size = 4632923, upload-time = "2026-10-02T23:35:55.318Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/94/03e98031dc9a1738c580c3bf01beeafe695f247eb647f4e8dda3fab2454f/sqlalchemy-2.1.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:efb1ebf695e80d9a69f5bbff9d812d2491fd168b27d6b42ad4c8a4d3d6faaef1", size = 4300224, upload-time = "2026-10-02T23:46:45.495Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/1f/388c87607eb6c7abee4866355eeebb6221b3bcb413a64ce4a5cfc3edd22f/sqlalchemy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a95754928d47b37ea5c4b5180101db2afabef1da0dd5cd0c573b547d5d036ff0", size = 4507170, upload-time = "2026-10-02T23:38:29.697Z" },
+ { url = "https://files.pythonhosted.org/packages/57/73/5515a3f555d31748564fbeb0b9e0b6ab7a647bf2ea0cdb6d233f20f710e3/sqlalchemy-2.1.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7606b413730ac6002930faa96f52195ed6969040b78071a3336370661f5e9e09", size = 4301439, upload-time = "2026-10-02T23:46:47.845Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/a0/0eae9b96f6f8a09f3e5d388f6fc5d6fe8d5ff2aced2a5c75bea7333d445e/sqlalchemy-2.1.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:76c3d5c7ada0925ff12e8c2395c547f88fcfccc13368ff8e8dd55ab5e28dcdab", size = 4594441, upload-time = "2026-10-03T02:46:16.928Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/b1/b6d681015414ce9048c07ef3ac49b693c1b8373de4716ee4bdeff4ec19a0/sqlalchemy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:cb6fc5c856e8f1c0228bf74b636c0a03a6c12dd502ced7aa82ced21dcdc7f547", size = 2371547, upload-time = "2026-10-03T02:49:23.549Z" },
+ { url = "https://files.pythonhosted.org/packages/56/ad/35aa4d737016640c6da7573a4bcd9d91b59a87359bc6df075627963a4781/sqlalchemy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:82d4c63d338737d593f1d29c4dae0a124577a9a76186d90b5c0f9ec9cbeeab35", size = 2421558, upload-time = "2026-10-03T02:49:25.448Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/42/0dcc7cd92a3f4773376a5488b6ce0d6d75d2d029ad7e7f9e0ecc5fb48fcd/sqlalchemy-2.1.3-cp313-cp313-win_arm64.whl", hash = "sha256:0ab6f42761cc5c48d2f3e8a70810d10630d8222a51a7111cf9b3a48f22c3f148", size = 2382507, upload-time = "2026-10-03T02:42:42.812Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/12/23d8fb7ed90e5b674006ace415ede1c60cdfc38ace50968b59ef42c22754/sqlalchemy-2.1.3-py3-none-any.whl", hash = "sha256:6f0cf4debd86cb5623a50310784bd005c215761ca7f618d0d4f46193d033675a", size = 2052603, upload-time = "2026-10-03T02:34:19.115Z" },
+]
+
+[package.optional-dependencies]
+asyncio = [
+ { name = "greenlet" },
+]
+
+[[package]]
+name = "starlette"
+version = "1.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7b/2b/3850dc6bf7ef71b088962eba31dafc6cffd2f96e577ebb0bb316df96da3e/starlette-1.7.0.tar.gz", hash = "sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d", size = 2736246, upload-time = "2026-09-23T07:30:26.35Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4e/d6/1ec1b290f9e0fb067899b61e1d37a30c923068bad260b216dbe37a7d2967/starlette-1.7.0-py3-none-any.whl", hash = "sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e", size = 78980, upload-time = "2026-09-23T07:30:24.567Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.54.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" },
+]
+
+[[package]]
+name = "yarl"
+version = "1.25.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "multidict" },
+ { name = "propcache" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" },
+ { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" },
+ { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" },
+ { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" },
+ { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" },
+ { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" },
+ { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" },
+ { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" },
+]
diff --git a/contracts/README.md b/contracts/README.md
new file mode 100644
index 0000000..054fe8d
--- /dev/null
+++ b/contracts/README.md
@@ -0,0 +1,14 @@
+# 跨服务契约
+
+`schemas/` 保存版本化 JSON Schema,`openapi/` 保存平台 API 快照。
+共享合法/非法样例位于 `tests/fixtures/contracts/`,Python 与 Rust 必须共同验证。
+
+目前只固定最小判题通知:`schema_version=1`、`kind=judge.evaluation`、UUID 格式的
+`evaluation_id`。它不包含源码、隐藏数据、租约或执行授权;消费者仍须向协调器领取。
+未知版本、未知类型及未知字段均拒绝。
+
+`make contracts` 从 Python 模型生成 Schema/OpenAPI,并生成前端 API 类型。
+`make check` 检查快照未过期及两种语言对样例的接受/拒绝一致。
+执行请求/报告、评测状态机、租约与结果提交契约在 JS-01/DR-01 中逐项补充。
+
+生成器 `export.py` 是开发工具,允许读取 API 组合层;运行期的契约模型禁止依赖业务模块。
diff --git a/contracts/openapi/platform.v1.json b/contracts/openapi/platform.v1.json
new file mode 100644
index 0000000..4401d71
--- /dev/null
+++ b/contracts/openapi/platform.v1.json
@@ -0,0 +1,62 @@
+{
+ "components": {
+ "schemas": {
+ "HealthResponse": {
+ "additionalProperties": false,
+ "properties": {
+ "service": {
+ "const": "platform",
+ "title": "Service",
+ "type": "string"
+ },
+ "status": {
+ "const": "ok",
+ "title": "Status",
+ "type": "string"
+ },
+ "version": {
+ "title": "Version",
+ "type": "string"
+ }
+ },
+ "required": [
+ "status",
+ "service",
+ "version"
+ ],
+ "title": "HealthResponse",
+ "type": "object"
+ }
+ }
+ },
+ "info": {
+ "description": "工程骨架。当前只提供进程存活检查,业务接口逐项实现。",
+ "title": "KF-OnlineJudge Platform API",
+ "version": "0.0.1"
+ },
+ "openapi": "3.1.0",
+ "paths": {
+ "/api/v1/health/live": {
+ "get": {
+ "description": "Check process liveness only; does not attest DB, queue or judge readiness.",
+ "operationId": "liveness",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HealthResponse"
+ }
+ }
+ },
+ "description": "Successful Response"
+ }
+ },
+ "summary": "Liveness",
+ "tags": [
+ "health"
+ ]
+ }
+ }
+ }
+}
diff --git a/contracts/schemas/judge-task.v1.schema.json b/contracts/schemas/judge-task.v1.schema.json
new file mode 100644
index 0000000..64475fb
--- /dev/null
+++ b/contracts/schemas/judge-task.v1.schema.json
@@ -0,0 +1,30 @@
+{
+ "$id": "urn:kf-oj:judge-task:v1",
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "additionalProperties": false,
+ "description": "A wake-up notification, not execution authority or the source of task state.",
+ "properties": {
+ "evaluation_id": {
+ "format": "uuid",
+ "title": "Evaluation Id",
+ "type": "string"
+ },
+ "kind": {
+ "const": "judge.evaluation",
+ "title": "Kind",
+ "type": "string"
+ },
+ "schema_version": {
+ "const": 1,
+ "title": "Schema Version",
+ "type": "integer"
+ }
+ },
+ "required": [
+ "schema_version",
+ "kind",
+ "evaluation_id"
+ ],
+ "title": "JudgeTaskNotification",
+ "type": "object"
+}
diff --git a/deploy/.env.example b/deploy/.env.example
new file mode 100644
index 0000000..844ded3
--- /dev/null
+++ b/deploy/.env.example
@@ -0,0 +1,10 @@
+# Development-only values; copy to deploy/.env before make infra-up.
+POSTGRES_USER=kf_oj
+POSTGRES_PASSWORD=local-dev-only
+POSTGRES_DB=kf_oj
+POSTGRES_PORT=5432
+RABBITMQ_USER=kf_oj
+RABBITMQ_PASSWORD=local-dev-only
+RABBITMQ_VHOST=kf_oj
+RABBITMQ_PORT=5672
+RABBITMQ_MANAGEMENT_PORT=15672
diff --git a/deploy/README.md b/deploy/README.md
new file mode 100644
index 0000000..c91f14e
--- /dev/null
+++ b/deploy/README.md
@@ -0,0 +1,21 @@
+# 本地开发依赖
+
+`compose.dev.yml` 只提供 PostgreSQL 和 RabbitMQ,端口绑定 localhost。
+先复制 `.env.example` 为 `.env`,然后在仓库根目录运行:
+
+```bash
+make infra-config
+make infra-up
+make infra-down
+```
+
+`infra-down` 保留数据卷;不要把示例口令用于正式部署。
+API 存活检查和前端预览不依赖这些容器,可以先独立启动。
+该 Compose 不运行判题 Worker 或不可信代码。
+
+镜像固定具体维护版本(PostgreSQL 18.6、RabbitMQ 4.3.6),正式实验前另锁镜像摘要。
+配置检查只验证 Compose 结构;成功启动、实际健康与备份恢复需要单独验证。
+后续受控主机服务和最小权限沙箱启动器配置放在本目录。
+
+官方镜像说明:[PostgreSQL](https://hub.docker.com/_/postgres)、
+[RabbitMQ](https://hub.docker.com/_/rabbitmq)。
diff --git a/deploy/compose.dev.yml b/deploy/compose.dev.yml
new file mode 100644
index 0000000..5eec5fc
--- /dev/null
+++ b/deploy/compose.dev.yml
@@ -0,0 +1,40 @@
+name: kf-oj-dev
+services:
+ postgres:
+ image: postgres:18.6-bookworm
+ environment:
+ POSTGRES_USER: ${POSTGRES_USER:?required}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?required}
+ POSTGRES_DB: ${POSTGRES_DB:?required}
+ ports:
+ - "127.0.0.1:${POSTGRES_PORT:-5432}:5432"
+ volumes:
+ - postgres-data:/var/lib/postgresql
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
+ interval: 5s
+ timeout: 3s
+ retries: 20
+ mem_limit: 1g
+ cpus: 1
+ rabbitmq:
+ image: rabbitmq:4.3.6-management
+ environment:
+ RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:?required}
+ RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:?required}
+ RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:?required}
+ ports:
+ - "127.0.0.1:${RABBITMQ_PORT:-5672}:5672"
+ - "127.0.0.1:${RABBITMQ_MANAGEMENT_PORT:-15672}:15672"
+ volumes:
+ - rabbitmq-data:/var/lib/rabbitmq
+ healthcheck:
+ test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
+ interval: 5s
+ timeout: 5s
+ retries: 20
+ mem_limit: 768m
+ cpus: 1
+volumes:
+ postgres-data:
+ rabbitmq-data:
diff --git a/experiments/README.md b/experiments/README.md
new file mode 100644
index 0000000..166f5b6
--- /dev/null
+++ b/experiments/README.md
@@ -0,0 +1,7 @@
+# 可复现实验
+
+后续负载生成、调度对照、故障注入和统计脚本放在本目录;工程骨架阶段未开始性能实验。
+每次实验保存 Git 提交、环境、配置、负载摘要、原始事件、结果和复现命令。
+
+`results/` 默认忽略,报告只提交去标识摘要和原始材料的复现索引。
+每项实验先明确条件、正确性检查和验收指标,再运行并保留复现材料。
diff --git a/frontend/.env.example b/frontend/.env.example
new file mode 100644
index 0000000..8974b4d
--- /dev/null
+++ b/frontend/.env.example
@@ -0,0 +1,3 @@
+VITE_API_BASE_URL=
+# Development proxy only; never bundled into the browser as a credential.
+KF_API_PROXY_TARGET=http://127.0.0.1:8000
diff --git a/frontend/.node-version b/frontend/.node-version
new file mode 100644
index 0000000..8a94b99
--- /dev/null
+++ b/frontend/.node-version
@@ -0,0 +1 @@
+26.10.0
diff --git a/frontend/.npmrc b/frontend/.npmrc
new file mode 100644
index 0000000..bcad92e
--- /dev/null
+++ b/frontend/.npmrc
@@ -0,0 +1,2 @@
+# pnpm 11 reads project settings from pnpm-workspace.yaml.
+# Keep this file for future registry/auth configuration only.
diff --git a/frontend/.prettierignore b/frontend/.prettierignore
new file mode 100644
index 0000000..cabbe58
--- /dev/null
+++ b/frontend/.prettierignore
@@ -0,0 +1,4 @@
+pnpm-lock.yaml
+dist
+node_modules
+src/api/generated
diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json
new file mode 100644
index 0000000..0965e7e
--- /dev/null
+++ b/frontend/.prettierrc.json
@@ -0,0 +1,6 @@
+{
+ "semi": false,
+ "singleQuote": true,
+ "trailingComma": "all",
+ "printWidth": 100
+}
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..19dc9a2
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,24 @@
+# Vue 前端工程
+
+Vue 3 + TypeScript + Vite + Element Plus,使用 Vue Router 与 Pinia。
+当前页面是开发预览和实际 API 连接检查;没有模拟登录或模拟评测结果。
+
+```bash
+pnpm install --frozen-lockfile
+pnpm dev
+pnpm typecheck
+pnpm build
+```
+
+开发服务在 `http://127.0.0.1:5173`,`/api` 代理到 `http://127.0.0.1:8000`。
+复制 `.env.example` 为 `.env` 可调整代理和 API 地址。
+生产部署须配置 SPA 路由回退及 `/api` 反向代理;开发代理不进入生产产物。
+
+- `views/`:路由页面;`layouts/`:页面框架;`components/`:可复用界面组件。
+- `api/`:HTTP 客户端和业务 API 封装;`api/generated/`:从 OpenAPI 生成的类型。
+- `composables/`:带生命周期的交互逻辑;`stores/`:跨页面共享状态。
+- `router/`:路由;`styles/`:全局样式。
+
+已有代码按实际需要落位;`components/` 和 `stores/` 是后续扩展位置,不放空状态实现。
+API 类型更新执行根目录 `make contracts`;前端通过 `pnpm check:api` 拒绝过期类型。
+登录凭据、模型密钥和数据库连接不能进入 `VITE_` 环境变量。
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 0000000..060e21e
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,21 @@
+import js from '@eslint/js'
+import tseslint from 'typescript-eslint'
+import vue from 'eslint-plugin-vue'
+import prettier from 'eslint-config-prettier/flat'
+
+export default tseslint.config(
+ { ignores: ['dist/**', 'node_modules/**', 'src/api/generated/**'] },
+ js.configs.recommended,
+ ...tseslint.configs.recommended,
+ ...vue.configs['flat/recommended'],
+ {
+ files: ['**/*.vue'],
+ languageOptions: { parserOptions: { parser: tseslint.parser } },
+ rules: { 'vue/multi-word-component-names': 'off' },
+ },
+ {
+ files: ['vite.config.ts'],
+ languageOptions: { globals: { process: 'readonly' } },
+ },
+ prettier,
+)
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..3472db5
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ KF-OnlineJudge
+
+
+
+
+
+
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..6efa910
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "kf-oj-frontend",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "packageManager": "pnpm@11.28.4",
+ "engines": {
+ "node": ">=24.12.0 <27"
+ },
+ "scripts": {
+ "dev": "vite --host 127.0.0.1",
+ "build": "vue-tsc --noEmit && vite build",
+ "preview": "vite preview --host 127.0.0.1",
+ "typecheck": "vue-tsc --noEmit",
+ "lint": "eslint . --max-warnings 0",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
+ "generate:api": "openapi-typescript ../contracts/openapi/platform.v1.json -o src/api/generated/schema.d.ts",
+ "check:api": "pnpm generate:api --check"
+ },
+ "dependencies": {
+ "element-plus": "^2.9.0",
+ "openapi-fetch": "^0.14.0",
+ "pinia": "^3.0.0",
+ "vue": "^3.5.0",
+ "vue-router": "^4.5.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.0",
+ "@types/node": "^24.0.0",
+ "@vitejs/plugin-vue": "^6.0.0",
+ "eslint": "^10.0.0",
+ "eslint-config-prettier": "10.1.8",
+ "eslint-plugin-vue": "^10.0.0",
+ "openapi-typescript": "^7.0.0",
+ "prettier": "^3.0.0",
+ "typescript": "~5.9.0",
+ "typescript-eslint": "^8.0.0",
+ "vite": "^7.0.0",
+ "vue-tsc": "^3.0.0"
+ }
+}
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
new file mode 100644
index 0000000..59fbebd
--- /dev/null
+++ b/frontend/pnpm-lock.yaml
@@ -0,0 +1,2356 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ element-plus:
+ specifier: ^2.9.0
+ version: 2.14.7(vue@3.5.43(typescript@5.9.3))
+ openapi-fetch:
+ specifier: ^0.14.0
+ version: 0.14.1
+ pinia:
+ specifier: ^3.0.0
+ version: 3.0.4(typescript@5.9.3)(vue@3.5.43(typescript@5.9.3))
+ vue:
+ specifier: ^3.5.0
+ version: 3.5.43(typescript@5.9.3)
+ vue-router:
+ specifier: ^4.5.0
+ version: 4.6.4(vue@3.5.43(typescript@5.9.3))
+ devDependencies:
+ '@eslint/js':
+ specifier: ^10.0.0
+ version: 10.0.1(eslint@10.12.0(supports-color@10.2.2))
+ '@types/node':
+ specifier: ^24.0.0
+ version: 24.19.1
+ '@vitejs/plugin-vue':
+ specifier: ^6.0.0
+ version: 6.0.9(vite@7.3.6(@types/node@24.19.1))(vue@3.5.43(typescript@5.9.3))
+ eslint:
+ specifier: ^10.0.0
+ version: 10.12.0(supports-color@10.2.2)
+ eslint-config-prettier:
+ specifier: 10.1.8
+ version: 10.1.8(eslint@10.12.0(supports-color@10.2.2))
+ eslint-plugin-vue:
+ specifier: ^10.0.0
+ version: 10.11.1(@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@10.12.0(supports-color@10.2.2))(vue-eslint-parser@10.4.1(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2))
+ openapi-typescript:
+ specifier: ^7.0.0
+ version: 7.13.0(typescript@5.9.3)
+ prettier:
+ specifier: ^3.0.0
+ version: 3.9.9
+ typescript:
+ specifier: ~5.9.0
+ version: 5.9.3
+ typescript-eslint:
+ specifier: ^8.0.0
+ version: 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
+ vite:
+ specifier: ^7.0.0
+ version: 7.3.6(@types/node@24.19.1)
+ vue-tsc:
+ specifier: ^3.0.0
+ version: 3.3.12(typescript@5.9.3)
+
+packages:
+
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.9':
+ resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/types@7.29.8':
+ resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@cacheable/memory@2.2.0':
+ resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==}
+
+ '@cacheable/utils@2.5.0':
+ resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==}
+
+ '@ctrl/tinycolor@4.2.1':
+ resolution: {integrity: sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==}
+ engines: {node: '>=14'}
+
+ '@element-plus/icons-vue@2.3.2':
+ resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==}
+ peerDependencies:
+ vue: ^3.2.0
+
+ '@esbuild/aix-ppc64@0.28.2':
+ resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.28.2':
+ resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.28.2':
+ resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.28.2':
+ resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.28.2':
+ resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.28.2':
+ resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.28.2':
+ resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.28.2':
+ resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.28.2':
+ resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.28.2':
+ resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.28.2':
+ resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.28.2':
+ resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.28.2':
+ resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.28.2':
+ resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.28.2':
+ resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.28.2':
+ resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.28.2':
+ resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.28.2':
+ resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.28.2':
+ resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.28.2':
+ resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.28.2':
+ resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.28.2':
+ resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.28.2':
+ resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.28.2':
+ resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.28.2':
+ resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.28.2':
+ resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint-community/eslint-utils@4.10.1':
+ resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.2':
+ resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.23.5':
+ resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/config-helpers@0.7.0':
+ resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/core@1.2.1':
+ resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/js@10.0.1':
+ resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ peerDependencies:
+ eslint: ^10.0.0
+ peerDependenciesMeta:
+ eslint:
+ optional: true
+
+ '@eslint/object-schema@3.0.5':
+ resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/plugin-kit@0.7.3':
+ resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@floating-ui/core@1.8.0':
+ resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==}
+
+ '@floating-ui/dom@1.8.0':
+ resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==}
+
+ '@floating-ui/utils@0.2.12':
+ resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
+
+ '@humanfs/core@0.19.2':
+ resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.8':
+ resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/types@0.15.0':
+ resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
+ '@jridgewell/sourcemap-codec@1.6.0':
+ resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
+
+ '@keyv/bigmap@1.3.1':
+ resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ keyv: ^5.6.0
+
+ '@keyv/serialize@1.1.1':
+ resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
+
+ '@napi-rs/lzma-linux-x64-gnu@1.5.1':
+ resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
+ engines: {node: ^22.20 || ^24.12 || >=25}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@redocly/ajv@8.11.2':
+ resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
+
+ '@redocly/config@0.22.0':
+ resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==}
+
+ '@redocly/openapi-core@1.34.20':
+ resolution: {integrity: sha512-ypeBZ/6BKXR9+7/TtbKhbl4UgD7raHhPS12oknlKno2A8+lnFkxIwiE/Aklu6L2cd/ioH+fCWuMxi9/p3EyAPw==}
+ engines: {node: '>=18.17.0', npm: '>=9.5.0'}
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@rollup/rollup-android-arm-eabi@4.64.0':
+ resolution: {integrity: sha512-FLLpX941CD/7aBvX3GqLGBM9OvY5PGXTryPJkG5r5/OuBpZpV5k45Uo3RxAvqd35tsyc3hH1bhtz921IKzoIzg==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.64.0':
+ resolution: {integrity: sha512-iI32XM/jnDYJsvx5ai/za/7gWO9XoSeFFtU06/ZCaRgyilfJOjaopD3nOpk2WrTCGOyeDt4hleUiGOAnd5tHNg==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.64.0':
+ resolution: {integrity: sha512-dRBZlbt2sEtF8Mn4MODThze9fIPdDoOhVSlXMAKy5dQbOYK9pOOoPY4Fat3OTtr0+8zbJo5TTZwlsfxY/kDNJg==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.64.0':
+ resolution: {integrity: sha512-Y3whrJOMucfkhgNkvcM93M6j8C53iEAMmPMYD99C6G1kUy4ELigLWk9ar2/4aCBSRgsql0zdlOemLgW2m5GZgQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.64.0':
+ resolution: {integrity: sha512-S6S1GEzPjFF6IONYJX3AKt/799+8RgBgCcwRnUo+Ep1IvBxUbw5M51iU0TDdK2NWWVnSvCQ8yi6Em0fGASMOng==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.64.0':
+ resolution: {integrity: sha512-Ed1DPoFENkXj4IGzSB12SesV40iP6LxCCrbTIkSGIQBpOOxpBM+0kj62lmaeIHfMWTDoddqTOtqc7s8LYfn+Pw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.64.0':
+ resolution: {integrity: sha512-J/6xhutJPtFyIkmHdf4olOdHHKH5kOJNumojdWn+Elv5ICEYTVOa362Vw27TxsK0BLzb1le3EDlO3UbvFK1lnQ==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.64.0':
+ resolution: {integrity: sha512-G4L/Nnx59d2LvDoV/P4S5lZ7oVLT6hodOHMacqjiq2LL2gts86KbkEBTYL53+fBt0odhf7XPIbIATGoYiJctxg==}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-arm64-gnu@4.64.0':
+ resolution: {integrity: sha512-zF16pvJDEUksGYRbfnXne7pdklAyj2XDmJeYKgP93oq0cUyX9Y8O7lUo1R+IY1GagP/YBvCEDCe7eEcAKpw//g==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm64-musl@4.64.0':
+ resolution: {integrity: sha512-nIggWn8n+w12+R5lsV+PYYoUY8vuObxPAQqJI7WES+9v0qspYuAHoZMfYM0yLxlipasT5BY9ht5W8t5c/rSCkg==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-loong64-gnu@4.64.0':
+ resolution: {integrity: sha512-WLOTiHPzszP0dxIMsHpFeDSOSB5I49Mj8K4+yKE3H1X/D0JRG5XZ5jrctJLMjnUc/LUzaKiwq6EHWNJ8pup8PA==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-loong64-musl@4.64.0':
+ resolution: {integrity: sha512-35RK9aRhh7g7Pmeu5gQ01aCvrhXzj9LoBE6xtUcEH1ns+nyTaSzxSpADmBQINu4QiTDGvmQYwo1j5hlg1rRXFw==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.64.0':
+ resolution: {integrity: sha512-RqTfnpoTjuWfPAmqK5p2edt2AMCDJWm6oTn7sKfmyx+6tRga0ip05SJ977o6udTcIiDeF/CJMUfkaN4gtWt6Aw==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-ppc64-musl@4.64.0':
+ resolution: {integrity: sha512-RQjZPigjxFvFZDijG6rjlSXxhRaTf9zLYVpGK+lVcga64oMqP1GbgVqCb46eHTTceSIgp89Ja0y5aPe3GFBYHg==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.64.0':
+ resolution: {integrity: sha512-tjL58mgQcTRnDRpeky+Sth1MKQeYi2Ct6r3Luq9E5aTSjnM/i/JbozqZkKqp9thM5+oDSZxo+05f9kdQedfTGA==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-riscv64-musl@4.64.0':
+ resolution: {integrity: sha512-ictoRyVaoZ8C67myHef+8hhCa6IplXJJ/xUCkVCxziV4zxMZh+jvhkhGsPBLqj1S72fnqD0QH5zL4lcaa4X7DQ==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-s390x-gnu@4.64.0':
+ resolution: {integrity: sha512-8aVvZ2hfr9RLFzBQkQNmYeHLDja8fLeQAmLt757zdKN/B+8L809KlLJKctQjS22wZotptV8G6o7fVPuAs8S1tQ==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-gnu@4.64.0':
+ resolution: {integrity: sha512-2dEF8GAcDKwshUfydD+GhosppNyBzhVUHkdFF3CXd3FpOkzj/RZzF5aoEtzklujklT5qFVRU2GK/cFeLaTNKVg==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-musl@4.64.0':
+ resolution: {integrity: sha512-3XHTvoo2hKkh0MiOfS7SkhHjMaaWF7P4F3drocZZaho/Z8w7h/jpnuBHm91BOQLfvrKHs1yR+zir4E8RI3XzTQ==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-openbsd-x64@4.64.0':
+ resolution: {integrity: sha512-HKtkzodL4a3KFWDXDoO61cRynAWTR3LnAE0oGtfG9lcNFpNuVYUFOByxDgNqEvic9rvaQbAj+SFwTEWDptRaIw==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.64.0':
+ resolution: {integrity: sha512-JBsxqiY16uyyCujtoC3s7zL+SrYyNElptD7K4V+KUoJ2V8/XG3hPWpheK+SuPwNh1cikJvIZmIdsrYXvD6PLJw==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.64.0':
+ resolution: {integrity: sha512-S5Qrijh37qhnUYQ0ay8WN3LuWeKjow7wTUyCOPMZtnXiBXYXdsZETI6ZLfYnQAKovltckPLBFdNeD61P2rJc7g==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.64.0':
+ resolution: {integrity: sha512-zbZ6iMmLFEsCtTdc2/y1F/cDFVNNCPF7Ae8PCT/wdPE//hGwYp6r+MyCLMFX+kOeIe09Pz6ASfnnfpSTN1FJQQ==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.64.0':
+ resolution: {integrity: sha512-L6Hw69oNpuYaOr1zCG30v0/DqKYCSRwodPGwIcVZ/6f8SOzq9rxtL/xNVI5MO4wjli+Ko0ErK9uHJL3mNPt8wQ==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.64.0':
+ resolution: {integrity: sha512-nl+CTWIgOGszQmUz/kanfgrfFZ4qiwxKIfT/ryl3N3Ewk5tqizmYtNXiqb7Bu/fDtNgu1kyxdFiAUJNur3iysg==}
+ cpu: [x64]
+ os: [win32]
+
+ '@sxzz/popperjs-es@2.11.8':
+ resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==}
+
+ '@types/esrecurse@4.3.1':
+ resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/lodash-es@4.17.12':
+ resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
+
+ '@types/lodash@4.17.25':
+ resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==}
+
+ '@types/node@24.19.1':
+ resolution: {integrity: sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==}
+
+ '@types/web-bluetooth@0.0.21':
+ resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
+
+ '@typescript-eslint/eslint-plugin@8.71.0':
+ resolution: {integrity: sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.71.0
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/parser@8.71.0':
+ resolution: {integrity: sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/project-service@8.71.0':
+ resolution: {integrity: sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/scope-manager@8.71.0':
+ resolution: {integrity: sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/tsconfig-utils@8.71.0':
+ resolution: {integrity: sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/type-utils@8.71.0':
+ resolution: {integrity: sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/types@8.71.0':
+ resolution: {integrity: sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.71.0':
+ resolution: {integrity: sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/utils@8.71.0':
+ resolution: {integrity: sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/visitor-keys@8.71.0':
+ resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitejs/plugin-vue@6.0.9':
+ resolution: {integrity: sha512-rD/MORlhaZMlXWW0rEn4FB4wMinWC0z/D7Ye160S5+Rs1mC8ZDAdcDp4SjKfUhG5t8q1ktcPVw4xuTkPRlzrSA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ vue: ^3.2.25
+
+ '@volar/language-core@2.4.28':
+ resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
+
+ '@volar/source-map@2.4.28':
+ resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==}
+
+ '@volar/typescript@2.4.28':
+ resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@vue/compiler-core@3.5.43':
+ resolution: {integrity: sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==}
+
+ '@vue/compiler-dom@3.5.43':
+ resolution: {integrity: sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==}
+
+ '@vue/compiler-sfc@3.5.43':
+ resolution: {integrity: sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==}
+
+ '@vue/compiler-ssr@3.5.43':
+ resolution: {integrity: sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==}
+
+ '@vue/devtools-api@6.6.4':
+ resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
+
+ '@vue/devtools-api@7.7.10':
+ resolution: {integrity: sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==}
+
+ '@vue/devtools-kit@7.7.10':
+ resolution: {integrity: sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==}
+
+ '@vue/devtools-shared@7.7.10':
+ resolution: {integrity: sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==}
+
+ '@vue/language-core@3.3.12':
+ resolution: {integrity: sha512-k7uIKR0n4CBidk233y7cac9osnDQ5WRidTl6nsrNvpZ3nf8cx9iJxRVsRzVsoPRHbowT/iKgGMTS9RGov3jeIA==}
+
+ '@vue/reactivity@3.5.43':
+ resolution: {integrity: sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==}
+
+ '@vue/runtime-core@3.5.43':
+ resolution: {integrity: sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==}
+
+ '@vue/runtime-dom@3.5.43':
+ resolution: {integrity: sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==}
+
+ '@vue/server-renderer@3.5.43':
+ resolution: {integrity: sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==}
+
+ '@vue/shared@3.5.43':
+ resolution: {integrity: sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==}
+
+ '@vueuse/core@15.0.0':
+ resolution: {integrity: sha512-Zv67h5mrk7YHDJsRObx+DXaCBz6RKBUpxqLesJ+3xZNPG6yuQs7ja7kg3veFrrouYLWAqydQ7CwGEbLVxB43Ng==}
+ engines: {node: '>=22'}
+ peerDependencies:
+ vue: ^3.5.0
+
+ '@vueuse/metadata@15.0.0':
+ resolution: {integrity: sha512-sfWE5LADAKJ0KnjtGliMdSN8Ij/L7uP6rXIZlgPloAI4zvvefbw6H5Lt7KJA6RqQOYgVqWF5Guj50HHMIzygBg==}
+ engines: {node: '>=22'}
+
+ '@vueuse/shared@15.0.0':
+ resolution: {integrity: sha512-vbNXhGrbV53xPOMSyOzMHQRvhMgZf7oNDw2RChUetVxwTIEkInQ1joZ9JbT0G3z2n2AJNjvmCWeM9ySYrnvogA==}
+ engines: {node: '>=22'}
+ peerDependencies:
+ vue: ^3.5.0
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.18.0:
+ resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ agent-base@7.1.4:
+ resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
+ engines: {node: '>= 14'}
+
+ ajv@6.15.0:
+ resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
+
+ alien-signals@3.2.1:
+ resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
+
+ ansi-colors@4.1.3:
+ resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
+ engines: {node: '>=6'}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ async-validator@4.2.5:
+ resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ balanced-match@4.0.4:
+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+ engines: {node: 18 || 20 || >=22}
+
+ birpc@2.9.0:
+ resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
+
+ boolbase@1.0.0:
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+
+ brace-expansion@2.1.7:
+ resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==}
+
+ brace-expansion@5.0.12:
+ resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
+ engines: {node: 20 || >=22}
+
+ cacheable@2.5.0:
+ resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==}
+
+ change-case@5.4.4:
+ resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
+
+ colorette@1.4.0:
+ resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
+
+ copy-anything@4.1.5:
+ resolution: {integrity: sha512-vePst2VLTSqPglW7Q98VwFpGiGpOHTyXIo81MO5VyLecA5zMbZw/P/V1vmwD1jksa2iJpn/YkDrJ3XhTfzUyhQ==}
+ engines: {node: '>=18'}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ dayjs@1.11.23:
+ resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ element-plus@2.14.7:
+ resolution: {integrity: sha512-lrG7pZyCS+mpCNVpzxxyu5r35+T6V0hzkAeIwn7HA6to772XSTHKuYCZG51OfzlqaxGbNegLxs650zAF1j43gA==}
+ peerDependencies:
+ vue: ^3.3.7
+
+ entities@7.0.1:
+ resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
+ engines: {node: '>=0.12'}
+
+ esbuild@0.28.2:
+ resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-config-prettier@10.1.8:
+ resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
+ hasBin: true
+ peerDependencies:
+ eslint: '>=7.0.0'
+
+ eslint-plugin-vue@10.11.1:
+ resolution: {integrity: sha512-pasVXhl7wUmIvhujoqs/m0mJ9r59QW1IR9XXYKGR0FETzSM+Hhlf4qKIYgWiDDP+9YKv4YfbwskD7LqIdVcgQw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
+ '@typescript-eslint/parser': ^7.0.0 || ^8.0.0
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ vue-eslint-parser: ^10.3.0
+ peerDependenciesMeta:
+ '@stylistic/eslint-plugin':
+ optional: true
+ '@typescript-eslint/parser':
+ optional: true
+
+ eslint-scope@9.1.2:
+ resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@5.0.1:
+ resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ eslint@10.12.0:
+ resolution: {integrity: sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@11.2.0:
+ resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ esquery@1.7.0:
+ resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ estree-walker@2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ file-entry-cache@11.1.5:
+ resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@6.1.23:
+ resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==}
+
+ flatted@3.4.4:
+ resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ hashery@1.5.1:
+ resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==}
+ engines: {node: '>=20'}
+
+ hookable@5.5.3:
+ resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
+
+ hookified@1.15.1:
+ resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==}
+
+ hookified@2.2.0:
+ resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==}
+
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ ignore@7.0.12:
+ resolution: {integrity: sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==}
+ engines: {node: '>= 4'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ index-to-position@1.2.0:
+ resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==}
+ engines: {node: '>=18'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ js-levenshtein@1.1.6:
+ resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
+ engines: {node: '>=0.10.0'}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.3.2:
+ resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
+ hasBin: true
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ keyv@5.6.0:
+ resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lodash-es@4.18.1:
+ resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
+
+ lodash-unified@1.0.3:
+ resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}
+ peerDependencies:
+ '@types/lodash-es': '*'
+ lodash: '*'
+ lodash-es: '*'
+
+ lodash@4.18.1:
+ resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ memoize-one@6.0.0:
+ resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
+
+ minimatch@10.2.6:
+ resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
+ engines: {node: 18 || 20 || >=22}
+
+ minimatch@5.1.9:
+ resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
+ engines: {node: '>=10'}
+
+ mitt@3.0.1:
+ resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ muggle-string@0.4.1:
+ resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
+
+ nanoid@3.3.19:
+ resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ normalize-wheel-es@1.2.0:
+ resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
+
+ nth-check@2.1.1:
+ resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+
+ openapi-fetch@0.14.1:
+ resolution: {integrity: sha512-l7RarRHxlEZYjMLd/PR0slfMVse2/vvIAGm75/F7J6MlQ8/b9uUQmUF2kCPrQhJqMXSxmYWObVgeYXbFYzZR+A==}
+
+ openapi-typescript-helpers@0.0.15:
+ resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
+
+ openapi-typescript@7.13.0:
+ resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==}
+ hasBin: true
+ peerDependencies:
+ typescript: ^5.x
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ parse-json@8.3.0:
+ resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
+ engines: {node: '>=18'}
+
+ path-browserify@1.0.1:
+ resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ perfect-debounce@1.0.0:
+ resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.7:
+ resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
+ engines: {node: '>=12'}
+
+ pinia@3.0.4:
+ resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
+ peerDependencies:
+ typescript: '>=4.5.0'
+ vue: ^3.5.11
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ pluralize@8.0.0:
+ resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
+ engines: {node: '>=4'}
+
+ postcss-selector-parser@7.1.6:
+ resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==}
+ engines: {node: '>=4'}
+
+ postcss@8.5.28:
+ resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ prettier@3.9.9:
+ resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==}
+ engines: {node: '>=14'}
+ hasBin: true
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ qified@0.10.1:
+ resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==}
+ engines: {node: '>=20'}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ rfdc@1.4.1:
+ resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
+
+ rollup@4.64.0:
+ resolution: {integrity: sha512-gjA/kFeDfffII8COoPsvLPvE16hWDTEc1+0QKQNKUkuV8iJjqsC4xXs8/FzuL0v9EV39kuxTbAgcFoTRyy2TGQ==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ semver@7.8.5:
+ resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
+ engines: {node: '>=0.10.0'}
+
+ speakingurl@14.0.1:
+ resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
+ engines: {node: '>=0.10.0'}
+
+ superjson@2.2.6:
+ resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==}
+ engines: {node: '>=16'}
+
+ supports-color@10.2.2:
+ resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ ts-api-utils@2.5.0:
+ resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ type-fest@4.41.0:
+ resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
+ engines: {node: '>=16'}
+
+ typescript-eslint@8.71.0:
+ resolution: {integrity: sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@7.24.6:
+ resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
+
+ uri-js-replace@1.0.1:
+ resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ vite@7.3.6:
+ resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ lightningcss: ^1.21.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ 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
+
+ vscode-uri@3.2.0:
+ resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==}
+
+ vue-component-type-helpers@3.3.12:
+ resolution: {integrity: sha512-HwCqmgTfLFskQc4P5TRnbduCMyPat2Ezu0elLWOGn0dD/6BKHP15B5bxce6aDi+E4kblPtru8amIBQzu0Di3qQ==}
+
+ vue-eslint-parser@10.4.1:
+ resolution: {integrity: sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+
+ vue-router@4.6.4:
+ resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
+ peerDependencies:
+ vue: ^3.5.0
+
+ vue-tsc@3.3.12:
+ resolution: {integrity: sha512-kbpaz18O9jPVkXVHD1BEjpUXM6WhXSu+62L1sGGr6Wbzd8GbvkBQWjoj3KZ/C0aq2MZllce7UdtWzwCIYsBMGA==}
+ hasBin: true
+ peerDependencies:
+ typescript: '>=5.0.0'
+
+ vue@3.5.43:
+ resolution: {integrity: sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
+ yaml-ast-parser@0.0.43:
+ resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
+
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+snapshots:
+
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/parser@7.29.9':
+ dependencies:
+ '@babel/types': 7.29.8
+
+ '@babel/types@7.29.8':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@cacheable/memory@2.2.0':
+ dependencies:
+ '@cacheable/utils': 2.5.0
+ '@keyv/bigmap': 1.3.1(keyv@5.6.0)
+ hookified: 1.15.1
+ keyv: 5.6.0
+
+ '@cacheable/utils@2.5.0':
+ dependencies:
+ hashery: 1.5.1
+ keyv: 5.6.0
+
+ '@ctrl/tinycolor@4.2.1': {}
+
+ '@element-plus/icons-vue@2.3.2(vue@3.5.43(typescript@5.9.3))':
+ dependencies:
+ vue: 3.5.43(typescript@5.9.3)
+
+ '@esbuild/aix-ppc64@0.28.2':
+ optional: true
+
+ '@esbuild/android-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/android-arm@0.28.2':
+ optional: true
+
+ '@esbuild/android-x64@0.28.2':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/darwin-x64@0.28.2':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-arm@0.28.2':
+ optional: true
+
+ '@esbuild/linux-ia32@0.28.2':
+ optional: true
+
+ '@esbuild/linux-loong64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.28.2':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-s390x@0.28.2':
+ optional: true
+
+ '@esbuild/linux-x64@0.28.2':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/sunos-x64@0.28.2':
+ optional: true
+
+ '@esbuild/win32-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/win32-ia32@0.28.2':
+ optional: true
+
+ '@esbuild/win32-x64@0.28.2':
+ optional: true
+
+ '@eslint-community/eslint-utils@4.10.1(eslint@10.12.0(supports-color@10.2.2))':
+ dependencies:
+ eslint: 10.12.0(supports-color@10.2.2)
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.2': {}
+
+ '@eslint/config-array@0.23.5(supports-color@10.2.2)':
+ dependencies:
+ '@eslint/object-schema': 3.0.5
+ debug: 4.4.3(supports-color@10.2.2)
+ minimatch: 10.2.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.7.0':
+ dependencies:
+ '@eslint/core': 1.2.1
+
+ '@eslint/core@1.2.1':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/js@10.0.1(eslint@10.12.0(supports-color@10.2.2))':
+ optionalDependencies:
+ eslint: 10.12.0(supports-color@10.2.2)
+
+ '@eslint/object-schema@3.0.5': {}
+
+ '@eslint/plugin-kit@0.7.3':
+ dependencies:
+ '@eslint/core': 1.2.1
+ levn: 0.4.1
+
+ '@floating-ui/core@1.8.0':
+ dependencies:
+ '@floating-ui/utils': 0.2.12
+
+ '@floating-ui/dom@1.8.0':
+ dependencies:
+ '@floating-ui/core': 1.8.0
+ '@floating-ui/utils': 0.2.12
+
+ '@floating-ui/utils@0.2.12': {}
+
+ '@humanfs/core@0.19.2':
+ dependencies:
+ '@humanfs/types': 0.15.0
+
+ '@humanfs/node@0.16.8':
+ dependencies:
+ '@humanfs/core': 0.19.2
+ '@humanfs/types': 0.15.0
+ '@humanwhocodes/retry': 0.4.3
+
+ '@humanfs/types@0.15.0': {}
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
+ '@jridgewell/sourcemap-codec@1.6.0': {}
+
+ '@keyv/bigmap@1.3.1(keyv@5.6.0)':
+ dependencies:
+ hashery: 1.5.1
+ hookified: 1.15.1
+ keyv: 5.6.0
+
+ '@keyv/serialize@1.1.1': {}
+
+ '@napi-rs/lzma-linux-x64-gnu@1.5.1':
+ optional: true
+
+ '@redocly/ajv@8.11.2':
+ dependencies:
+ fast-deep-equal: 3.1.3
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+ uri-js-replace: 1.0.1
+
+ '@redocly/config@0.22.0': {}
+
+ '@redocly/openapi-core@1.34.20(supports-color@10.2.2)':
+ dependencies:
+ '@redocly/ajv': 8.11.2
+ '@redocly/config': 0.22.0
+ colorette: 1.4.0
+ https-proxy-agent: 7.0.6(supports-color@10.2.2)
+ js-levenshtein: 1.1.6
+ js-yaml: 4.3.2
+ minimatch: 5.1.9
+ pluralize: 8.0.0
+ yaml-ast-parser: 0.0.43
+ transitivePeerDependencies:
+ - supports-color
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@rollup/rollup-android-arm-eabi@4.64.0':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.64.0':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.64.0':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.64.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.64.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.64.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.64.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.64.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.64.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.64.0':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.64.0':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-musl@4.64.0':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.64.0':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-musl@4.64.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.64.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.64.0':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.64.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.64.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.64.0':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.64.0':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.64.0':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.64.0':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.64.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.64.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.64.0':
+ optional: true
+
+ '@sxzz/popperjs-es@2.11.8': {}
+
+ '@types/esrecurse@4.3.1': {}
+
+ '@types/estree@1.0.9': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/lodash-es@4.17.12':
+ dependencies:
+ '@types/lodash': 4.17.25
+
+ '@types/lodash@4.17.25': {}
+
+ '@types/node@24.19.1':
+ dependencies:
+ undici-types: 7.24.6
+
+ '@types/web-bluetooth@0.0.21': {}
+
+ '@typescript-eslint/eslint-plugin@8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.2
+ '@typescript-eslint/parser': 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
+ '@typescript-eslint/scope-manager': 8.71.0
+ '@typescript-eslint/type-utils': 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
+ '@typescript-eslint/visitor-keys': 8.71.0
+ eslint: 10.12.0(supports-color@10.2.2)
+ ignore: 7.0.12
+ natural-compare: 1.4.0
+ ts-api-utils: 2.5.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.71.0
+ '@typescript-eslint/types': 8.71.0
+ '@typescript-eslint/typescript-estree': 8.71.0(supports-color@10.2.2)(typescript@5.9.3)
+ '@typescript-eslint/visitor-keys': 8.71.0
+ debug: 4.4.3(supports-color@10.2.2)
+ eslint: 10.12.0(supports-color@10.2.2)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/project-service@8.71.0(supports-color@10.2.2)(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@5.9.3)
+ '@typescript-eslint/types': 8.71.0
+ debug: 4.4.3(supports-color@10.2.2)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.71.0':
+ dependencies:
+ '@typescript-eslint/types': 8.71.0
+ '@typescript-eslint/visitor-keys': 8.71.0
+
+ '@typescript-eslint/tsconfig-utils@8.71.0(typescript@5.9.3)':
+ dependencies:
+ typescript: 5.9.3
+
+ '@typescript-eslint/type-utils@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.71.0
+ '@typescript-eslint/typescript-estree': 8.71.0(supports-color@10.2.2)(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
+ debug: 4.4.3(supports-color@10.2.2)
+ eslint: 10.12.0(supports-color@10.2.2)
+ ts-api-utils: 2.5.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.71.0': {}
+
+ '@typescript-eslint/typescript-estree@8.71.0(supports-color@10.2.2)(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.71.0(supports-color@10.2.2)(typescript@5.9.3)
+ '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@5.9.3)
+ '@typescript-eslint/types': 8.71.0
+ '@typescript-eslint/visitor-keys': 8.71.0
+ debug: 4.4.3(supports-color@10.2.2)
+ minimatch: 10.2.6
+ semver: 7.8.5
+ tinyglobby: 0.2.17
+ ts-api-utils: 2.5.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(supports-color@10.2.2))
+ '@typescript-eslint/scope-manager': 8.71.0
+ '@typescript-eslint/types': 8.71.0
+ '@typescript-eslint/typescript-estree': 8.71.0(supports-color@10.2.2)(typescript@5.9.3)
+ eslint: 10.12.0(supports-color@10.2.2)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.71.0':
+ dependencies:
+ '@typescript-eslint/types': 8.71.0
+ eslint-visitor-keys: 5.0.1
+
+ '@vitejs/plugin-vue@6.0.9(vite@7.3.6(@types/node@24.19.1))(vue@3.5.43(typescript@5.9.3))':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.1
+ vite: 7.3.6(@types/node@24.19.1)
+ vue: 3.5.43(typescript@5.9.3)
+
+ '@volar/language-core@2.4.28':
+ dependencies:
+ '@volar/source-map': 2.4.28
+
+ '@volar/source-map@2.4.28': {}
+
+ '@volar/typescript@2.4.28(typescript@5.9.3)':
+ dependencies:
+ '@volar/language-core': 2.4.28
+ path-browserify: 1.0.1
+ vscode-uri: 3.2.0
+ optionalDependencies:
+ typescript: 5.9.3
+
+ '@vue/compiler-core@3.5.43':
+ dependencies:
+ '@babel/parser': 7.29.9
+ '@vue/shared': 3.5.43
+ entities: 7.0.1
+ estree-walker: 2.0.2
+ source-map-js: 1.2.2
+
+ '@vue/compiler-dom@3.5.43':
+ dependencies:
+ '@vue/compiler-core': 3.5.43
+ '@vue/shared': 3.5.43
+
+ '@vue/compiler-sfc@3.5.43':
+ dependencies:
+ '@babel/parser': 7.29.9
+ '@vue/compiler-core': 3.5.43
+ '@vue/compiler-dom': 3.5.43
+ '@vue/compiler-ssr': 3.5.43
+ '@vue/shared': 3.5.43
+ estree-walker: 2.0.2
+ magic-string: 0.30.21
+ postcss: 8.5.28
+ source-map-js: 1.2.2
+
+ '@vue/compiler-ssr@3.5.43':
+ dependencies:
+ '@vue/compiler-dom': 3.5.43
+ '@vue/shared': 3.5.43
+
+ '@vue/devtools-api@6.6.4': {}
+
+ '@vue/devtools-api@7.7.10':
+ dependencies:
+ '@vue/devtools-kit': 7.7.10
+
+ '@vue/devtools-kit@7.7.10':
+ dependencies:
+ '@vue/devtools-shared': 7.7.10
+ birpc: 2.9.0
+ hookable: 5.5.3
+ mitt: 3.0.1
+ perfect-debounce: 1.0.0
+ speakingurl: 14.0.1
+ superjson: 2.2.6
+
+ '@vue/devtools-shared@7.7.10':
+ dependencies:
+ rfdc: 1.4.1
+
+ '@vue/language-core@3.3.12':
+ dependencies:
+ '@volar/language-core': 2.4.28
+ '@vue/compiler-dom': 3.5.43
+ '@vue/shared': 3.5.43
+ alien-signals: 3.2.1
+ muggle-string: 0.4.1
+ path-browserify: 1.0.1
+ picomatch: 4.0.7
+
+ '@vue/reactivity@3.5.43':
+ dependencies:
+ '@vue/shared': 3.5.43
+
+ '@vue/runtime-core@3.5.43':
+ dependencies:
+ '@vue/reactivity': 3.5.43
+ '@vue/shared': 3.5.43
+
+ '@vue/runtime-dom@3.5.43':
+ dependencies:
+ '@vue/reactivity': 3.5.43
+ '@vue/runtime-core': 3.5.43
+ '@vue/shared': 3.5.43
+ csstype: 3.2.3
+
+ '@vue/server-renderer@3.5.43':
+ dependencies:
+ '@vue/compiler-ssr': 3.5.43
+ '@vue/runtime-dom': 3.5.43
+ '@vue/shared': 3.5.43
+
+ '@vue/shared@3.5.43': {}
+
+ '@vueuse/core@15.0.0(vue@3.5.43(typescript@5.9.3))':
+ dependencies:
+ '@types/web-bluetooth': 0.0.21
+ '@vueuse/metadata': 15.0.0
+ '@vueuse/shared': 15.0.0(vue@3.5.43(typescript@5.9.3))
+ vue: 3.5.43(typescript@5.9.3)
+
+ '@vueuse/metadata@15.0.0': {}
+
+ '@vueuse/shared@15.0.0(vue@3.5.43(typescript@5.9.3))':
+ dependencies:
+ vue: 3.5.43(typescript@5.9.3)
+
+ acorn-jsx@5.3.2(acorn@8.18.0):
+ dependencies:
+ acorn: 8.18.0
+
+ acorn@8.18.0: {}
+
+ agent-base@7.1.4: {}
+
+ ajv@6.15.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ alien-signals@3.2.1: {}
+
+ ansi-colors@4.1.3: {}
+
+ argparse@2.0.1: {}
+
+ async-validator@4.2.5: {}
+
+ balanced-match@1.0.2: {}
+
+ balanced-match@4.0.4: {}
+
+ birpc@2.9.0: {}
+
+ boolbase@1.0.0: {}
+
+ brace-expansion@2.1.7:
+ dependencies:
+ balanced-match: 1.0.2
+
+ brace-expansion@5.0.12:
+ dependencies:
+ balanced-match: 4.0.4
+
+ cacheable@2.5.0:
+ dependencies:
+ '@cacheable/memory': 2.2.0
+ '@cacheable/utils': 2.5.0
+ hookified: 1.15.1
+ keyv: 5.6.0
+ qified: 0.10.1
+
+ change-case@5.4.4: {}
+
+ colorette@1.4.0: {}
+
+ copy-anything@4.1.5: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ cssesc@3.0.0: {}
+
+ csstype@3.2.3: {}
+
+ dayjs@1.11.23: {}
+
+ debug@4.4.3(supports-color@10.2.2):
+ dependencies:
+ ms: 2.1.3
+ optionalDependencies:
+ supports-color: 10.2.2
+
+ deep-is@0.1.4: {}
+
+ element-plus@2.14.7(vue@3.5.43(typescript@5.9.3)):
+ dependencies:
+ '@ctrl/tinycolor': 4.2.1
+ '@element-plus/icons-vue': 2.3.2(vue@3.5.43(typescript@5.9.3))
+ '@floating-ui/dom': 1.8.0
+ '@popperjs/core': '@sxzz/popperjs-es@2.11.8'
+ '@types/lodash': 4.17.25
+ '@types/lodash-es': 4.17.12
+ '@vueuse/core': 15.0.0(vue@3.5.43(typescript@5.9.3))
+ async-validator: 4.2.5
+ dayjs: 1.11.23
+ lodash: 4.18.1
+ lodash-es: 4.18.1
+ lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1)
+ memoize-one: 6.0.0
+ normalize-wheel-es: 1.2.0
+ vue: 3.5.43(typescript@5.9.3)
+ vue-component-type-helpers: 3.3.12
+
+ entities@7.0.1: {}
+
+ esbuild@0.28.2:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.28.2
+ '@esbuild/android-arm': 0.28.2
+ '@esbuild/android-arm64': 0.28.2
+ '@esbuild/android-x64': 0.28.2
+ '@esbuild/darwin-arm64': 0.28.2
+ '@esbuild/darwin-x64': 0.28.2
+ '@esbuild/freebsd-arm64': 0.28.2
+ '@esbuild/freebsd-x64': 0.28.2
+ '@esbuild/linux-arm': 0.28.2
+ '@esbuild/linux-arm64': 0.28.2
+ '@esbuild/linux-ia32': 0.28.2
+ '@esbuild/linux-loong64': 0.28.2
+ '@esbuild/linux-mips64el': 0.28.2
+ '@esbuild/linux-ppc64': 0.28.2
+ '@esbuild/linux-riscv64': 0.28.2
+ '@esbuild/linux-s390x': 0.28.2
+ '@esbuild/linux-x64': 0.28.2
+ '@esbuild/netbsd-arm64': 0.28.2
+ '@esbuild/netbsd-x64': 0.28.2
+ '@esbuild/openbsd-arm64': 0.28.2
+ '@esbuild/openbsd-x64': 0.28.2
+ '@esbuild/openharmony-arm64': 0.28.2
+ '@esbuild/sunos-x64': 0.28.2
+ '@esbuild/win32-arm64': 0.28.2
+ '@esbuild/win32-ia32': 0.28.2
+ '@esbuild/win32-x64': 0.28.2
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-config-prettier@10.1.8(eslint@10.12.0(supports-color@10.2.2)):
+ dependencies:
+ eslint: 10.12.0(supports-color@10.2.2)
+
+ eslint-plugin-vue@10.11.1(@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@10.12.0(supports-color@10.2.2))(vue-eslint-parser@10.4.1(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(supports-color@10.2.2))
+ eslint: 10.12.0(supports-color@10.2.2)
+ natural-compare: 1.4.0
+ nth-check: 2.1.1
+ postcss-selector-parser: 7.1.6
+ semver: 7.8.5
+ vue-eslint-parser: 10.4.1(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)
+ xml-name-validator: 5.0.0
+ optionalDependencies:
+ '@typescript-eslint/parser': 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
+
+ eslint-scope@9.1.2:
+ dependencies:
+ '@types/esrecurse': 4.3.1
+ '@types/estree': 1.0.9
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@5.0.1: {}
+
+ eslint@10.12.0(supports-color@10.2.2):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(supports-color@10.2.2))
+ '@eslint-community/regexpp': 4.12.2
+ '@eslint/config-array': 0.23.5(supports-color@10.2.2)
+ '@eslint/config-helpers': 0.7.0
+ '@eslint/core': 1.2.1
+ '@eslint/plugin-kit': 0.7.3
+ '@humanfs/node': 0.16.8
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.9
+ ajv: 6.15.0
+ cross-spawn: 7.0.6
+ debug: 4.4.3(supports-color@10.2.2)
+ escape-string-regexp: 4.0.0
+ eslint-scope: 9.1.2
+ eslint-visitor-keys: 5.0.1
+ espree: 11.2.0
+ esquery: 1.7.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 11.1.5
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ minimatch: 10.2.6
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@11.2.0:
+ dependencies:
+ acorn: 8.18.0
+ acorn-jsx: 5.3.2(acorn@8.18.0)
+ eslint-visitor-keys: 5.0.1
+
+ esquery@1.7.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ estree-walker@2.0.2: {}
+
+ esutils@2.0.3: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fdir@6.5.0(picomatch@4.0.7):
+ optionalDependencies:
+ picomatch: 4.0.7
+
+ file-entry-cache@11.1.5:
+ dependencies:
+ flat-cache: 6.1.23
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@6.1.23:
+ dependencies:
+ cacheable: 2.5.0
+ flatted: 3.4.4
+ hookified: 1.15.1
+
+ flatted@3.4.4: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ hashery@1.5.1:
+ dependencies:
+ hookified: 1.15.1
+
+ hookable@5.5.3: {}
+
+ hookified@1.15.1: {}
+
+ hookified@2.2.0: {}
+
+ https-proxy-agent@7.0.6(supports-color@10.2.2):
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3(supports-color@10.2.2)
+ transitivePeerDependencies:
+ - supports-color
+
+ ignore@5.3.2: {}
+
+ ignore@7.0.12: {}
+
+ imurmurhash@0.1.4: {}
+
+ index-to-position@1.2.0: {}
+
+ is-extglob@2.1.1: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ isexe@2.0.0: {}
+
+ js-levenshtein@1.1.6: {}
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.3.2:
+ dependencies:
+ argparse: 2.0.1
+
+ json-schema-traverse@0.4.1: {}
+
+ json-schema-traverse@1.0.0: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ keyv@5.6.0:
+ dependencies:
+ '@keyv/serialize': 1.1.1
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lodash-es@4.18.1: {}
+
+ lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1):
+ dependencies:
+ '@types/lodash-es': 4.17.12
+ lodash: 4.18.1
+ lodash-es: 4.18.1
+
+ lodash@4.18.1: {}
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.6.0
+
+ memoize-one@6.0.0: {}
+
+ minimatch@10.2.6:
+ dependencies:
+ brace-expansion: 5.0.12
+
+ minimatch@5.1.9:
+ dependencies:
+ brace-expansion: 2.1.7
+
+ mitt@3.0.1: {}
+
+ ms@2.1.3: {}
+
+ muggle-string@0.4.1: {}
+
+ nanoid@3.3.19: {}
+
+ natural-compare@1.4.0: {}
+
+ normalize-wheel-es@1.2.0: {}
+
+ nth-check@2.1.1:
+ dependencies:
+ boolbase: 1.0.0
+
+ openapi-fetch@0.14.1:
+ dependencies:
+ openapi-typescript-helpers: 0.0.15
+
+ openapi-typescript-helpers@0.0.15: {}
+
+ openapi-typescript@7.13.0(typescript@5.9.3):
+ dependencies:
+ '@redocly/openapi-core': 1.34.20(supports-color@10.2.2)
+ ansi-colors: 4.1.3
+ change-case: 5.4.4
+ parse-json: 8.3.0
+ supports-color: 10.2.2
+ typescript: 5.9.3
+ yargs-parser: 21.1.1
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ parse-json@8.3.0:
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ index-to-position: 1.2.0
+ type-fest: 4.41.0
+
+ path-browserify@1.0.1: {}
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ perfect-debounce@1.0.0: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.7: {}
+
+ pinia@3.0.4(typescript@5.9.3)(vue@3.5.43(typescript@5.9.3)):
+ dependencies:
+ '@vue/devtools-api': 7.7.10
+ vue: 3.5.43(typescript@5.9.3)
+ optionalDependencies:
+ typescript: 5.9.3
+
+ pluralize@8.0.0: {}
+
+ postcss-selector-parser@7.1.6:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss@8.5.28:
+ dependencies:
+ nanoid: 3.3.19
+ picocolors: 1.1.1
+ source-map-js: 1.2.2
+
+ prelude-ls@1.2.1: {}
+
+ prettier@3.9.9: {}
+
+ punycode@2.3.1: {}
+
+ qified@0.10.1:
+ dependencies:
+ hookified: 2.2.0
+
+ require-from-string@2.0.2: {}
+
+ rfdc@1.4.1: {}
+
+ rollup@4.64.0:
+ dependencies:
+ '@types/estree': 1.0.9
+ optionalDependencies:
+ '@napi-rs/lzma-linux-x64-gnu': 1.5.1
+ '@rollup/rollup-android-arm-eabi': 4.64.0
+ '@rollup/rollup-android-arm64': 4.64.0
+ '@rollup/rollup-darwin-arm64': 4.64.0
+ '@rollup/rollup-darwin-x64': 4.64.0
+ '@rollup/rollup-freebsd-arm64': 4.64.0
+ '@rollup/rollup-freebsd-x64': 4.64.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.64.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.64.0
+ '@rollup/rollup-linux-arm64-gnu': 4.64.0
+ '@rollup/rollup-linux-arm64-musl': 4.64.0
+ '@rollup/rollup-linux-loong64-gnu': 4.64.0
+ '@rollup/rollup-linux-loong64-musl': 4.64.0
+ '@rollup/rollup-linux-ppc64-gnu': 4.64.0
+ '@rollup/rollup-linux-ppc64-musl': 4.64.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.64.0
+ '@rollup/rollup-linux-riscv64-musl': 4.64.0
+ '@rollup/rollup-linux-s390x-gnu': 4.64.0
+ '@rollup/rollup-linux-x64-gnu': 4.64.0
+ '@rollup/rollup-linux-x64-musl': 4.64.0
+ '@rollup/rollup-openbsd-x64': 4.64.0
+ '@rollup/rollup-openharmony-arm64': 4.64.0
+ '@rollup/rollup-win32-arm64-msvc': 4.64.0
+ '@rollup/rollup-win32-ia32-msvc': 4.64.0
+ '@rollup/rollup-win32-x64-gnu': 4.64.0
+ '@rollup/rollup-win32-x64-msvc': 4.64.0
+ fsevents: 2.3.3
+
+ semver@7.8.5: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ source-map-js@1.2.2: {}
+
+ speakingurl@14.0.1: {}
+
+ superjson@2.2.6:
+ dependencies:
+ copy-anything: 4.1.5
+
+ supports-color@10.2.2: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.7)
+ picomatch: 4.0.7
+
+ ts-api-utils@2.5.0(typescript@5.9.3):
+ dependencies:
+ typescript: 5.9.3
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ type-fest@4.41.0: {}
+
+ typescript-eslint@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
+ '@typescript-eslint/parser': 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
+ '@typescript-eslint/typescript-estree': 8.71.0(supports-color@10.2.2)(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
+ eslint: 10.12.0(supports-color@10.2.2)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@5.9.3: {}
+
+ undici-types@7.24.6: {}
+
+ uri-js-replace@1.0.1: {}
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ util-deprecate@1.0.2: {}
+
+ vite@7.3.6(@types/node@24.19.1):
+ dependencies:
+ esbuild: 0.28.2
+ fdir: 6.5.0(picomatch@4.0.7)
+ picomatch: 4.0.7
+ postcss: 8.5.28
+ rollup: 4.64.0
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 24.19.1
+ fsevents: 2.3.3
+
+ vscode-uri@3.2.0: {}
+
+ vue-component-type-helpers@3.3.12: {}
+
+ vue-eslint-parser@10.4.1(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2):
+ dependencies:
+ debug: 4.4.3(supports-color@10.2.2)
+ eslint: 10.12.0(supports-color@10.2.2)
+ eslint-scope: 9.1.2
+ eslint-visitor-keys: 5.0.1
+ espree: 11.2.0
+ esquery: 1.7.0
+ semver: 7.8.5
+ transitivePeerDependencies:
+ - supports-color
+
+ vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)):
+ dependencies:
+ '@vue/devtools-api': 6.6.4
+ vue: 3.5.43(typescript@5.9.3)
+
+ vue-tsc@3.3.12(typescript@5.9.3):
+ dependencies:
+ '@volar/typescript': 2.4.28(typescript@5.9.3)
+ '@vue/language-core': 3.3.12
+ typescript: 5.9.3
+
+ vue@3.5.43(typescript@5.9.3):
+ dependencies:
+ '@vue/compiler-dom': 3.5.43
+ '@vue/compiler-sfc': 3.5.43
+ '@vue/runtime-dom': 3.5.43
+ '@vue/server-renderer': 3.5.43
+ '@vue/shared': 3.5.43
+ optionalDependencies:
+ typescript: 5.9.3
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ word-wrap@1.2.5: {}
+
+ xml-name-validator@5.0.0: {}
+
+ yaml-ast-parser@0.0.43: {}
+
+ yargs-parser@21.1.1: {}
+
+ yocto-queue@0.1.0: {}
diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml
new file mode 100644
index 0000000..51f6505
--- /dev/null
+++ b/frontend/pnpm-workspace.yaml
@@ -0,0 +1,4 @@
+saveExact: true
+verifyDepsBeforeRun: false
+allowBuilds:
+ esbuild: true
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
new file mode 100644
index 0000000..1e40838
--- /dev/null
+++ b/frontend/src/App.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts
new file mode 100644
index 0000000..ac8260f
--- /dev/null
+++ b/frontend/src/api/client.ts
@@ -0,0 +1,6 @@
+import createClient from 'openapi-fetch'
+import type { paths } from './generated/schema'
+
+export const api = createClient({
+ baseUrl: import.meta.env.VITE_API_BASE_URL || '',
+})
diff --git a/frontend/src/api/generated/schema.d.ts b/frontend/src/api/generated/schema.d.ts
new file mode 100644
index 0000000..1872ce9
--- /dev/null
+++ b/frontend/src/api/generated/schema.d.ts
@@ -0,0 +1,75 @@
+/**
+ * This file was auto-generated by openapi-typescript.
+ * Do not make direct changes to the file.
+ */
+
+export interface paths {
+ "/api/v1/health/live": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Liveness
+ * @description Check process liveness only; does not attest DB, queue or judge readiness.
+ */
+ get: operations["liveness"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+}
+export type webhooks = Record;
+export interface components {
+ schemas: {
+ /** HealthResponse */
+ HealthResponse: {
+ /**
+ * Service
+ * @constant
+ */
+ service: "platform";
+ /**
+ * Status
+ * @constant
+ */
+ status: "ok";
+ /** Version */
+ version: string;
+ };
+ };
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+export type $defs = Record;
+export interface operations {
+ liveness: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HealthResponse"];
+ };
+ };
+ };
+ };
+}
diff --git a/frontend/src/api/health.ts b/frontend/src/api/health.ts
new file mode 100644
index 0000000..855d682
--- /dev/null
+++ b/frontend/src/api/health.ts
@@ -0,0 +1,9 @@
+import { api } from './client'
+
+export async function fetchLiveness(signal: AbortSignal) {
+ const { data, error, response } = await api.GET('/api/v1/health/live', { signal })
+ if (!response.ok || error || !data) {
+ throw new Error(`平台暂时无法连接(HTTP ${response.status})`)
+ }
+ return data
+}
diff --git a/frontend/src/composables/usePlatformHealth.ts b/frontend/src/composables/usePlatformHealth.ts
new file mode 100644
index 0000000..ffaa31e
--- /dev/null
+++ b/frontend/src/composables/usePlatformHealth.ts
@@ -0,0 +1,28 @@
+import { onMounted, onScopeDispose, ref } from 'vue'
+import { fetchLiveness } from '../api/health'
+
+export function usePlatformHealth() {
+ const state = ref<'loading' | 'connected' | 'unavailable'>('loading')
+ const version = ref('')
+ let controller: AbortController | undefined
+
+ async function refresh() {
+ controller?.abort()
+ const request = new AbortController()
+ controller = request
+ state.value = 'loading'
+ try {
+ const health = await fetchLiveness(request.signal)
+ if (!request.signal.aborted) {
+ version.value = health.version
+ state.value = 'connected'
+ }
+ } catch {
+ if (!request.signal.aborted) state.value = 'unavailable'
+ }
+ }
+
+ onMounted(refresh)
+ onScopeDispose(() => controller?.abort())
+ return { state, version, refresh }
+}
diff --git a/frontend/src/layouts/AppLayout.vue b/frontend/src/layouts/AppLayout.vue
new file mode 100644
index 0000000..efa2fbd
--- /dev/null
+++ b/frontend/src/layouts/AppLayout.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
new file mode 100644
index 0000000..00dddce
--- /dev/null
+++ b/frontend/src/main.ts
@@ -0,0 +1,8 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import 'element-plus/dist/index.css'
+import App from './App.vue'
+import { router } from './router'
+import './styles/base.css'
+
+createApp(App).use(createPinia()).use(router).mount('#app')
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
new file mode 100644
index 0000000..79b992a
--- /dev/null
+++ b/frontend/src/router/index.ts
@@ -0,0 +1,13 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+export const router = createRouter({
+ history: createWebHistory(),
+ routes: [
+ { path: '/', name: 'home', component: () => import('../views/HomeView.vue') },
+ {
+ path: '/:pathMatch(.*)*',
+ name: 'not-found',
+ component: () => import('../views/NotFoundView.vue'),
+ },
+ ],
+})
diff --git a/frontend/src/styles/base.css b/frontend/src/styles/base.css
new file mode 100644
index 0000000..ce5705c
--- /dev/null
+++ b/frontend/src/styles/base.css
@@ -0,0 +1,105 @@
+:root {
+ font-family: Inter, 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
+ color: #18243c;
+ background: #f5f7fc;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ --el-color-primary: #2563eb;
+}
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+}
+a {
+ color: inherit;
+ text-decoration: none;
+}
+button,
+a {
+ -webkit-tap-highlight-color: transparent;
+}
+.app-shell {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+.app-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 24px 6vw;
+ border-bottom: 1px solid #e4e9f2;
+ background: #fff;
+}
+.brand {
+ font-weight: 800;
+ font-size: 24px;
+ color: #2563eb;
+}
+.brand span {
+ font-size: 17px;
+ margin-left: 10px;
+ color: #18243c;
+}
+.header-caption,
+.app-footer {
+ color: #65738b;
+ font-size: 14px;
+}
+.app-main {
+ width: min(100% - 40px, 1040px);
+ margin: 0 auto;
+ flex: 1;
+}
+.welcome {
+ padding: 80px 0;
+ max-width: 720px;
+}
+h1 {
+ font-size: clamp(32px, 5vw, 52px);
+ line-height: 1.3;
+ letter-spacing: -1px;
+ margin: 24px 0;
+}
+.welcome-description {
+ color: #65738b;
+ font-size: 17px;
+ line-height: 1.8;
+ max-width: 620px;
+}
+.connection-card {
+ margin-top: 40px;
+ border-radius: 14px;
+}
+.connection-row {
+ display: flex;
+ gap: 24px;
+ align-items: center;
+ justify-content: space-between;
+}
+.connection-row h2 {
+ font-size: 16px;
+ margin: 0 0 8px;
+}
+.connection-row p {
+ font-size: 14px;
+ color: #65738b;
+ margin: 0;
+}
+.app-footer {
+ padding: 24px 6vw;
+}
+@media (max-width: 600px) {
+ .header-caption {
+ display: none;
+ }
+ .welcome {
+ padding-top: 48px;
+ }
+ .connection-row {
+ flex-wrap: wrap;
+ }
+}
diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue
new file mode 100644
index 0000000..feec2ac
--- /dev/null
+++ b/frontend/src/views/HomeView.vue
@@ -0,0 +1,36 @@
+
+
+
+
+ 开发预览
+ 从一道题开始,
积累每一次进步。
+
+ KF-OnlineJudge 正在建设中。题目练习、提交评测和个人学习记录将在后续版本开放。
+
+
+
+
+
平台连接
+
+ {{ statusText }} · {{ version }}
+
+
+
重新检查
+
+
+
+
diff --git a/frontend/src/views/NotFoundView.vue b/frontend/src/views/NotFoundView.vue
new file mode 100644
index 0000000..0fbca32
--- /dev/null
+++ b/frontend/src/views/NotFoundView.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ 返回首页
+
+
+
+
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..a216dce
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "noEmit": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "verbatimModuleSyntax": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "types": ["vite/client", "node"]
+ },
+ "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..cacb854
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig, loadEnv } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig(({ mode }) => {
+ const env = loadEnv(mode, process.cwd(), '')
+ return {
+ plugins: [vue()],
+ server: {
+ port: 5173,
+ strictPort: true,
+ proxy: {
+ '/api': { target: env.KF_API_PROXY_TARGET || 'http://127.0.0.1:8000' },
+ },
+ },
+ preview: { port: 4173, strictPort: true },
+ }
+})
diff --git a/judge/Cargo.lock b/judge/Cargo.lock
new file mode 100644
index 0000000..1699e97
--- /dev/null
+++ b/judge/Cargo.lock
@@ -0,0 +1,911 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "axum"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
+dependencies = [
+ "axum-core",
+ "bytes",
+ "form_urlencoded",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde_core",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
+
+[[package]]
+name = "clap"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[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 = "futures-channel"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
+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 = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
+dependencies = [
+ "bytes",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kf-contracts"
+version = "0.0.1"
+dependencies = [
+ "serde",
+ "serde_json",
+ "uuid",
+]
+
+[[package]]
+name = "kf-coordinator"
+version = "0.0.1"
+dependencies = [
+ "axum",
+ "clap",
+ "kf-contracts",
+ "serde",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "kf-engine"
+version = "0.0.1"
+dependencies = [
+ "kf-contracts",
+]
+
+[[package]]
+name = "kf-sandbox"
+version = "0.0.1"
+dependencies = [
+ "clap",
+ "kf-contracts",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "kf-worker"
+version = "0.0.1"
+dependencies = [
+ "clap",
+ "kf-contracts",
+ "kf-engine",
+ "kf-sandbox",
+ "serde_json",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
+
+[[package]]
+name = "libc"
+version = "0.2.190"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
+
+[[package]]
+name = "log"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mio"
+version = "1.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.151"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "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 = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
+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"
+
+[[package]]
+name = "thread_local"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "tokio"
+version = "1.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
+dependencies = [
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[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",
+ "tracing",
+]
+
+[[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 = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-serde"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
+dependencies = [
+ "serde",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "serde",
+ "serde_json",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+ "tracing-serde",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a"
+dependencies = [
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/judge/Cargo.toml b/judge/Cargo.toml
new file mode 100644
index 0000000..72995f1
--- /dev/null
+++ b/judge/Cargo.toml
@@ -0,0 +1,35 @@
+[workspace]
+members = [
+ "crates/contracts",
+ "crates/sandbox",
+ "crates/engine",
+ "crates/worker",
+ "crates/coordinator",
+]
+resolver = "3"
+
+[workspace.package]
+version = "0.0.1"
+edition = "2024"
+rust-version = "1.99"
+license = "MIT"
+
+[workspace.dependencies]
+kf-contracts = { path = "crates/contracts" }
+kf-sandbox = { path = "crates/sandbox" }
+kf-engine = { path = "crates/engine" }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+uuid = { version = "1", features = ["serde"] }
+clap = { version = "4", features = ["derive", "env"] }
+tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal"] }
+axum = "0.8"
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
+
+[workspace.lints.rust]
+unsafe_code = "deny"
+
+[workspace.lints.clippy]
+dbg_macro = "deny"
+todo = "deny"
diff --git a/judge/README.md b/judge/README.md
new file mode 100644
index 0000000..1aec378
--- /dev/null
+++ b/judge/README.md
@@ -0,0 +1,29 @@
+# Rust 判题工程
+
+Cargo workspace 分为五个 crate:
+
+| Crate | 职责 | 当前能力 |
+| --- | --- | --- |
+| `kf-contracts` | 跨服务通知和执行契约 | 最小 v1 通知、共享 JSON 样例检查 |
+| `kf-sandbox` | Linux 隔离、限制、计量与清理 | `probe` 只读观察;`run` 拒绝执行 |
+| `kf-engine` | 语言适配、测试点编排、比较与确定性汇总 | 预留库边界,无模拟判定 |
+| `kf-worker` | 拉取、租约、工件准备与本地编排 | `--check` 入口,无消费循环 |
+| `kf-coordinator` | 节点登记、租约、状态与结果写入权 | HTTP 存活检查,无任务端点 |
+
+```bash
+# 从仓库根目录调用。
+make install
+make check-judge
+make sandbox-probe
+make worker-check
+make coordinator
+```
+
+工具链由 `rust-toolchain.toml` 固定,依赖由 `Cargo.lock` 固定。
+协调器默认只监听 `127.0.0.1:8100`,唯一接口是 `/health/live`。
+`KF_COORDINATOR_LISTEN` 可覆盖地址;在业务协议落地前不对外暴露内部服务。
+
+沙箱的 `probe` 输出只表明当前进程看到的 namespace/cgroup/seccomp 信息,
+不代表已经验证创建权限、委派或安全隔离。`run` 返回结构化拒绝和退出码 2。
+真正的权限操作和 `unsafe` 代码只能落在沙箱模块,实施时逐项记录安全理由与 Linux 验证。
+当前统一禁止 `unsafe`,需要时在对应模块明确缩小例外范围。
diff --git a/judge/crates/contracts/Cargo.toml b/judge/crates/contracts/Cargo.toml
new file mode 100644
index 0000000..07b0e0b
--- /dev/null
+++ b/judge/crates/contracts/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "kf-contracts"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+
+[dependencies]
+serde.workspace = true
+uuid.workspace = true
+
+[dev-dependencies]
+serde_json.workspace = true
+
+[lints]
+workspace = true
diff --git a/judge/crates/contracts/src/lib.rs b/judge/crates/contracts/src/lib.rs
new file mode 100644
index 0000000..ff4bbcb
--- /dev/null
+++ b/judge/crates/contracts/src/lib.rs
@@ -0,0 +1,43 @@
+//! Wire contracts only. No OS, networking, database or business orchestration.
+
+use serde::{Deserialize, Serialize};
+use uuid::Uuid;
+
+pub const SCHEMA_VERSION: u16 = 1;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(try_from = "u16", into = "u16")]
+pub struct SchemaVersion;
+
+impl TryFrom for SchemaVersion {
+ type Error = &'static str;
+
+ fn try_from(value: u16) -> Result {
+ if value == SCHEMA_VERSION {
+ Ok(Self)
+ } else {
+ Err("unsupported contract version")
+ }
+ }
+}
+
+impl From for u16 {
+ fn from(_: SchemaVersion) -> Self {
+ SCHEMA_VERSION
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub enum TaskKind {
+ #[serde(rename = "judge.evaluation")]
+ JudgeEvaluation,
+}
+
+/// Notification only; consumers must obtain execution authority from the coordinator.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct JudgeTaskNotification {
+ pub schema_version: SchemaVersion,
+ pub kind: TaskKind,
+ pub evaluation_id: Uuid,
+}
diff --git a/judge/crates/contracts/tests/wire_examples.rs b/judge/crates/contracts/tests/wire_examples.rs
new file mode 100644
index 0000000..d558b76
--- /dev/null
+++ b/judge/crates/contracts/tests/wire_examples.rs
@@ -0,0 +1,29 @@
+use kf_contracts::JudgeTaskNotification;
+use std::{fs, path::Path};
+
+#[test]
+fn shared_examples_match_python_contract() {
+ let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../tests/fixtures/contracts");
+ let mut count = 0;
+ for entry in fs::read_dir(fixtures).expect("shared fixture directory must exist") {
+ let path = entry.expect("fixture entry").path();
+ let name = path.file_name().expect("fixture name").to_string_lossy();
+ if !name.starts_with("judge-task.") || !name.ends_with(".json") {
+ continue;
+ }
+ count += 1;
+ let text = fs::read_to_string(&path).expect("fixture is UTF-8");
+ let decoded = serde_json::from_str::(&text);
+ if name.contains(".invalid-") {
+ assert!(decoded.is_err(), "must reject {name}");
+ } else {
+ let notification = decoded.unwrap_or_else(|error| panic!("{name}: {error}"));
+ let original: serde_json::Value = serde_json::from_str(&text).expect("valid JSON");
+ assert_eq!(serde_json::to_value(notification).unwrap(), original);
+ }
+ }
+ assert!(
+ count >= 5,
+ "shared acceptance/rejection cases must not disappear"
+ );
+}
diff --git a/judge/crates/coordinator/Cargo.toml b/judge/crates/coordinator/Cargo.toml
new file mode 100644
index 0000000..66853bc
--- /dev/null
+++ b/judge/crates/coordinator/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "kf-coordinator"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+
+[dependencies]
+kf-contracts.workspace = true
+axum.workspace = true
+tokio.workspace = true
+clap.workspace = true
+serde.workspace = true
+tracing.workspace = true
+tracing-subscriber.workspace = true
+
+[lints]
+workspace = true
diff --git a/judge/crates/coordinator/src/api.rs b/judge/crates/coordinator/src/api.rs
new file mode 100644
index 0000000..4c0d7e7
--- /dev/null
+++ b/judge/crates/coordinator/src/api.rs
@@ -0,0 +1,21 @@
+use axum::{Json, Router, routing::get};
+use serde::Serialize;
+
+#[derive(Serialize)]
+struct HealthResponse {
+ status: &'static str,
+ service: &'static str,
+ version: &'static str,
+}
+
+async fn liveness() -> Json {
+ Json(HealthResponse {
+ status: "ok",
+ service: "coordinator",
+ version: env!("CARGO_PKG_VERSION"),
+ })
+}
+
+pub fn router() -> Router {
+ Router::new().route("/health/live", get(liveness))
+}
diff --git a/judge/crates/coordinator/src/lib.rs b/judge/crates/coordinator/src/lib.rs
new file mode 100644
index 0000000..9f05be0
--- /dev/null
+++ b/judge/crates/coordinator/src/lib.rs
@@ -0,0 +1,6 @@
+//! Internal HTTP process and future lease/state authority.
+//!
+//! Only process liveness is exposed now. SQLx repositories and task transitions
+//! are added after DR-01/PF-01 define their shared schema and concurrency rules.
+
+pub mod api;
diff --git a/judge/crates/coordinator/src/main.rs b/judge/crates/coordinator/src/main.rs
new file mode 100644
index 0000000..95d91ba
--- /dev/null
+++ b/judge/crates/coordinator/src/main.rs
@@ -0,0 +1,31 @@
+use clap::Parser;
+use std::net::SocketAddr;
+use tokio::net::TcpListener;
+use tracing_subscriber::EnvFilter;
+
+#[derive(Parser)]
+#[command(version, about = "KF judge coordinator HTTP shell")]
+struct Cli {
+ #[arg(long, env = "KF_COORDINATOR_LISTEN", default_value = "127.0.0.1:8100")]
+ listen: SocketAddr,
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ tracing_subscriber::fmt()
+ .json()
+ .with_env_filter(
+ EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
+ )
+ .init();
+ let listener = TcpListener::bind(Cli::parse().listen).await?;
+ tracing::info!(address = %listener.local_addr()?, "coordinator scaffold listening");
+ axum::serve(listener, kf_coordinator::api::router())
+ .with_graceful_shutdown(async {
+ if let Err(error) = tokio::signal::ctrl_c().await {
+ tracing::error!(%error, "failed to listen for shutdown signal");
+ }
+ })
+ .await?;
+ Ok(())
+}
diff --git a/judge/crates/engine/Cargo.toml b/judge/crates/engine/Cargo.toml
new file mode 100644
index 0000000..c725af3
--- /dev/null
+++ b/judge/crates/engine/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "kf-engine"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+
+[dependencies]
+kf-contracts.workspace = true
+
+[lints]
+workspace = true
diff --git a/judge/crates/engine/src/lib.rs b/judge/crates/engine/src/lib.rs
new file mode 100644
index 0000000..6956846
--- /dev/null
+++ b/judge/crates/engine/src/lib.rs
@@ -0,0 +1,6 @@
+//! Reserved for deterministic judging orchestration (JS-05/JS-06).
+//!
+//! Owns language adaptation, case order, output comparison and verdict aggregation.
+//! Execution is supplied through a port; this crate must not access SQL, AMQP,
+//! HTTP, privilege setup or hidden host paths directly.
+//! No fake verdict or user-program execution is provided by this scaffold.
diff --git a/judge/crates/sandbox/Cargo.toml b/judge/crates/sandbox/Cargo.toml
new file mode 100644
index 0000000..c36bdf2
--- /dev/null
+++ b/judge/crates/sandbox/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "kf-sandbox"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+
+[dependencies]
+kf-contracts.workspace = true
+clap.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+
+[lints]
+workspace = true
diff --git a/judge/crates/sandbox/src/lib.rs b/judge/crates/sandbox/src/lib.rs
new file mode 100644
index 0000000..2181421
--- /dev/null
+++ b/judge/crates/sandbox/src/lib.rs
@@ -0,0 +1,6 @@
+//! Linux execution boundary. Current implementation only provides read-only observations.
+//!
+//! JS-01 defines execution contracts; JS-02..04 add the launcher, isolation and limits.
+//! Until that work is verified, the CLI cannot execute any user program.
+
+pub mod probe;
diff --git a/judge/crates/sandbox/src/main.rs b/judge/crates/sandbox/src/main.rs
new file mode 100644
index 0000000..05f26e8
--- /dev/null
+++ b/judge/crates/sandbox/src/main.rs
@@ -0,0 +1,38 @@
+use clap::{Parser, Subcommand};
+use std::process::ExitCode;
+
+#[derive(Parser)]
+#[command(version, about = "KF Linux sandbox entrypoint")]
+struct Cli {
+ #[command(subcommand)]
+ command: Command,
+}
+
+#[derive(Subcommand)]
+enum Command {
+ /// Observe the current Linux environment without changing it.
+ Probe,
+ /// Reserved execution entrypoint; refuses execution until isolation is implemented.
+ Run,
+}
+
+fn main() -> ExitCode {
+ match Cli::parse().command {
+ Command::Probe => match serde_json::to_string_pretty(&kf_sandbox::probe::observe()) {
+ Ok(report) => {
+ println!("{report}");
+ ExitCode::SUCCESS
+ }
+ Err(error) => {
+ eprintln!("cannot encode environment observation: {error}");
+ ExitCode::FAILURE
+ }
+ },
+ Command::Run => {
+ eprintln!(
+ "{{\"error\":\"sandbox_not_implemented\",\"message\":\"isolated execution is unavailable\"}}"
+ );
+ ExitCode::from(2)
+ }
+ }
+}
diff --git a/judge/crates/sandbox/src/probe.rs b/judge/crates/sandbox/src/probe.rs
new file mode 100644
index 0000000..45dea35
--- /dev/null
+++ b/judge/crates/sandbox/src/probe.rs
@@ -0,0 +1,49 @@
+use serde::Serialize;
+use std::fs;
+
+#[derive(Debug, Serialize)]
+pub struct EnvironmentObservation {
+ pub os: &'static str,
+ pub namespace_entries: Vec,
+ pub cgroup_v2_visible: bool,
+ pub cgroup_v2_controllers: Vec,
+ pub current_process_seccomp_mode: Option,
+ pub isolation_verified: bool,
+}
+
+/// Filesystem observations do not verify delegation, syscall access or safe execution.
+pub fn observe() -> EnvironmentObservation {
+ let mut namespace_entries: Vec = fs::read_dir("/proc/self/ns")
+ .map(|entries| {
+ entries
+ .filter_map(Result::ok)
+ .map(|entry| entry.file_name().to_string_lossy().into_owned())
+ .collect()
+ })
+ .unwrap_or_default();
+ namespace_entries.sort();
+ let controllers = fs::read_to_string("/sys/fs/cgroup/cgroup.controllers");
+ let cgroup_v2_visible = controllers.is_ok();
+ let cgroup_v2_controllers = controllers
+ .unwrap_or_default()
+ .split_whitespace()
+ .map(str::to_owned)
+ .collect();
+ let current_process_seccomp_mode =
+ fs::read_to_string("/proc/self/status")
+ .ok()
+ .and_then(|status| {
+ status.lines().find_map(|line| {
+ line.strip_prefix("Seccomp:")
+ .and_then(|mode| mode.trim().parse().ok())
+ })
+ });
+ EnvironmentObservation {
+ os: std::env::consts::OS,
+ namespace_entries,
+ cgroup_v2_visible,
+ cgroup_v2_controllers,
+ current_process_seccomp_mode,
+ isolation_verified: false,
+ }
+}
diff --git a/judge/crates/sandbox/tests/refuse_execution.rs b/judge/crates/sandbox/tests/refuse_execution.rs
new file mode 100644
index 0000000..7a0e980
--- /dev/null
+++ b/judge/crates/sandbox/tests/refuse_execution.rs
@@ -0,0 +1,14 @@
+use std::process::Command;
+
+#[test]
+fn execution_entrypoint_fails_closed() {
+ let output = Command::new(env!("CARGO_BIN_EXE_kf-sandbox"))
+ .arg("run")
+ .output()
+ .expect("sandbox binary must start");
+ assert_eq!(output.status.code(), Some(2));
+ assert!(output.stdout.is_empty());
+ let error: serde_json::Value =
+ serde_json::from_slice(&output.stderr).expect("structured refusal");
+ assert_eq!(error["error"], "sandbox_not_implemented");
+}
diff --git a/judge/crates/worker/Cargo.toml b/judge/crates/worker/Cargo.toml
new file mode 100644
index 0000000..71152e8
--- /dev/null
+++ b/judge/crates/worker/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "kf-worker"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+
+[dependencies]
+kf-contracts.workspace = true
+kf-engine.workspace = true
+kf-sandbox.workspace = true
+clap.workspace = true
+serde_json.workspace = true
+
+[lints]
+workspace = true
diff --git a/judge/crates/worker/src/main.rs b/judge/crates/worker/src/main.rs
new file mode 100644
index 0000000..893ae94
--- /dev/null
+++ b/judge/crates/worker/src/main.rs
@@ -0,0 +1,28 @@
+use clap::Parser;
+use std::process::ExitCode;
+
+#[derive(Parser)]
+#[command(version, about = "KF judge worker entrypoint")]
+struct Cli {
+ /// Check the process entrypoint without consuming or executing tasks.
+ #[arg(long)]
+ check: bool,
+}
+
+fn main() -> ExitCode {
+ if !Cli::parse().check {
+ eprintln!("judge worker task loop is not implemented; use --check");
+ return ExitCode::from(2);
+ }
+ println!(
+ "{}",
+ serde_json::json!({
+ "service": "judge-worker",
+ "version": env!("CARGO_PKG_VERSION"),
+ "status": "scaffold",
+ "contract_version": kf_contracts::SCHEMA_VERSION,
+ "consuming_tasks": false
+ })
+ );
+ ExitCode::SUCCESS
+}
diff --git a/judge/rust-toolchain.toml b/judge/rust-toolchain.toml
new file mode 100644
index 0000000..b432996
--- /dev/null
+++ b/judge/rust-toolchain.toml
@@ -0,0 +1,4 @@
+[toolchain]
+channel = "1.99.0"
+profile = "minimal"
+components = ["rustfmt", "clippy"]
diff --git a/scripts/check_architecture.py b/scripts/check_architecture.py
new file mode 100644
index 0000000..b6dac3e
--- /dev/null
+++ b/scripts/check_architecture.py
@@ -0,0 +1,109 @@
+"""Enforce Python import direction and the Rust internal crate dependency graph."""
+
+import ast
+import sys
+import tomllib
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+PACKAGE = ROOT / "backend/src/kf_oj"
+RUST_ALLOWED = {
+ "kf-contracts": set(),
+ "kf-sandbox": {"kf-contracts"},
+ "kf-engine": {"kf-contracts"},
+ "kf-coordinator": {"kf-contracts"},
+ "kf-worker": {"kf-contracts", "kf-engine", "kf-sandbox"},
+}
+PYTHON_ALLOWED = {
+ "contracts": {"contracts"},
+ "core": {"core", "contracts"},
+ "infra": {"infra", "core", "contracts"},
+ "runtime": {"runtime", "core", "contracts"},
+ "modules": {"modules", "runtime", "infra", "core", "contracts"},
+ "api": {"api", "modules", "runtime", "infra", "core", "contracts"},
+ "workers": {"workers", "modules", "runtime", "infra", "core", "contracts"},
+}
+
+
+def imports(tree: ast.AST, package: str):
+ for node in ast.walk(tree):
+ if isinstance(node, ast.Import):
+ for alias in node.names:
+ yield alias.name
+ elif isinstance(node, ast.ImportFrom):
+ if node.level:
+ parts = package.split(".")
+ base = ".".join(parts[: len(parts) - node.level + 1])
+ target = ".".join(filter(None, [base, node.module]))
+ else:
+ target = node.module or ""
+ for alias in node.names:
+ yield target if alias.name == "*" else f"{target}.{alias.name}"
+
+
+def python_violations() -> list[str]:
+ violations = []
+ for path in sorted(PACKAGE.rglob("*.py")):
+ relative = path.relative_to(PACKAGE)
+ if len(relative.parts) < 2:
+ continue
+ # Offline build tooling composes the API to export its schema, not a runtime dependency.
+ if relative.as_posix() == "contracts/export.py":
+ continue
+ layer = relative.parts[0]
+ package = "kf_oj." + ".".join(relative.parts[:-1])
+ for target in imports(ast.parse(path.read_text(encoding="utf-8")), package):
+ parts = target.split(".")
+ if len(parts) < 2 or parts[0] != "kf_oj" or parts[1] == "__version__":
+ continue
+ if parts[1] not in PYTHON_ALLOWED.get(layer, set()):
+ violations.append(f"{path.relative_to(ROOT)}: {layer} cannot import {target}")
+ elif layer == "modules" and parts[1] == "modules" and len(parts) >= 3:
+ own_module = relative.parts[1]
+ if parts[2] != own_module and (len(parts) < 4 or parts[3] != "public"):
+ violations.append(
+ f"{path.relative_to(ROOT)}: cross-module import must use public.py: "
+ f"{target}"
+ )
+ return violations
+
+
+def rust_violations() -> list[str]:
+ violations = []
+ manifests = list((ROOT / "judge/crates").glob("*/Cargo.toml"))
+ found = set()
+ for path in manifests:
+ manifest = tomllib.loads(path.read_text(encoding="utf-8"))
+ name = manifest["package"]["name"]
+ found.add(name)
+ if name not in RUST_ALLOWED:
+ violations.append(f"{path.relative_to(ROOT)}: declare the new crate boundary first")
+ continue
+ groups = [manifest]
+ groups.extend(manifest.get("target", {}).values())
+ for group in groups:
+ for section in ("dependencies", "dev-dependencies", "build-dependencies"):
+ for key, dependency in group.get(section, {}).items():
+ actual = dependency.get("package", key) if isinstance(dependency, dict) else key
+ if actual.startswith("kf-") and actual not in RUST_ALLOWED[name]:
+ violations.append(f"{name} cannot depend on {actual}")
+ if name in {"kf-contracts", "kf-engine"}:
+ allowed_external = {"serde", "serde_json", "uuid"}
+ for key in manifest.get("dependencies", {}):
+ if not key.startswith("kf-") and key not in allowed_external:
+ violations.append(f"{name}: domain/contract crate cannot depend on {key}")
+ if found != set(RUST_ALLOWED):
+ violations.append("Rust workspace crates differ from the declared architecture")
+ return violations
+
+
+def main() -> None:
+ violations = python_violations() + rust_violations()
+ if violations:
+ print("\n".join(violations), file=sys.stderr)
+ raise SystemExit(1)
+ print("Architecture boundaries: OK (Python imports + Rust crate graph)")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/skills/README.md b/skills/README.md
new file mode 100644
index 0000000..79646a0
--- /dev/null
+++ b/skills/README.md
@@ -0,0 +1,7 @@
+# 平台内置技能
+
+此目录预留给 KF-OnlineJudge 自有的声明式内置技能:提交复盘、渐进式答疑、专项训练和阶段复习。
+这里的内容属于产品数据,不是指示开发工具运行的 Codex Skills。
+
+包格式和许可规则在 SP-01 中固定后再添加实际技能。
+个人绑定、配置、发布记录和私有学习证据由后端对应业务模块管理;不存放在共享包中。
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..28ee8ff
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,7 @@
+# 跨工程测试材料
+
+`fixtures/contracts/` 保存 Python/Rust 共用的合法与非法协议样例。
+各工程自身的测试仍放在 `backend/tests/` 与 Rust crate 的 `tests/` 中。
+
+后续在这里增加构造题目、正常/异常程序及端到端夹具,不提交真实私有代码或隐藏题库。
+沙箱破坏性测试在明确的专用执行环境中进行,不由普通构建检查自动运行。
diff --git a/tests/fixtures/contracts/judge-task.invalid-extra.json b/tests/fixtures/contracts/judge-task.invalid-extra.json
new file mode 100644
index 0000000..ba47cf6
--- /dev/null
+++ b/tests/fixtures/contracts/judge-task.invalid-extra.json
@@ -0,0 +1,6 @@
+{
+ "schema_version": 1,
+ "kind": "judge.evaluation",
+ "evaluation_id": "018f26d6-713c-7b7f-8d5d-5f832a5f47f2",
+ "source_code": "not allowed in notification"
+}
diff --git a/tests/fixtures/contracts/judge-task.invalid-id.json b/tests/fixtures/contracts/judge-task.invalid-id.json
new file mode 100644
index 0000000..9415eda
--- /dev/null
+++ b/tests/fixtures/contracts/judge-task.invalid-id.json
@@ -0,0 +1,5 @@
+{
+ "schema_version": 1,
+ "kind": "judge.evaluation",
+ "evaluation_id": "not-a-uuid"
+}
diff --git a/tests/fixtures/contracts/judge-task.invalid-kind.json b/tests/fixtures/contracts/judge-task.invalid-kind.json
new file mode 100644
index 0000000..98df0fe
--- /dev/null
+++ b/tests/fixtures/contracts/judge-task.invalid-kind.json
@@ -0,0 +1,5 @@
+{
+ "schema_version": 1,
+ "kind": "learning.run",
+ "evaluation_id": "018f26d6-713c-7b7f-8d5d-5f832a5f47f2"
+}
diff --git a/tests/fixtures/contracts/judge-task.invalid-version.json b/tests/fixtures/contracts/judge-task.invalid-version.json
new file mode 100644
index 0000000..750a63c
--- /dev/null
+++ b/tests/fixtures/contracts/judge-task.invalid-version.json
@@ -0,0 +1,5 @@
+{
+ "schema_version": 2,
+ "kind": "judge.evaluation",
+ "evaluation_id": "018f26d6-713c-7b7f-8d5d-5f832a5f47f2"
+}
diff --git a/tests/fixtures/contracts/judge-task.valid.json b/tests/fixtures/contracts/judge-task.valid.json
new file mode 100644
index 0000000..0a8cc84
--- /dev/null
+++ b/tests/fixtures/contracts/judge-task.valid.json
@@ -0,0 +1,5 @@
+{
+ "schema_version": 1,
+ "kind": "judge.evaluation",
+ "evaluation_id": "018f26d6-713c-7b7f-8d5d-5f832a5f47f2"
+}
From 720c69a198923c8e25646d29cfd3b3ec78840add Mon Sep 17 00:00:00 2001
From: kronecker <2026692687@qq.com>
Date: Mon, 5 Oct 2026 01:48:51 +0800
Subject: [PATCH 2/8] chore(frontend): configure UI dependencies and Vue
linting
Pin icon components, Monaco Editor and TagCanvas modules in the lockfile.
Let TypeScript validate identifiers in Vue script setup.
---
frontend/eslint.config.js | 3 ++-
frontend/package.json | 3 +++
frontend/pnpm-lock.yaml | 42 +++++++++++++++++++++++++++++++++++++++
3 files changed, 47 insertions(+), 1 deletion(-)
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
index 060e21e..d00c768 100644
--- a/frontend/eslint.config.js
+++ b/frontend/eslint.config.js
@@ -11,7 +11,8 @@ export default tseslint.config(
{
files: ['**/*.vue'],
languageOptions: { parserOptions: { parser: tseslint.parser } },
- rules: { 'vue/multi-word-component-names': 'off' },
+ // TypeScript checks identifiers and DOM globals in Vue's script setup.
+ rules: { 'vue/multi-word-component-names': 'off', 'no-undef': 'off' },
},
{
files: ['vite.config.ts'],
diff --git a/frontend/package.json b/frontend/package.json
index 6efa910..59b7459 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -19,7 +19,10 @@
"check:api": "pnpm generate:api --check"
},
"dependencies": {
+ "@element-plus/icons-vue": "2.3.2",
+ "@misskey-dev/tagcanvas-es": "0.1.2",
"element-plus": "^2.9.0",
+ "monaco-editor": "0.57.0",
"openapi-fetch": "^0.14.0",
"pinia": "^3.0.0",
"vue": "^3.5.0",
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 59fbebd..8616279 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -8,9 +8,18 @@ importers:
.:
dependencies:
+ '@element-plus/icons-vue':
+ specifier: 2.3.2
+ version: 2.3.2(vue@3.5.43(typescript@5.9.3))
+ '@misskey-dev/tagcanvas-es':
+ specifier: 0.1.2
+ version: 0.1.2
element-plus:
specifier: ^2.9.0
version: 2.14.7(vue@3.5.43(typescript@5.9.3))
+ monaco-editor:
+ specifier: 0.57.0
+ version: 0.57.0
openapi-fetch:
specifier: ^0.14.0
version: 0.14.1
@@ -335,6 +344,9 @@ packages:
'@keyv/serialize@1.1.1':
resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
+ '@misskey-dev/tagcanvas-es@0.1.2':
+ resolution: {integrity: sha512-SGqyu30/CCQyIC95Zcmj3k1vYXwfX66/arsxpl1UUulUqvlRpTpuhB7kEAAps1lqXin0rQzXknb31EeveNKsaw==}
+
'@napi-rs/lzma-linux-x64-gnu@1.5.1':
resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
engines: {node: ^22.20 || ^24.12 || >=25}
@@ -514,6 +526,9 @@ packages:
'@types/node@24.19.1':
resolution: {integrity: sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==}
+ '@types/trusted-types@2.0.7':
+ resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
+
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
@@ -745,6 +760,9 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ dompurify@3.4.15:
+ resolution: {integrity: sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==}
+
element-plus@2.14.7:
resolution: {integrity: sha512-lrG7pZyCS+mpCNVpzxxyu5r35+T6V0hzkAeIwn7HA6to772XSTHKuYCZG51OfzlqaxGbNegLxs650zAF1j43gA==}
peerDependencies:
@@ -959,6 +977,11 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+ marked@14.0.0:
+ resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==}
+ engines: {node: '>= 18'}
+ hasBin: true
+
memoize-one@6.0.0:
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
@@ -973,6 +996,9 @@ packages:
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+ monaco-editor@0.57.0:
+ resolution: {integrity: sha512-5BkI9KGoqrNvBGUe15/QlZq3OooZ8WLg1AxTpaqHRCP3HNpzPPZKE2EDz8M7c+VRmCeUw1Brp4cx/PWm3kI/5A==}
+
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -1446,6 +1472,8 @@ snapshots:
'@keyv/serialize@1.1.1': {}
+ '@misskey-dev/tagcanvas-es@0.1.2': {}
+
'@napi-rs/lzma-linux-x64-gnu@1.5.1':
optional: true
@@ -1567,6 +1595,9 @@ snapshots:
dependencies:
undici-types: 7.24.6
+ '@types/trusted-types@2.0.7':
+ optional: true
+
'@types/web-bluetooth@0.0.21': {}
'@typescript-eslint/eslint-plugin@8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)':
@@ -1850,6 +1881,10 @@ snapshots:
deep-is@0.1.4: {}
+ dompurify@3.4.15:
+ optionalDependencies:
+ '@types/trusted-types': 2.0.7
+
element-plus@2.14.7(vue@3.5.43(typescript@5.9.3)):
dependencies:
'@ctrl/tinycolor': 4.2.1
@@ -2093,6 +2128,8 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.6.0
+ marked@14.0.0: {}
+
memoize-one@6.0.0: {}
minimatch@10.2.6:
@@ -2105,6 +2142,11 @@ snapshots:
mitt@3.0.1: {}
+ monaco-editor@0.57.0:
+ dependencies:
+ dompurify: 3.4.15
+ marked: 14.0.0
+
ms@2.1.3: {}
muggle-string@0.4.1: {}
From 9fa25189a2194b5488d5ef98f2e4c4448012d435 Mon Sep 17 00:00:00 2001
From: kronecker <2026692687@qq.com>
Date: Mon, 5 Oct 2026 01:48:53 +0800
Subject: [PATCH 3/8] feat(frontend): establish shared components and blue page
layouts
Add reusable headings, panels, tables, charts, pagination and explicit preview fixtures.
Apply the shared blue theme, responsive top navigation and Chinese UI locale.
---
frontend/src/App.vue | 11 +-
frontend/src/components/DifficultyTag.vue | 15 +
frontend/src/components/EmptyState.vue | 10 +
frontend/src/components/PageHeading.vue | 13 +
frontend/src/components/PanelCard.vue | 15 +
frontend/src/components/ProblemTable.vue | 39 +
frontend/src/components/SubmissionChart.vue | 150 ++
frontend/src/components/SubmissionTable.vue | 40 +
frontend/src/components/UiIcon.vue | 54 +
frontend/src/components/VerdictTag.vue | 9 +
frontend/src/composables/usePagination.ts | 13 +
frontend/src/data/preview.ts | 465 ++++++
frontend/src/env.d.ts | 2 +
frontend/src/layouts/AppLayout.vue | 94 +-
frontend/src/main.ts | 7 +-
frontend/src/plugins/ui.ts | 65 +
frontend/src/styles/base.css | 1437 ++++++++++++++++++-
frontend/src/styles/blueprint.css | 492 +++++++
18 files changed, 2867 insertions(+), 64 deletions(-)
create mode 100644 frontend/src/components/DifficultyTag.vue
create mode 100644 frontend/src/components/EmptyState.vue
create mode 100644 frontend/src/components/PageHeading.vue
create mode 100644 frontend/src/components/PanelCard.vue
create mode 100644 frontend/src/components/ProblemTable.vue
create mode 100644 frontend/src/components/SubmissionChart.vue
create mode 100644 frontend/src/components/SubmissionTable.vue
create mode 100644 frontend/src/components/UiIcon.vue
create mode 100644 frontend/src/components/VerdictTag.vue
create mode 100644 frontend/src/composables/usePagination.ts
create mode 100644 frontend/src/data/preview.ts
create mode 100644 frontend/src/env.d.ts
create mode 100644 frontend/src/plugins/ui.ts
create mode 100644 frontend/src/styles/blueprint.css
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 1e40838..792e08c 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,9 +1,14 @@
-
-
-
+
+
+
+
+
+
+
diff --git a/frontend/src/components/DifficultyTag.vue b/frontend/src/components/DifficultyTag.vue
new file mode 100644
index 0000000..ccac152
--- /dev/null
+++ b/frontend/src/components/DifficultyTag.vue
@@ -0,0 +1,15 @@
+
+
+ {{ value }}
+
diff --git a/frontend/src/components/EmptyState.vue b/frontend/src/components/EmptyState.vue
new file mode 100644
index 0000000..922cbe2
--- /dev/null
+++ b/frontend/src/components/EmptyState.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
{{ description }}
+
+
+
diff --git a/frontend/src/components/PageHeading.vue b/frontend/src/components/PageHeading.vue
new file mode 100644
index 0000000..b11b840
--- /dev/null
+++ b/frontend/src/components/PageHeading.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
{{ eyebrow }}
+
{{ title }}
+
{{ description }}
+
+
+
+
diff --git a/frontend/src/components/PanelCard.vue b/frontend/src/components/PanelCard.vue
new file mode 100644
index 0000000..1f42cfa
--- /dev/null
+++ b/frontend/src/components/PanelCard.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+ {{ title }}{{ subtitle }}
+
+
+
+
+
+
diff --git a/frontend/src/components/ProblemTable.vue b/frontend/src/components/ProblemTable.vue
new file mode 100644
index 0000000..26db178
--- /dev/null
+++ b/frontend/src/components/ProblemTable.vue
@@ -0,0 +1,39 @@
+
+
+
+ {{ row.id }}
+ {{
+ row.title
+ }}
+
+ {{ tag }}
+
+
+ {{ acceptance({ accepted: row.accepted, submissions: row.submissions }) }}%
+
+
+
diff --git a/frontend/src/components/SubmissionChart.vue b/frontend/src/components/SubmissionChart.vue
new file mode 100644
index 0000000..79bc754
--- /dev/null
+++ b/frontend/src/components/SubmissionChart.vue
@@ -0,0 +1,150 @@
+
+
+
+
+ 单位:次 · 示例统计
+
+
+
+ {{
+ selected
+ ? selected.day + ' · 总提交 ' + selected.total + ' 次 · 通过 ' + selected.accepted + ' 次'
+ : '近 7 天的提交与通过情况'
+ }}
+
+
+
+
diff --git a/frontend/src/components/SubmissionTable.vue b/frontend/src/components/SubmissionTable.vue
new file mode 100644
index 0000000..495629f
--- /dev/null
+++ b/frontend/src/components/SubmissionTable.vue
@@ -0,0 +1,40 @@
+
+
+ #{{ row.id }}{{
+ findProblem(row.problemId)?.title
+ }}{{ row.username }}{{
+ row.time === null ? '—' : row.time + ' ms'
+ }}{{
+ row.memory === null ? '—' : row.memory + ' KiB'
+ }}
+
diff --git a/frontend/src/components/UiIcon.vue b/frontend/src/components/UiIcon.vue
new file mode 100644
index 0000000..f11e6cb
--- /dev/null
+++ b/frontend/src/components/UiIcon.vue
@@ -0,0 +1,54 @@
+
+
+
+
diff --git a/frontend/src/components/VerdictTag.vue b/frontend/src/components/VerdictTag.vue
new file mode 100644
index 0000000..007b149
--- /dev/null
+++ b/frontend/src/components/VerdictTag.vue
@@ -0,0 +1,9 @@
+
+
+ {{
+ full ? verdictLabels[value] : value === 'Pending' ? '等待评测' : value
+ }}
+
diff --git a/frontend/src/composables/usePagination.ts b/frontend/src/composables/usePagination.ts
new file mode 100644
index 0000000..6d15e13
--- /dev/null
+++ b/frontend/src/composables/usePagination.ts
@@ -0,0 +1,13 @@
+import { computed, ref, watch, type Ref } from 'vue'
+export function usePagination(items: Readonly[>, initialSize = 10) {
+ const page = ref(1)
+ const pageSize = ref(initialSize)
+ const total = computed(() => items.value.length)
+ const paged = computed(() =>
+ items.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value),
+ )
+ watch([items, pageSize], () => {
+ page.value = 1
+ })
+ return { page, pageSize, total, paged }
+}
diff --git a/frontend/src/data/preview.ts b/frontend/src/data/preview.ts
new file mode 100644
index 0000000..d0b0496
--- /dev/null
+++ b/frontend/src/data/preview.ts
@@ -0,0 +1,465 @@
+// Explicit UI fixtures; business endpoints will be connected through API composables.
+export type Difficulty = '入门' | '简单' | '中等' | '困难'
+export interface Problem {
+ id: string
+ title: string
+ difficulty: Difficulty
+ tags: string[]
+ accepted: number
+ submissions: number
+ updated: string
+ description: string
+ input: string
+ output: string
+ sampleInput: string
+ sampleOutput: string
+ constraint: string
+}
+const definitions: [string, string, Difficulty, string[], number, number][] = [
+ ['1001', 'A + B Problem', '入门', ['模拟', '输入输出'], 428, 536],
+ ['1002', '两数之和', '简单', ['数组', '哈希表'], 315, 482],
+ ['1003', '最长无重复子串', '中等', ['字符串', '滑动窗口'], 206, 527],
+ ['1004', '旅行者的背包', '中等', ['动态规划', '背包'], 186, 461],
+ ['1005', '城市间的最短路径', '中等', ['图论', '最短路'], 152, 387],
+ ['1006', '寻找第一个位置', '简单', ['二分', '数组'], 241, 402],
+ ['1007', '括号匹配', '简单', ['栈', '字符串'], 287, 389],
+ ['1008', '岛屿的数量', '中等', ['搜索', '图论'], 143, 328],
+ ['1009', '区间求和', '中等', ['数据结构', '前缀和'], 124, 306],
+ ['1010', '最小生成树', '困难', ['图论', '并查集'], 96, 288],
+ ['1011', '活动安排', '简单', ['贪心', '排序'], 203, 312],
+ ['1012', '最长公共子序列', '中等', ['动态规划', '字符串'], 112, 345],
+]
+export const problems: Problem[] = definitions.map(
+ ([id, title, difficulty, tags, accepted, submissions], index) => ({
+ id,
+ title,
+ difficulty,
+ tags,
+ accepted,
+ submissions,
+ updated: '2026-10-' + String(4 - Math.floor(index / 4)).padStart(2, '0'),
+ description:
+ id === '1001'
+ ? '给定两个整数 A 和 B,请计算它们的和。认真阅读输入和输出格式,完成你的第一次提交。'
+ : '这道题的完整题面将在题库接入后展示。当前提供列表与编辑器的界面预览。',
+ input:
+ id === '1001'
+ ? '输入一行,包含两个整数 A 和 B,以一个空格分隔。'
+ : '完整输入格式待题库接入。',
+ output: id === '1001' ? '输出一个整数,表示 A 与 B 的和。' : '完整输出格式待题库接入。',
+ sampleInput: id === '1001' ? '1 2' : '',
+ sampleOutput: id === '1001' ? '3' : '',
+ constraint:
+ id === '1001'
+ ? '−10⁹ ≤ A, B ≤ 10⁹。建议使用 64 位整数保存计算结果。'
+ : '数据范围待题库接入。',
+ }),
+)
+export const problemTags = [...new Set(problems.flatMap((problem) => problem.tags))]
+export const rankings = [
+ {
+ username: 'Ada',
+ motto: '每天进步一点点',
+ solved: 167,
+ submissions: 238,
+ rating: 1924,
+ color: '#6196ec',
+ },
+ {
+ username: 'Kronecker',
+ motto: '写下思路,再写代码。',
+ solved: 142,
+ submissions: 216,
+ rating: 1846,
+ color: '#6b70d6',
+ },
+ {
+ username: 'Sora',
+ motto: '今天也要 AC',
+ solved: 129,
+ submissions: 204,
+ rating: 1732,
+ color: '#3fb7a1',
+ },
+ {
+ username: 'Natsume',
+ motto: '慢慢来,比较快',
+ solved: 118,
+ submissions: 182,
+ rating: 1685,
+ color: '#e9a35d',
+ },
+ {
+ username: 'Lin',
+ motto: '保持好奇',
+ solved: 106,
+ submissions: 175,
+ rating: 1621,
+ color: '#dc82a4',
+ },
+ {
+ username: 'Mika',
+ motto: '从一道题开始',
+ solved: 98,
+ submissions: 153,
+ rating: 1594,
+ color: '#63a9c9',
+ },
+ {
+ username: 'Chen',
+ motto: 'Hello, algorithm',
+ solved: 92,
+ submissions: 151,
+ rating: 1540,
+ color: '#897cb7',
+ },
+ {
+ username: 'Yuki',
+ motto: '专注这一题',
+ solved: 87,
+ submissions: 144,
+ rating: 1506,
+ color: '#6aaa8a',
+ },
+ {
+ username: 'Joago',
+ motto: 'Thinking in code',
+ solved: 81,
+ submissions: 132,
+ rating: 1482,
+ color: '#bb9b67',
+ },
+ {
+ username: 'Hyperbola',
+ motto: '努力成为更好的自己',
+ solved: 74,
+ submissions: 128,
+ rating: 1428,
+ color: '#8095b9',
+ },
+]
+export const notices = [
+ {
+ id: '1',
+ title: '欢迎来到 KF OnlineJudge',
+ date: '2026-10-04',
+ category: '平台公告',
+ text: '从基础练习开始,记录每一次提交,让思路在代码里落地。当前页面使用示例内容。',
+ },
+ {
+ id: '2',
+ title: '新学期基础训练题单已更新',
+ date: '2026-10-03',
+ category: '训练',
+ text: '输入输出、数组、字符串和简单搜索,按顺序完成你的第一组算法练习。',
+ },
+ {
+ id: '3',
+ title: '周末练习赛 · 热身题单',
+ date: '2026-10-01',
+ category: '比赛',
+ text: '练习赛页面已准备好,实际比赛安排以平台发布的信息为准。',
+ },
+]
+export const trainings = [
+ {
+ id: 'basics',
+ title: '从零开始 · 编程基础',
+ category: '基础入门',
+ level: '入门',
+ description: '从输入输出到数组与字符串,熟悉在线评测的基本流程。',
+ ids: ['1001', '1002', '1006', '1007'],
+ color: 'blue',
+ icon: '01',
+ },
+ {
+ id: 'search',
+ title: '搜索与图论入门',
+ category: '算法专题',
+ level: '中等',
+ description: '理解遍历、连通性与路径,建立图论的基础解题思路。',
+ ids: ['1008', '1005', '1010'],
+ color: 'green',
+ icon: '02',
+ },
+ {
+ id: 'dp',
+ title: '动态规划 · 一步一步推出来',
+ category: '算法专题',
+ level: '中等',
+ description: '从状态定义开始,练习转移、边界和空间优化。',
+ ids: ['1004', '1012'],
+ color: 'violet',
+ icon: '03',
+ },
+ {
+ id: 'structures',
+ title: '常用数据结构练习',
+ category: '进阶练习',
+ level: '中等',
+ description: '通过具体问题理解哈希表、栈、前缀和与并查集。',
+ ids: ['1002', '1007', '1009', '1010'],
+ color: 'amber',
+ icon: '04',
+ },
+]
+export const contests = [
+ {
+ id: 'weekly-01',
+ title: 'KF 周末练习赛 #01',
+ type: 'ACM',
+ status: '即将开始',
+ start: '2026-10-10 14:00',
+ duration: '2 小时',
+ description: '五道由浅入深的算法题,给自己一个专注思考的下午。',
+ ids: ['1001', '1002', '1003', '1004', '1005'],
+ },
+ {
+ id: 'starter',
+ title: '新同学编程热身赛',
+ type: 'OI',
+ status: '即将开始',
+ start: '2026-10-12 19:00',
+ duration: '2 小时',
+ description: '面向刚开始学习程序设计的同学,练习输入输出与基本逻辑。',
+ ids: ['1001', '1006', '1007'],
+ },
+ {
+ id: 'graph',
+ title: '图论专题 · 路径与连通性',
+ type: 'ACM',
+ status: '进行中',
+ start: '2026-10-04 14:00',
+ duration: '4 小时',
+ description: '题单式比赛界面示例,状态与时间为展示用数据。',
+ ids: ['1005', '1008', '1010'],
+ },
+ {
+ id: 'weekly-00',
+ title: 'KF 周末练习赛 #00',
+ type: 'ACM',
+ status: '已结束',
+ start: '2026-09-27 14:00',
+ duration: '2 小时',
+ description: '往期比赛界面示例,便于浏览题目和比赛说明。',
+ ids: ['1001', '1003', '1011'],
+ },
+ {
+ id: 'dp-practice',
+ title: '动态规划专项练习',
+ type: 'OI',
+ status: '已结束',
+ start: '2026-09-20 09:00',
+ duration: '3 小时',
+ description: '集中练习常见动态规划模型。',
+ ids: ['1004', '1012'],
+ },
+]
+export type Verdict = 'AC' | 'WA' | 'TLE' | 'RE' | 'CE' | 'Pending'
+export const verdictLabels: Record = {
+ AC: 'Accepted',
+ WA: 'Wrong Answer',
+ TLE: 'Time Limit Exceeded',
+ RE: 'Runtime Error',
+ CE: 'Compile Error',
+ Pending: '等待评测',
+}
+export const submissions: {
+ id: string
+ problemId: string
+ username: string
+ verdict: Verdict
+ language: string
+ time: number | null
+ memory: number | null
+ created: string
+}[] = [
+ {
+ id: '10248',
+ problemId: '1001',
+ username: 'Kronecker',
+ verdict: 'AC',
+ language: 'Rust',
+ time: 12,
+ memory: 2048,
+ created: '2026-10-04 14:32:18',
+ },
+ {
+ id: '10247',
+ problemId: '1003',
+ username: 'Ada',
+ verdict: 'AC',
+ language: 'C++',
+ time: 34,
+ memory: 4096,
+ created: '2026-10-04 14:30:05',
+ },
+ {
+ id: '10246',
+ problemId: '1004',
+ username: 'Sora',
+ verdict: 'WA',
+ language: 'Python',
+ time: 62,
+ memory: 8192,
+ created: '2026-10-04 14:28:41',
+ },
+ {
+ id: '10245',
+ problemId: '1005',
+ username: 'Lin',
+ verdict: 'TLE',
+ language: 'C++',
+ time: 1000,
+ memory: 6144,
+ created: '2026-10-04 14:26:22',
+ },
+ {
+ id: '10244',
+ problemId: '1002',
+ username: 'Mika',
+ verdict: 'AC',
+ language: 'Python',
+ time: 48,
+ memory: 7168,
+ created: '2026-10-04 14:24:09',
+ },
+ {
+ id: '10243',
+ problemId: '1007',
+ username: 'Chen',
+ verdict: 'CE',
+ language: 'C++',
+ time: null,
+ memory: null,
+ created: '2026-10-04 14:21:33',
+ },
+ {
+ id: '10242',
+ problemId: '1008',
+ username: 'Yuki',
+ verdict: 'RE',
+ language: 'Rust',
+ time: 18,
+ memory: 3072,
+ created: '2026-10-04 14:19:06',
+ },
+ {
+ id: '10241',
+ problemId: '1006',
+ username: 'Kronecker',
+ verdict: 'AC',
+ language: 'Rust',
+ time: 11,
+ memory: 2048,
+ created: '2026-10-04 14:17:26',
+ },
+ {
+ id: '10240',
+ problemId: '1011',
+ username: 'Natsume',
+ verdict: 'Pending',
+ language: 'C++',
+ time: null,
+ memory: null,
+ created: '2026-10-04 14:15:12',
+ },
+ {
+ id: '10239',
+ problemId: '1009',
+ username: 'Ada',
+ verdict: 'AC',
+ language: 'C++',
+ time: 22,
+ memory: 4096,
+ created: '2026-10-04 14:12:45',
+ },
+ {
+ id: '10238',
+ problemId: '1002',
+ username: 'Sora',
+ verdict: 'WA',
+ language: 'Python',
+ time: 56,
+ memory: 7168,
+ created: '2026-10-04 14:10:18',
+ },
+ {
+ id: '10237',
+ problemId: '1012',
+ username: 'Hyperbola',
+ verdict: 'AC',
+ language: 'C++',
+ time: 71,
+ memory: 10240,
+ created: '2026-10-04 14:08:51',
+ },
+]
+export const discussions = [
+ {
+ id: '1',
+ title: '第一次使用在线评测,可以从哪里开始?',
+ category: '交流',
+ author: 'Kronecker',
+ replies: 12,
+ views: 328,
+ date: '2026-10-04',
+ pinned: true,
+ content:
+ '欢迎来到 KF OnlineJudge!可以先打开「从零开始 · 编程基础」题单,了解标准输入输出,再尝试 A + B Problem。\n\n遇到问题时,记录题号、语言和具体报错,把自己的思路一起写出来,讨论会更容易展开。',
+ },
+ {
+ id: '2',
+ title: 'A + B Problem:输入输出与整数范围',
+ category: '题解',
+ author: 'Ada',
+ replies: 8,
+ views: 216,
+ date: '2026-10-04',
+ pinned: false,
+ content:
+ '读取两个整数,计算它们的和,输出结果。\n\n记得检查整数范围。Rust 可以选择 i64;Python 的整数可以直接参与计算。输出只需要答案和换行。',
+ problemId: '1001',
+ },
+ {
+ id: '3',
+ title: '动态规划中的状态应该怎么定义?',
+ category: '问答',
+ author: 'Sora',
+ replies: 6,
+ views: 184,
+ date: '2026-10-03',
+ pinned: false,
+ content:
+ '练习背包问题时,我经常卡在状态定义上。现在尝试先描述「已经处理哪些物品」和「剩余多少容量」,再考虑每一步有哪些选择。\n\n大家平时会怎样从题意里提取状态?',
+ },
+ {
+ id: '4',
+ title: '分享我的 Rust 刷题输入模板',
+ category: '交流',
+ author: 'Lin',
+ replies: 4,
+ views: 152,
+ date: '2026-10-02',
+ pinned: false,
+ content:
+ '先用 read_to_string 获取输入,再按空白分隔。\n\n不同题目的输入结构可能不同,使用模板之前还是需要认真阅读题面。',
+ },
+ {
+ id: '5',
+ title: '关于题库筛选的一点建议',
+ category: '反馈',
+ author: 'Mika',
+ replies: 2,
+ views: 96,
+ date: '2026-10-01',
+ pinned: false,
+ content: '希望题库可以同时按照标签和难度筛选,这样练习某个知识点会更方便。',
+ },
+]
+export function findProblem(id: string) {
+ return problems.find((problem) => problem.id === id)
+}
+export function acceptance(problem: Pick) {
+ return Math.round((problem.accepted / problem.submissions) * 100)
+}
diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts
new file mode 100644
index 0000000..f614c5b
--- /dev/null
+++ b/frontend/src/env.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/frontend/src/layouts/AppLayout.vue b/frontend/src/layouts/AppLayout.vue
index efa2fbd..851d43b 100644
--- a/frontend/src/layouts/AppLayout.vue
+++ b/frontend/src/layouts/AppLayout.vue
@@ -1,12 +1,94 @@
+
]
-
-
-
-
+
+
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index 00dddce..4c9b9af 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -1,8 +1,11 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
-import 'element-plus/dist/index.css'
+import { registerUi } from './plugins/ui'
import App from './App.vue'
import { router } from './router'
import './styles/base.css'
+import './styles/blueprint.css'
-createApp(App).use(createPinia()).use(router).mount('#app')
+const app = createApp(App)
+registerUi(app)
+app.use(createPinia()).use(router).mount('#app')
diff --git a/frontend/src/plugins/ui.ts b/frontend/src/plugins/ui.ts
new file mode 100644
index 0000000..72fb75a
--- /dev/null
+++ b/frontend/src/plugins/ui.ts
@@ -0,0 +1,65 @@
+import type { App } from 'vue'
+import {
+ ElButton,
+ ElConfigProvider,
+ ElDescriptions,
+ ElDescriptionsItem,
+ ElDialog,
+ ElEmpty,
+ ElForm,
+ ElFormItem,
+ ElIcon,
+ ElInput,
+ ElOption,
+ ElPagination,
+ ElPopover,
+ ElProgress,
+ ElRadio,
+ ElRadioButton,
+ ElRadioGroup,
+ ElResult,
+ ElSelect,
+ ElSlider,
+ ElSwitch,
+ ElTabPane,
+ ElTable,
+ ElTableColumn,
+ ElTabs,
+ ElTag,
+ ElTooltip,
+} from 'element-plus'
+import 'element-plus/dist/index.css'
+
+export function registerUi(app: App) {
+ for (const component of [
+ ElButton,
+ ElConfigProvider,
+ ElDescriptions,
+ ElDescriptionsItem,
+ ElDialog,
+ ElEmpty,
+ ElForm,
+ ElFormItem,
+ ElIcon,
+ ElInput,
+ ElOption,
+ ElPagination,
+ ElPopover,
+ ElProgress,
+ ElRadio,
+ ElRadioButton,
+ ElRadioGroup,
+ ElResult,
+ ElSelect,
+ ElSlider,
+ ElSwitch,
+ ElTabPane,
+ ElTable,
+ ElTableColumn,
+ ElTabs,
+ ElTag,
+ ElTooltip,
+ ]) {
+ app.use(component)
+ }
+}
diff --git a/frontend/src/styles/base.css b/frontend/src/styles/base.css
index ce5705c..1b02049 100644
--- a/frontend/src/styles/base.css
+++ b/frontend/src/styles/base.css
@@ -1,11 +1,19 @@
:root {
font-family: Inter, 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
- color: #18243c;
- background: #f5f7fc;
+ color: #354257;
+ background: #f0f3f7;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
- --el-color-primary: #2563eb;
+ --el-color-primary: #409eff;
+ --el-border-radius-base: 5px;
+ --el-border-color: #e0e6ee;
+ --el-text-color-primary: #354257;
+ --el-text-color-regular: #566379;
+ --el-fill-color-lighter: #f8fafc;
+ --blue: #409eff;
+ --muted: #8490a3;
+ --line: #edf0f5;
}
* {
box-sizing: border-box;
@@ -17,9 +25,31 @@ a {
color: inherit;
text-decoration: none;
}
+a:hover {
+ color: var(--blue);
+}
button,
-a {
- -webkit-tap-highlight-color: transparent;
+input,
+textarea {
+ font: inherit;
+}
+button {
+ cursor: pointer;
+}
+button:disabled {
+ cursor: not-allowed;
+}
+h1,
+h2,
+h3,
+p {
+ margin: 0;
+}
+p {
+ line-height: 1.75;
+}
+::selection {
+ background: #daecff;
}
.app-shell {
min-height: 100vh;
@@ -27,79 +57,1380 @@ a {
flex-direction: column;
}
.app-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 24px 6vw;
- border-bottom: 1px solid #e4e9f2;
background: #fff;
+ border-bottom: 1px solid #e5eaf1;
+ box-shadow: 0 2px 5px #263c5605;
+}
+.header-inner {
+ width: min(100% - 48px, 1480px);
+ margin: auto;
+ display: flex;
+ align-items: center;
+ min-height: 68px;
+ gap: 28px;
}
.brand {
- font-weight: 800;
- font-size: 24px;
- color: #2563eb;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ white-space: nowrap;
}
-.brand span {
- font-size: 17px;
- margin-left: 10px;
- color: #18243c;
+.brand-mark {
+ font-size: 29px;
+ letter-spacing: -2px;
+ font-weight: 850;
+ color: #409eff;
+ line-height: 1;
}
-.header-caption,
-.app-footer {
- color: #65738b;
+.brand-dot {
+ color: #91bff3;
+}
+.brand-name {
+ font-size: 15px;
+ font-weight: 750;
+ color: #2d4261;
+ letter-spacing: -0.3px;
+}
+.brand-name small {
+ display: block;
+ font-size: 8px;
+ letter-spacing: 1px;
+ color: #91a3bb;
+ margin-top: 4px;
+}
+.main-nav {
+ display: flex;
+ align-self: stretch;
+ gap: 3px;
+}
+.main-nav a {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ padding: 0 13px;
+ font-size: 13px;
+ border-bottom: 2px solid transparent;
+ white-space: nowrap;
+ color: #67758a;
+}
+.main-nav a.active {
+ color: var(--blue);
+ background: #f4f9ff;
+ border-color: var(--blue);
+}
+.main-nav .el-icon {
font-size: 14px;
}
+.header-account {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-left: auto;
+ white-space: nowrap;
+}
+.preview-label {
+ font-size: 11px;
+ color: #91a0b2;
+ margin-right: 6px;
+}
+.mobile-toggle {
+ display: none;
+}
.app-main {
- width: min(100% - 40px, 1040px);
- margin: 0 auto;
+ width: min(100% - 48px, 1480px);
+ margin: 24px auto 40px;
flex: 1;
}
-.welcome {
- padding: 80px 0;
- max-width: 720px;
+.app-footer {
+ background: #fff;
+ border-top: 1px solid #e6ecf3;
+ padding: 36px 0 20px;
}
-h1 {
- font-size: clamp(32px, 5vw, 52px);
- line-height: 1.3;
- letter-spacing: -1px;
- margin: 24px 0;
+.footer-inner,
+.footer-bottom {
+ width: min(100% - 64px, 1400px);
+ margin: auto;
+ display: grid;
+ grid-template-columns: 2fr 1fr 1fr 1fr;
+ gap: 32px;
}
-.welcome-description {
- color: #65738b;
- font-size: 17px;
- line-height: 1.8;
- max-width: 620px;
+.footer-brand strong {
+ display: block;
+ margin: 15px 0 7px;
+ font-size: 14px;
+ font-weight: 500;
}
-.connection-card {
- margin-top: 40px;
- border-radius: 14px;
+.footer-brand p {
+ font-size: 12px;
+ color: var(--muted);
}
-.connection-row {
+.footer-inner h3 {
+ font-size: 13px;
+ font-weight: 600;
+ margin: 2px 0 12px;
+}
+.footer-inner a {
+ display: block;
+ font-size: 12px;
+ color: #8490a3;
+ margin: 10px 0;
+}
+.footer-inner a:hover {
+ color: var(--blue);
+}
+.footer-bottom {
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ border-top: 1px solid var(--line);
+ padding-top: 20px;
+ margin-top: 30px;
+ font-size: 11px;
+ color: #9aa5b5;
+}
+.panel {
+ background: #fff;
+ border: 1px solid #e8edf4;
+ border-radius: 6px;
+ box-shadow: 0 2px 7px #263c5605;
+ min-width: 0;
+ overflow: hidden;
+}
+.panel-heading {
+ min-height: 54px;
+ padding: 15px 20px;
display: flex;
- gap: 24px;
align-items: center;
justify-content: space-between;
+ gap: 12px;
+ border-bottom: 1px solid var(--line);
}
-.connection-row h2 {
- font-size: 16px;
- margin: 0 0 8px;
+.panel-heading h2 {
+ font-size: 15px;
+ color: #4995e5;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
}
-.connection-row p {
+.panel-heading small {
+ font-size: 11px;
+ font-weight: 400;
+ color: #98a4b4;
+ margin-left: 4px;
+}
+.panel-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+ color: var(--muted);
+ flex-shrink: 0;
+}
+.panel-actions a {
+ color: var(--blue);
+}
+.panel-body {
+ padding: 20px;
+ min-width: 0;
+}
+.page-heading {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 20px;
+ margin: 8px 0 24px;
+}
+.page-heading h1 {
+ font-size: 26px;
+ font-weight: 650;
+ letter-spacing: -0.6px;
+ margin-bottom: 8px;
+ color: #2e405b;
+}
+.page-heading p {
+ font-size: 13px;
+}
+.page-heading-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+.eyebrow {
+ text-transform: uppercase;
+ font-size: 11px !important;
+ letter-spacing: 2px;
+ color: #86a0be;
+ margin-bottom: 8px;
+}
+.muted {
+ color: var(--muted);
+}
+.small {
+ font-size: 12px;
+}
+.mono {
+ font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
+}
+.tabular {
+ font-variant-numeric: tabular-nums;
+}
+.text-link {
+ color: var(--blue);
+ font-size: 13px;
+}
+.table-link {
+ color: #455e80;
+ font-weight: 500;
+}
+.table-link:hover {
+ color: var(--blue);
+}
+.tag-list {
+ display: flex;
+ gap: 5px;
+ flex-wrap: wrap;
+}
+.tag {
+ display: inline-flex;
+ align-items: center;
+ padding: 3px 7px;
+ color: #8492a6;
+ background: #f3f6fa;
+ border-radius: 3px;
+ font-size: 11px;
+ white-space: nowrap;
+}
+.difficulty {
+ padding: 3px 7px;
+ border-radius: 4px;
+ font-size: 11px;
+ white-space: nowrap;
+ background: #f0f3f7;
+ color: #8a99ac;
+}
+.difficulty-easy {
+ color: #49a389;
+ background: #eff9f5;
+}
+.difficulty-medium {
+ color: #c28e37;
+ background: #fff7e8;
+}
+.difficulty-hard {
+ color: #c86c85;
+ background: #fff0f4;
+}
+.verdict {
+ font:
+ 600 11px ui-monospace,
+ monospace;
+ padding: 4px 8px;
+ border-radius: 4px;
+ white-space: nowrap;
+}
+.verdict-ac {
+ color: #22a278;
+ background: #edf9f4;
+}
+.verdict-wa,
+.verdict-re {
+ color: #dd7184;
+ background: #fff0f3;
+}
+.verdict-tle {
+ color: #c39737;
+ background: #fff8e7;
+}
+.verdict-ce {
+ color: #9879cc;
+ background: #f6f0fc;
+}
+.verdict-pending {
+ color: #8897ac;
+ background: #f0f4f9;
+}
+.filters {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ align-items: center;
+ margin-bottom: 22px;
+}
+.filters .el-input {
+ width: 260px;
+}
+.filters .el-select {
+ width: 155px;
+}
+.filter-label {
+ font-size: 12px;
+ color: #8a99ad;
+}
+.pagination-row {
+ padding: 20px 0 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ font-size: 12px;
+ color: var(--muted);
+}
+.el-table {
+ --el-table-header-bg-color: #f8fafc;
+ --el-table-header-text-color: #7e8a9c;
+ --el-table-border-color: #edf0f5;
+ font-size: 12px;
+}
+.el-table th.el-table__cell {
+ font-weight: 500;
+}
+.el-table .el-table__cell {
+ padding: 12px 0;
+}
+.stack {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ min-width: 0;
+}
+.content-with-aside {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 320px;
+ gap: 20px;
+}
+.summary-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 16px;
+ margin-bottom: 20px;
+}
+.summary-card {
+ background: #fff;
+ border: 1px solid #e8edf4;
+ border-radius: 6px;
+ padding: 20px;
+}
+.summary-card p {
+ font-size: 12px;
+ color: var(--muted);
+}
+.summary-card strong {
+ display: block;
+ font-size: 27px;
+ font-weight: 600;
+ margin: 8px 0 2px;
+ letter-spacing: -0.5px;
+}
+.summary-card small {
+ font-size: 11px;
+ color: #9cabbc;
+}
+.avatar {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ color: #fff;
+ font-size: 12px;
+ flex-shrink: 0;
+}
+.user-inline {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+.number-medal {
+ display: inline-grid;
+ place-items: center;
+ width: 21px;
+ height: 25px;
+ color: #9aa5b5;
+ font-weight: 600;
+ font-size: 12px;
+}
+.number-medal.top-1 {
+ color: #e3983e;
+ background: #fff3df;
+}
+.number-medal.top-2 {
+ color: #8498b7;
+ background: #edf2f9;
+}
+.number-medal.top-3 {
+ color: #c58d74;
+ background: #f9efe8;
+}
+.empty-state {
+ text-align: center;
+ padding: 12px 0 24px;
+}
+.empty-state .el-empty {
+ padding: 20px;
+}
+.empty-state > p {
+ font-size: 13px;
+ margin-bottom: 14px;
+}
+.breadcrumbs {
+ margin: 0 0 20px;
+ font-size: 12px;
+ color: #95a1b1;
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+.breadcrumbs a {
+ color: #7299c2;
+}
+.intro-banner {
+ background: linear-gradient(115deg, #eaf4ff, #f5f8fd);
+ border: 1px solid #e2edfa;
+ border-radius: 6px;
+ padding: 30px;
+ margin-bottom: 24px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 24px;
+}
+.intro-banner h2 {
+ font-size: 25px;
+ margin: 8px 0 12px;
+ font-weight: 600;
+}
+.intro-banner p {
+ font-size: 13px;
+ color: #7a8eaa;
+ max-width: 540px;
+}
+.intro-number {
+ font-size: 65px;
+ color: #afceef;
+ font-family: Georgia, serif;
+}
+.card-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 20px;
+}
+.training-card {
+ padding: 26px;
+ background: #fff;
+ border: 1px solid #e8edf4;
+ border-radius: 6px;
+ display: flex;
+ gap: 22px;
+}
+.training-illustration {
+ width: 70px;
+ height: 78px;
+ border-radius: 12px;
+ display: grid;
+ place-items: center;
+ flex-shrink: 0;
+ font:
+ 600 27px ui-monospace,
+ monospace;
+ background: #ecf4ff;
+ color: #81b2ea;
+}
+.training-illustration.green {
+ background: #edf8f3;
+ color: #73b39f;
+}
+.training-illustration.violet {
+ background: #f3effb;
+ color: #a291cc;
+}
+.training-illustration.amber {
+ background: #fff7ea;
+ color: #d6b072;
+}
+.training-card h2 {
+ font-size: 17px;
+ margin: 10px 0;
+ color: #425874;
+}
+.training-card p {
+ font-size: 12px;
+ color: #92a0b2;
+ margin: 9px 0 18px;
+}
+.card-meta {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ font-size: 12px;
+ color: #92a0b2;
+}
+.card-meta .text-link {
+ margin-left: auto;
+}
+.detail-prose {
font-size: 14px;
- color: #65738b;
+ line-height: 1.9;
+ color: #566379;
+}
+.detail-prose h2 {
+ font-size: 17px;
+ color: #40536d;
+ margin: 24px 0 10px;
+ font-weight: 600;
+}
+.detail-prose h2:first-child {
+ margin-top: 0;
+}
+.detail-prose p {
+ white-space: pre-line;
+}
+.sample-block {
+ background: #f7f9fc;
+ border: 1px solid #e9eef5;
+ border-radius: 5px;
+ padding: 16px;
+ font:
+ 13px/1.8 ui-monospace,
+ monospace;
+ white-space: pre-wrap;
+ margin: 12px 0;
+}
+.detail-meta {
+ display: flex;
+ align-items: center;
+ gap: 18px;
+ flex-wrap: wrap;
+ font-size: 12px;
+ color: #92a0b2;
+ margin: 14px 0 20px;
+}
+.details-list {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ font-size: 13px;
+}
+.details-list div {
+ display: flex;
+ justify-content: space-between;
+ gap: 20px;
+}
+.details-list dt {
+ color: #91a0b2;
+}
+.details-list dd {
margin: 0;
+ text-align: right;
}
-.app-footer {
- padding: 24px 6vw;
+.notice-strip {
+ padding: 12px 16px;
+ background: #f5f9ff;
+ border: 1px solid #e6f0fc;
+ border-radius: 5px;
+ font-size: 12px;
+ color: #7b94b2;
+ margin-bottom: 20px;
+ line-height: 1.7;
}
-@media (max-width: 600px) {
- .header-caption {
+.form-panel {
+ max-width: 860px;
+ margin: auto;
+}
+.form-panel .panel-body {
+ padding: 32px;
+}
+.form-help {
+ font-size: 12px;
+ color: #98a5b6;
+ margin: 5px 0 0;
+}
+.form-actions {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ margin-top: 28px;
+}
+.form-actions .muted {
+ font-size: 12px;
+}
+.tab-summary {
+ padding: 16px 0;
+ font-size: 13px;
+ color: var(--muted);
+}
+.about-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 18px;
+}
+.about-feature {
+ padding: 24px;
+ border: 1px solid #edf0f5;
+ border-radius: 6px;
+}
+.about-feature h3 {
+ font-size: 16px;
+ margin: 12px 0;
+}
+.about-feature p {
+ font-size: 13px;
+ color: #8c9db2;
+}
+.about-feature .el-icon {
+ font-size: 23px;
+ color: #7cacde;
+}
+.service-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+}
+.service-row h3 {
+ font-size: 15px;
+ margin-bottom: 8px;
+}
+.service-row p {
+ font-size: 12px;
+ color: #94a2b6;
+}
+.profile-banner {
+ background: #fff;
+ border: 1px solid #e8edf4;
+ border-radius: 6px;
+ padding: 36px;
+ display: flex;
+ align-items: center;
+ gap: 25px;
+ margin-bottom: 22px;
+}
+.profile-banner > .avatar {
+ width: 84px;
+ height: 84px;
+ font-size: 32px;
+}
+.profile-banner h1 {
+ font-size: 27px;
+ margin-bottom: 8px;
+}
+.profile-banner p {
+ color: #92a0b2;
+ font-size: 13px;
+}
+.profile-actions {
+ margin-left: auto;
+ display: flex;
+ gap: 10px;
+}
+.profile-tags {
+ margin-top: 12px;
+ display: flex;
+ gap: 7px;
+}
+.heatmap {
+ display: grid;
+ grid-template-columns: repeat(20, minmax(0, 1fr));
+ gap: 5px;
+ padding: 15px 0;
+}
+.heat-cell {
+ aspect-ratio: 1;
+ border-radius: 3px;
+ background: #edf3fa;
+}
+.heat-cell[data-level='1'] {
+ background: #d6eaff;
+}
+.heat-cell[data-level='2'] {
+ background: #a3d1fc;
+}
+.heat-cell[data-level='3'] {
+ background: #6caff0;
+}
+.heat-cell[data-level='4'] {
+ background: #4094e4;
+}
+.skill-row {
+ margin: 17px 0;
+}
+.skill-row > div {
+ display: flex;
+ justify-content: space-between;
+ font-size: 12px;
+ margin-bottom: 8px;
+}
+.admin-shell {
+ display: grid;
+ grid-template-columns: 210px minmax(0, 1fr);
+ gap: 22px;
+}
+.admin-sidebar {
+ padding: 20px 12px;
+ align-self: start;
+}
+.admin-sidebar h2 {
+ font-size: 14px;
+ padding: 0 12px;
+ margin-bottom: 18px;
+}
+.admin-sidebar a {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px;
+ border-radius: 5px;
+ font-size: 13px;
+ color: #7b8da5;
+}
+.admin-sidebar a.active {
+ background: #eef6ff;
+ color: #409eff;
+}
+.admin-notice {
+ font-size: 12px;
+ color: #8fa0b6;
+ padding: 18px 12px 0;
+ border-top: 1px solid #edf0f5;
+ margin-top: 20px;
+ line-height: 1.7;
+}
+.status-dot {
+ display: inline-block;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: #80b69a;
+ margin-right: 6px;
+}
+.status-dot.offline {
+ background: #c5cedb;
+}
+.home-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.75fr) minmax(320px, 1fr);
+ gap: 20px;
+}
+.home-banner .panel-body {
+ padding: 16px;
+}
+.hero-image {
+ position: relative;
+ min-height: 300px;
+ overflow: hidden;
+ border-radius: 4px;
+ background: #e6f2ff;
+ isolation: isolate;
+}
+.hero-image > img {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ inset: 0;
+ z-index: -1;
+}
+.hero-copy {
+ position: relative;
+ padding: 42px 36px;
+ max-width: 75%;
+}
+.hero-copy .eyebrow {
+ font-size: 10px !important;
+ color: #6b96c6;
+}
+.hero-copy h1 {
+ font-size: clamp(25px, 2.35vw, 38px);
+ font-weight: 600;
+ line-height: 1.4;
+ color: #335983;
+ letter-spacing: -1px;
+ margin: 12px 0;
+}
+.hero-copy p {
+ font-size: 13px;
+ color: #7093b7;
+ max-width: 320px;
+ margin-bottom: 23px;
+}
+.hero-controls {
+ display: flex;
+ justify-content: center;
+ gap: 7px;
+ padding: 12px 0 1px;
+}
+.hero-controls button {
+ width: 20px;
+ height: 3px;
+ padding: 0;
+ border: 0;
+ background: #d9e3ee;
+ border-radius: 2px;
+}
+.hero-controls button.active {
+ background: #409eff;
+}
+.home-rank .panel-body {
+ padding: 10px 20px;
+}
+.home-rank .el-table .el-table__cell {
+ padding: 8px 0;
+}
+.home-rank .avatar {
+ width: 23px;
+ height: 23px;
+ font-size: 10px;
+}
+.home-rank .number-medal {
+ height: 21px;
+ font-size: 11px;
+}
+.home-notice {
+ padding: 15px 4px;
+ display: flex;
+ gap: 15px;
+ border-bottom: 1px solid #edf0f5;
+}
+.home-notice:last-child {
+ border: 0;
+ padding-bottom: 2px;
+}
+.home-notice > div {
+ min-width: 0;
+}
+.home-notice-title {
+ display: block;
+ font-size: 13px;
+ margin: 0 0 5px;
+ font-weight: 500;
+ color: #4c6685;
+}
+.home-notice p {
+ font-size: 12px;
+ color: #98a5b7;
+}
+.home-notice time {
+ font-size: 11px;
+ color: #a0adbc;
+ white-space: nowrap;
+ margin-left: auto;
+}
+.notice-symbol {
+ background: #edf6ff;
+ width: 28px;
+ height: 28px;
+ border-radius: 5px;
+ display: grid;
+ place-items: center;
+ color: #66a8eb;
+ flex-shrink: 0;
+}
+.contest-mini {
+ display: flex;
+ gap: 11px;
+ padding: 13px 0;
+ border-bottom: 1px solid #edf0f5;
+ align-items: center;
+}
+.contest-mini:last-child {
+ border-bottom: 0;
+}
+.contest-mini strong {
+ display: block;
+ font-size: 12px;
+ color: #59708d;
+ font-weight: 500;
+}
+.contest-mini p {
+ font-size: 11px;
+ color: #9cacbf;
+ margin-top: 4px;
+}
+.contest-format {
+ font-size: 10px;
+ background: #eaf5ff;
+ color: #68a3de;
+ border-radius: 3px;
+ min-width: 31px;
+ text-align: center;
+ padding: 4px;
+}
+.contest-format.oi {
+ background: #eaf8f1;
+ color: #64b296;
+}
+.contest-status {
+ margin-left: auto;
+ font-size: 11px;
+ color: #cfad76;
+ white-space: nowrap;
+}
+.home-quick {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+}
+.home-quick a {
+ padding: 14px;
+ background: #f8fafc;
+ border: 1px solid #edf2f7;
+ border-radius: 5px;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ color: #7b94b4;
+}
+.home-quick a:hover {
+ background: #eff6ff;
+ color: #409eff;
+}
+.auth-shell {
+ max-width: 1000px;
+ margin: 35px auto;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ min-height: 540px;
+ background: #fff;
+ border: 1px solid #e5ecf4;
+ border-radius: 8px;
+ overflow: hidden;
+}
+.auth-art {
+ padding: 48px 40px;
+ background: #e9f3ff url('/images/home-banner.svg') center bottom/cover;
+}
+.auth-art h1 {
+ font-size: 33px;
+ color: #4f78a4;
+ margin: 35px 0 15px;
+}
+.auth-art p {
+ font-size: 14px;
+ color: #86a4c5;
+}
+.auth-form {
+ padding: 40px;
+}
+.auth-form h2 {
+ font-size: 25px;
+ margin-bottom: 8px;
+}
+.auth-form > p {
+ font-size: 12px;
+ color: #9caabc;
+ margin-bottom: 25px;
+}
+.auth-form .el-form-item {
+ margin-bottom: 21px;
+}
+.auth-form .el-button {
+ width: 100%;
+}
+.auth-links {
+ font-size: 12px;
+ display: flex;
+ justify-content: space-between;
+ margin-top: 20px;
+ color: #91a2b8;
+}
+.discussion-item {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 23px 0;
+ border-bottom: 1px solid #edf0f5;
+}
+.discussion-item:last-child {
+ border-bottom: 0;
+}
+.discussion-item .avatar {
+ width: 40px;
+ height: 40px;
+ font-size: 16px;
+}
+.discussion-item h2 {
+ font-size: 15px;
+ font-weight: 500;
+ margin-bottom: 7px;
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+.discussion-item p {
+ display: flex;
+ gap: 12px;
+ font-size: 11px;
+ color: #9cabbc;
+}
+.discussion-numbers {
+ margin-left: auto;
+ font-size: 12px;
+ color: #9dabbc;
+ display: flex;
+ gap: 20px;
+ white-space: nowrap;
+}
+.discussion-numbers strong {
+ font-weight: 500;
+ color: #6a819e;
+}
+.comment-placeholder {
+ padding: 25px;
+ background: #f7f9fc;
+ border-radius: 5px;
+ margin: 20px 0 0;
+ font-size: 13px;
+ color: #94a4b9;
+ text-align: center;
+}
+.team-card {
+ padding: 24px;
+ display: flex;
+ gap: 20px;
+ align-items: start;
+}
+.team-logo {
+ width: 55px;
+ height: 55px;
+ border-radius: 10px;
+ background: #edf4ff;
+ color: #7faae0;
+ font:
+ 600 20px ui-monospace,
+ monospace;
+ display: grid;
+ place-items: center;
+}
+.team-card h2 {
+ font-size: 17px;
+ margin: 3px 0 8px;
+}
+.team-card p {
+ font-size: 12px;
+ color: #96a5b7;
+}
+.team-card .tag-list {
+ margin-top: 15px;
+}
+@media (max-width: 1280px) {
+ .header-inner {
+ gap: 16px;
+ }
+ .main-nav a {
+ padding: 0 9px;
+ gap: 5px;
+ }
+ .main-nav .el-icon {
display: none;
}
- .welcome {
- padding-top: 48px;
+ .preview-label {
+ display: none;
}
- .connection-row {
- flex-wrap: wrap;
+ .home-grid {
+ grid-template-columns: minmax(0, 1.6fr) minmax(300px, 1fr);
+ }
+ .hero-copy {
+ padding: 36px 28px;
+ }
+ .hero-copy p {
+ max-width: 270px;
+ }
+ .card-grid {
+ gap: 16px;
+ }
+ .training-card {
+ padding: 20px;
+ gap: 16px;
+ }
+ .training-illustration {
+ width: 55px;
+ height: 65px;
+ }
+ .content-with-aside {
+ grid-template-columns: minmax(0, 1fr) 280px;
+ }
+}
+@media (max-width: 960px) {
+ .brand-name {
+ font-size: 13px;
+ }
+ .brand-mark {
+ font-size: 25px;
+ }
+ .header-inner {
+ gap: 12px;
+ }
+ .main-nav a {
+ font-size: 12px;
+ padding: 0 7px;
+ }
+ .header-account {
+ gap: 5px;
+ }
+ .hero-copy {
+ max-width: 100%;
+ padding: 30px 24px;
+ }
+ .home-grid {
+ grid-template-columns: minmax(0, 1.45fr) minmax(270px, 1fr);
+ gap: 16px;
+ }
+ .home-grid .panel-heading {
+ padding: 14px 16px;
+ }
+ .home-grid .panel-body {
+ padding: 16px;
+ }
+ .home-rank .panel-body {
+ padding: 8px 10px;
+ }
+ .contest-status {
+ display: none;
+ }
+ .content-with-aside,
+ .card-grid,
+ .about-grid {
+ grid-template-columns: 1fr;
+ }
+ .admin-shell {
+ grid-template-columns: 175px minmax(0, 1fr);
+ }
+ .summary-card {
+ padding: 16px;
+ }
+ .summary-card strong {
+ font-size: 24px;
+ }
+}
+@media (max-width: 760px) {
+ .app-main {
+ width: calc(100% - 28px);
+ margin-top: 18px;
+ }
+ .header-inner {
+ width: calc(100% - 28px);
+ flex-wrap: wrap;
+ gap: 12px;
+ min-height: 64px;
+ padding: 14px 0;
+ }
+ .brand-name small {
+ font-size: 7px;
+ }
+ .mobile-toggle {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 5px 7px;
+ background: none;
+ border: 1px solid #e6edf5;
+ border-radius: 4px;
+ font-size: 12px;
+ color: #7e93ad;
+ margin-left: auto;
+ }
+ .header-account {
+ margin-left: 0;
+ }
+ .main-nav {
+ display: none;
+ order: 4;
+ width: 100%;
+ flex-wrap: wrap;
+ gap: 4px;
+ padding-top: 5px;
+ }
+ .main-nav.mobile-open {
+ display: flex;
+ }
+ .main-nav a {
+ padding: 9px 13px;
+ flex: 1 0 auto;
+ justify-content: center;
+ border-radius: 4px;
+ border-bottom: 0;
+ }
+ .main-nav .el-icon {
+ display: inline-flex;
+ }
+ .home-grid {
+ grid-template-columns: minmax(0, 1fr);
+ }
+ .hero-copy h1 {
+ font-size: 32px;
+ }
+ .hero-copy {
+ padding: 32px;
+ }
+ .hero-copy p {
+ max-width: 270px;
+ }
+ .footer-inner {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 20px;
+ }
+ .footer-brand {
+ grid-column: 1/-1;
+ }
+ .footer-bottom {
+ gap: 12px;
+ }
+ .page-heading {
+ align-items: flex-start;
+ flex-wrap: wrap;
+ gap: 12px;
+ }
+ .page-heading h1 {
+ font-size: 23px;
+ }
+ .filters .el-input {
+ width: 100%;
+ }
+ .filters .el-select {
+ flex: 1;
+ min-width: 130px;
+ width: auto;
+ }
+ .pagination-row {
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+ .pagination-row .el-pagination {
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 8px;
+ }
+ .summary-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+ }
+ .intro-banner {
+ padding: 24px;
+ }
+ .intro-number {
+ display: none;
+ }
+ .profile-banner {
+ padding: 24px;
+ gap: 16px;
+ flex-wrap: wrap;
+ }
+ .profile-banner > .avatar {
+ width: 60px;
+ height: 60px;
+ font-size: 24px;
+ }
+ .profile-actions {
+ margin-left: 0;
+ width: 100%;
+ }
+ .heatmap {
+ grid-template-columns: repeat(14, minmax(0, 1fr));
+ }
+ .auth-shell {
+ grid-template-columns: 1fr;
+ margin: 15px auto;
+ }
+ .auth-art {
+ display: none;
+ }
+ .auth-form {
+ padding: 28px;
+ }
+ .admin-shell {
+ grid-template-columns: 1fr;
+ }
+ .admin-sidebar {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 5px;
+ padding: 12px;
+ }
+ .admin-sidebar h2,
+ .admin-notice {
+ display: none;
+ }
+ .admin-sidebar a {
+ padding: 8px;
+ font-size: 12px;
+ }
+ .discussion-numbers {
+ display: none;
+ }
+ .form-panel .panel-body {
+ padding: 22px;
+ }
+ .service-row {
+ flex-wrap: wrap;
+ }
+ .panel-heading {
+ padding: 14px 16px;
+ }
+ .panel-body {
+ padding: 16px;
+ }
+ .details-list {
+ font-size: 12px;
+ }
+ .footer-inner,
+ .footer-bottom {
+ width: calc(100% - 40px);
+ }
+}
+@media (max-width: 420px) {
+ .brand-name {
+ font-size: 11px;
+ }
+ .brand-name small {
+ display: none;
+ }
+ .brand {
+ gap: 7px;
+ }
+ .header-inner {
+ gap: 7px;
+ }
+ .header-account .el-button {
+ padding: 7px 10px;
+ }
+ .mobile-toggle {
+ padding: 5px;
+ font-size: 11px;
+ }
+ .hero-copy {
+ padding: 30px 24px;
+ }
+ .hero-copy h1 {
+ font-size: 27px;
+ }
+ .hero-copy p {
+ max-width: 230px;
+ font-size: 12px;
+ }
+ .panel-heading h2 {
+ font-size: 14px;
+ }
+ .panel-heading small {
+ display: none;
+ }
+ .training-illustration {
+ display: none;
+ }
+ .discussion-item {
+ gap: 10px;
+ }
+ .discussion-item h2 {
+ font-size: 14px;
+ }
+ .discussion-item p {
+ gap: 7px;
+ flex-wrap: wrap;
+ }
+ .summary-card strong {
+ font-size: 22px;
}
}
diff --git a/frontend/src/styles/blueprint.css b/frontend/src/styles/blueprint.css
new file mode 100644
index 0000000..4101af6
--- /dev/null
+++ b/frontend/src/styles/blueprint.css
@@ -0,0 +1,492 @@
+:root {
+ --el-border-radius-base: 8px;
+}
+.app-header {
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ box-shadow: none;
+}
+.header-inner {
+ min-height: 76px;
+ max-width: 1360px;
+}
+.brand-mark {
+ font-size: 25px;
+ padding: 10px 12px;
+ border-radius: 11px;
+ background: #eaf4ff;
+ letter-spacing: -1.5px;
+}
+.brand-dot {
+ color: #409eff;
+}
+.brand-name {
+ font-size: 13px;
+}
+.brand-name small {
+ letter-spacing: 1.3px;
+}
+.main-nav {
+ gap: 5px;
+ align-items: center;
+}
+.main-nav a {
+ align-self: center;
+ padding: 10px 13px;
+ border: 0;
+ border-radius: 8px;
+ font-size: 13px;
+}
+.main-nav a.active {
+ border: 0;
+ background: #edf6ff;
+ font-weight: 650;
+}
+.main-nav .el-icon {
+ display: none;
+}
+.header-account .el-button {
+ border-radius: 8px;
+ padding: 8px 14px;
+}
+.app-main {
+ width: min(100% - 64px, 1360px);
+ margin-top: 30px;
+ margin-bottom: 50px;
+}
+.app-footer {
+ background: transparent;
+ box-shadow: none;
+ padding: 38px 0 22px;
+}
+.footer-inner,
+.footer-bottom {
+ max-width: 1360px;
+}
+.footer-brand .brand-mark {
+ display: inline-block;
+ background: #e3eefb;
+}
+.footer-inner h3 {
+ font-size: 12px;
+ color: #657e9e;
+}
+.panel {
+ border-radius: 16px;
+ box-shadow: none;
+ border: 1px solid #e1e8f1;
+}
+.panel-heading {
+ border: 0;
+ padding: 23px 24px 10px;
+ min-height: 68px;
+ align-items: flex-start;
+}
+.panel-heading h2 {
+ color: #455e80;
+ font-size: 17px;
+ font-weight: 650;
+ gap: 6px;
+ display: grid;
+}
+.panel-heading h2 > .el-icon {
+ display: none;
+}
+.panel-heading small {
+ margin: 0;
+ font-size: 10px;
+ letter-spacing: 0.3px;
+}
+.panel-body {
+ padding: 18px 24px 24px;
+}
+.panel-actions {
+ padding-top: 4px;
+ font-size: 11px;
+}
+.panel-actions a {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+.page-heading {
+ margin: 8px 0 30px;
+ align-items: flex-end;
+}
+.page-heading h1 {
+ font-size: 34px;
+ letter-spacing: -1.2px;
+ color: #2e405b;
+ font-weight: 700;
+ margin: 10px 0 10px;
+}
+.page-heading .eyebrow {
+ color: #91a3bb;
+ font:
+ 10px ui-monospace,
+ monospace;
+ letter-spacing: 2px;
+}
+.page-heading p {
+ font-size: 13px;
+}
+.el-button {
+ border-radius: 8px;
+}
+.el-table {
+ --el-table-header-bg-color: #f3f7fc;
+ --el-table-row-hover-bg-color: #f4f9ff;
+ --el-fill-color-lighter: #fff;
+ font-size: 13px;
+ border-radius: 8px;
+}
+.el-table .el-table__cell {
+ padding: 16px 0;
+}
+.el-table th.el-table__cell {
+ padding: 13px 0;
+ font-size: 11px;
+ color: #8a99ac;
+ letter-spacing: 0.3px;
+}
+.el-table .cell {
+ padding: 0 14px;
+}
+.table-link {
+ font-weight: 600;
+}
+.filters {
+ padding: 16px;
+ background: #f6f9fd;
+ border-radius: 10px;
+ gap: 10px;
+ margin-bottom: 22px;
+}
+.filters .el-input {
+ width: 260px;
+}
+.pagination-row {
+ margin-top: 22px;
+ padding-top: 18px;
+ border-top: 1px solid var(--line);
+}
+.tag,
+.difficulty,
+.el-tag {
+ border-radius: 5px;
+}
+.summary-card {
+ border-radius: 14px;
+ position: relative;
+ padding: 24px;
+ border-bottom: 3px solid #dbeaff;
+}
+.summary-card strong {
+ font:
+ 650 32px ui-monospace,
+ monospace;
+ margin-top: 12px;
+}
+.intro-banner {
+ border-radius: 16px;
+ border-left: 4px solid #9cc8f5;
+ padding: 32px;
+}
+.training-card {
+ border-radius: 16px;
+ flex-direction: column;
+ gap: 10px;
+ position: relative;
+ padding: 26px;
+}
+.training-illustration {
+ width: 48px;
+ height: 48px;
+ border-radius: 12px;
+ font-size: 21px;
+}
+.training-card h2 {
+ font-size: 20px;
+ margin: 15px 0 10px;
+}
+.training-card p {
+ font-size: 13px;
+ line-height: 1.9;
+}
+.card-meta {
+ padding-top: 15px;
+ border-top: 1px solid var(--line);
+ flex-wrap: wrap;
+}
+.team-card {
+ border-radius: 16px;
+ flex-direction: column;
+}
+.team-logo {
+ width: 48px;
+ height: 48px;
+}
+.profile-banner {
+ border-radius: 18px;
+ border-left: 5px solid #a9cef5;
+}
+.profile-banner > .avatar {
+ border-radius: 22px;
+}
+.discussion-item {
+ padding: 24px 8px;
+}
+.discussion-item .avatar {
+ border-radius: 12px;
+}
+.discussion-item h2 {
+ font-size: 16px;
+ font-weight: 600;
+}
+.detail-prose h2 {
+ color: #455e80;
+ font-size: 18px;
+ padding-left: 12px;
+ border-left: 3px solid #b5d5f7;
+}
+.sample-block {
+ border-radius: 10px;
+ background: #f6f9fd;
+ border: 1px solid #e4edf7;
+}
+.auth-shell {
+ border-radius: 22px;
+ border: 1px solid #e1e8f1;
+ box-shadow: none;
+}
+.auth-art {
+ padding: 50px;
+ background-image: url('/images/practice-space.svg');
+ background-size: 90%;
+ background-position: center bottom;
+ background-repeat: no-repeat;
+ min-height: 520px;
+}
+.auth-art h1 {
+ font-size: 33px;
+ max-width: 300px;
+ line-height: 1.5;
+}
+.about-feature {
+ border-radius: 14px;
+ padding: 26px;
+}
+#tags {
+ scroll-margin-top: 98px;
+}
+.admin-application {
+ min-height: 100dvh;
+}
+.admin-app-header {
+ min-height: 80px;
+ display: flex;
+ align-items: center;
+ gap: 26px;
+ padding: 0 30px;
+ background: #fff;
+ border-bottom: 1px solid #e1e8f1;
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+.admin-app-label {
+ font-size: 11px;
+ color: #91a3bb;
+ padding-left: 26px;
+ border-left: 1px solid #e1e8f1;
+}
+.admin-return {
+ font-size: 12px;
+ color: #657e9e;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-left: auto;
+}
+.admin-app-nav {
+ position: fixed;
+ left: 0;
+ top: 80px;
+ bottom: 0;
+ width: 210px;
+ background: #fff;
+ border-right: 1px solid #e1e8f1;
+ padding: 30px 18px;
+ display: flex;
+ flex-direction: column;
+ gap: 7px;
+}
+.admin-app-nav a {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 14px 18px;
+ font-size: 13px;
+ color: #7b8da5;
+ border-radius: 9px;
+}
+.admin-app-nav a.active {
+ background: #edf6ff;
+ color: #409eff;
+ font-weight: 600;
+}
+.admin-app-main {
+ margin: 32px 32px 48px 242px;
+ min-width: 0;
+}
+.admin-app-main .admin-shell {
+ display: block;
+}
+.admin-app-footer {
+ margin-left: 242px;
+ padding: 22px 32px 28px 0;
+ border-top: 1px solid #e1e8f1;
+ display: flex;
+ gap: 12px;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ color: #9aa5b5;
+ font-size: 10px;
+}
+@media (min-width: 1200px) {
+ .card-grid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+}
+@media (max-width: 1100px) {
+ .header-inner {
+ gap: 18px;
+ }
+ .main-nav a {
+ padding: 10px;
+ }
+ .brand-name {
+ display: none;
+ }
+ .preview-label {
+ display: none;
+ }
+}
+@media (max-width: 960px) {
+ .panel-body {
+ padding: 16px 20px 22px;
+ }
+ .panel-heading {
+ padding: 22px 20px 8px;
+ }
+ .page-heading h1 {
+ font-size: 30px;
+ }
+ .admin-app-label {
+ display: none;
+ }
+}
+@media (max-width: 760px) {
+ .header-inner {
+ min-height: 66px;
+ padding: 10px 0;
+ }
+ .brand-name {
+ display: block;
+ }
+ .brand-mark {
+ font-size: 20px;
+ padding: 9px 10px;
+ }
+ .main-nav a {
+ padding: 11px 15px;
+ }
+ .main-nav .el-icon {
+ display: none;
+ }
+ .header-account .el-button {
+ padding: 8px 11px;
+ }
+ .mobile-toggle {
+ border-radius: 7px;
+ padding: 7px;
+ }
+ .app-main {
+ width: calc(100% - 28px);
+ margin-top: 22px;
+ }
+ .page-heading {
+ align-items: flex-start;
+ }
+ .page-heading h1 {
+ font-size: 28px;
+ }
+ .panel-heading {
+ padding: 20px 18px 8px;
+ min-height: 65px;
+ }
+ .panel-heading h2 {
+ font-size: 16px;
+ }
+ .panel-heading small {
+ display: block;
+ }
+ .panel-body {
+ padding: 16px 18px 20px;
+ }
+ .filters {
+ padding: 12px;
+ }
+ .filters .el-input {
+ width: 100%;
+ }
+ .training-card,
+ .team-card {
+ padding: 22px;
+ }
+ .training-illustration {
+ display: grid;
+ }
+ .summary-card {
+ padding: 20px;
+ }
+ .summary-card strong {
+ font-size: 26px;
+ }
+ .admin-app-header {
+ min-height: 70px;
+ padding: 0 18px;
+ }
+ .admin-app-header .brand-name {
+ font-size: 12px;
+ }
+ .admin-app-nav {
+ width: 62px;
+ top: 70px;
+ padding: 20px 7px;
+ }
+ .admin-app-nav a {
+ padding: 14px 15px;
+ }
+ .admin-app-nav a span {
+ display: none;
+ }
+ .admin-app-main {
+ margin: 24px 14px 35px 76px;
+ }
+ .admin-app-footer {
+ margin-left: 76px;
+ padding-right: 14px;
+ }
+ .admin-return {
+ font-size: 11px;
+ }
+ .auth-art {
+ min-height: 0;
+ }
+ .footer-inner,
+ .footer-bottom {
+ width: calc(100% - 36px);
+ }
+}
From 5c7a45fe41706722adaa6fce5ea5303ecfc9b040 Mon Sep 17 00:00:00 2001
From: kronecker <2026692687@qq.com>
Date: Mon, 5 Oct 2026 01:49:55 +0800
Subject: [PATCH 4/8] feat(frontend): add practice page previews and code
workbench
Add problem filtering, trainings, contests, submission details and rankings.
Provide a split problem/editor layout with multilingual local drafts and self-test panels.
Run and submit actions show pending service integration.
---
frontend/src/components/CodeEditor.vue | 84 ++++
frontend/src/composables/useCodeDraft.ts | 77 +++
frontend/src/router/index.ts | 58 ++-
frontend/src/views/ContestDetailView.vue | 77 +++
frontend/src/views/ContestsView.vue | 73 +++
frontend/src/views/ProblemDetailView.vue | 520 ++++++++++++++++++++
frontend/src/views/ProblemsView.vue | 62 +++
frontend/src/views/RankingsView.vue | 67 +++
frontend/src/views/SubmissionDetailView.vue | 63 +++
frontend/src/views/SubmissionsView.vue | 75 +++
frontend/src/views/TrainingDetailView.vue | 59 +++
frontend/src/views/TrainingsView.vue | 55 +++
12 files changed, 1268 insertions(+), 2 deletions(-)
create mode 100644 frontend/src/components/CodeEditor.vue
create mode 100644 frontend/src/composables/useCodeDraft.ts
create mode 100644 frontend/src/views/ContestDetailView.vue
create mode 100644 frontend/src/views/ContestsView.vue
create mode 100644 frontend/src/views/ProblemDetailView.vue
create mode 100644 frontend/src/views/ProblemsView.vue
create mode 100644 frontend/src/views/RankingsView.vue
create mode 100644 frontend/src/views/SubmissionDetailView.vue
create mode 100644 frontend/src/views/SubmissionsView.vue
create mode 100644 frontend/src/views/TrainingDetailView.vue
create mode 100644 frontend/src/views/TrainingsView.vue
diff --git a/frontend/src/components/CodeEditor.vue b/frontend/src/components/CodeEditor.vue
new file mode 100644
index 0000000..ab88a09
--- /dev/null
+++ b/frontend/src/components/CodeEditor.vue
@@ -0,0 +1,84 @@
+
+
+
diff --git a/frontend/src/composables/useCodeDraft.ts b/frontend/src/composables/useCodeDraft.ts
new file mode 100644
index 0000000..f1ac72a
--- /dev/null
+++ b/frontend/src/composables/useCodeDraft.ts
@@ -0,0 +1,77 @@
+import { computed, onScopeDispose, ref, watch } from 'vue'
+export type Language = 'cpp' | 'python' | 'rust'
+export const languageLabels: Record = {
+ cpp: 'C++',
+ python: 'Python',
+ rust: 'Rust',
+}
+export const codeTemplates: Record = {
+ cpp: '#include \nusing namespace std;\n\nint main() {\n // 在这里编写你的解法\n return 0;\n}\n',
+ python: '# 在这里编写你的解法\n\n',
+ rust: 'use std::io::{self, Read};\n\nfn main() {\n let mut input = String::new();\n io::stdin().read_to_string(&mut input).unwrap();\n // 在这里编写你的解法\n}\n',
+}
+export function useCodeDraft(problemId: string) {
+ const key = 'kf-oj:code-draft:' + problemId
+ const language = ref('cpp')
+ const drafts = ref({ ...codeTemplates })
+ const state = ref<'initial' | 'saved' | 'changed' | 'unavailable'>('initial')
+ let timer: ReturnType | undefined
+ try {
+ const raw = localStorage.getItem(key)
+ if (raw) {
+ const saved: unknown = JSON.parse(raw)
+ if (
+ saved &&
+ typeof saved === 'object' &&
+ 'version' in saved &&
+ saved.version === 1 &&
+ 'drafts' in saved &&
+ saved.drafts &&
+ typeof saved.drafts === 'object'
+ ) {
+ const storedDrafts = saved.drafts as Record
+ for (const lang of ['cpp', 'python', 'rust'] as const) {
+ const value = storedDrafts[lang]
+ if (typeof value === 'string') drafts.value[lang] = value
+ }
+ if ('language' in saved && ['cpp', 'python', 'rust'].includes(String(saved.language)))
+ language.value = saved.language as Language
+ state.value = 'saved'
+ }
+ }
+ } catch {
+ state.value = 'unavailable'
+ }
+ const code = computed({
+ get: () => drafts.value[language.value],
+ set: (value) => {
+ drafts.value[language.value] = value
+ },
+ })
+ function save() {
+ clearTimeout(timer)
+ try {
+ localStorage.setItem(
+ key,
+ JSON.stringify({ version: 1, language: language.value, drafts: drafts.value }),
+ )
+ state.value = 'saved'
+ } catch {
+ state.value = 'unavailable'
+ }
+ }
+ watch(
+ [drafts, language],
+ () => {
+ state.value = 'changed'
+ clearTimeout(timer)
+ timer = setTimeout(save, 400)
+ },
+ { deep: true },
+ )
+ onScopeDispose(() => {
+ if (state.value === 'changed') save()
+ clearTimeout(timer)
+ })
+ return { language, code, state, save }
+}
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index 79b992a..f4524b4 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -1,13 +1,67 @@
import { createRouter, createWebHistory } from 'vue-router'
-
export const router = createRouter({
history: createWebHistory(),
+ scrollBehavior: (to, _from, saved) => saved ?? (to.hash ? { el: to.hash } : { top: 0 }),
routes: [
- { path: '/', name: 'home', component: () => import('../views/HomeView.vue') },
+ {
+ path: '/',
+ name: 'home',
+ meta: { title: '首页' },
+ component: () => import('../views/HomeView.vue'),
+ },
+ {
+ path: '/problems',
+ meta: { title: '题库' },
+ component: () => import('../views/ProblemsView.vue'),
+ },
+ {
+ path: '/problems/:id',
+ meta: { title: '题目详情' },
+ component: () => import('../views/ProblemDetailView.vue'),
+ },
+ {
+ path: '/trainings',
+ meta: { title: '训练' },
+ component: () => import('../views/TrainingsView.vue'),
+ },
+ {
+ path: '/trainings/:id',
+ meta: { title: '训练详情' },
+ component: () => import('../views/TrainingDetailView.vue'),
+ },
+ {
+ path: '/contests',
+ meta: { title: '比赛' },
+ component: () => import('../views/ContestsView.vue'),
+ },
+ {
+ path: '/contests/:id',
+ meta: { title: '比赛详情' },
+ component: () => import('../views/ContestDetailView.vue'),
+ },
+ {
+ path: '/submissions',
+ meta: { title: '评测' },
+ component: () => import('../views/SubmissionsView.vue'),
+ },
+ {
+ path: '/submissions/:id',
+ meta: { title: '评测详情' },
+ component: () => import('../views/SubmissionDetailView.vue'),
+ },
+ {
+ path: '/rankings',
+ meta: { title: '排名' },
+ component: () => import('../views/RankingsView.vue'),
+ },
{
path: '/:pathMatch(.*)*',
name: 'not-found',
+ meta: { title: '页面不存在' },
component: () => import('../views/NotFoundView.vue'),
},
],
})
+router.afterEach((to) => {
+ document.title = `${String(to.meta.title ?? '首页')} · KF OnlineJudge`
+})
diff --git a/frontend/src/views/ContestDetailView.vue b/frontend/src/views/ContestDetailView.vue
new file mode 100644
index 0000000..ef8712b
--- /dev/null
+++ b/frontend/src/views/ContestDetailView.vue
@@ -0,0 +1,77 @@
+
+
+
+ 比赛/{{ contest.title }}
+
+ {{ contest.status }} · 示例
+
+
+
比赛介绍
+
{{ contest.description }}
+
比赛规则
+
+ {{
+ contest.type === 'ACM'
+ ? 'ACM 赛制按照通过题目数排序,通过数相同时比较罚时。'
+ : 'OI 赛制按照题目得分总和排序。具体规则以实际比赛公告为准。'
+ }}
+
+
参赛须知
+
+ 认真阅读每道题的数据范围和限制。赛程、题目和状态当前为界面示例,报名功能将在比赛服务接入后开放。
+
+
+
+
- 赛制
+ - {{ contest.type }}
+
+
+
- 开始时间
+ - {{ contest.start }}
+
+
+
- 比赛时长
+ - {{ contest.duration }}
+
+
+
- 题目数量
+ - {{ items.length }} 道
+
+
+ 报名功能待接入
+
返回比赛列表
+
diff --git a/frontend/src/views/ContestsView.vue b/frontend/src/views/ContestsView.vue
new file mode 100644
index 0000000..a7315fe
--- /dev/null
+++ b/frontend/src/views/ContestsView.vue
@@ -0,0 +1,73 @@
+
+
+
+
+ {{ s }}
+
+ {{
+ row.title
+ }}{{ row.status }}{{ row.ids.length }} 道查看比赛 →
+ 共 {{ items.length }} 场比赛
+
diff --git a/frontend/src/views/ProblemDetailView.vue b/frontend/src/views/ProblemDetailView.vue
new file mode 100644
index 0000000..6683560
--- /dev/null
+++ b/frontend/src/views/ProblemDetailView.vue
@@ -0,0 +1,520 @@
+
+
+
+
+
+
+
+
+
题号 {{ problem.id }}
+
{{ problem.title }}
+
+
+
+
+ {{ tag }}示例题面
+
+
+ 时间限制:1000 ms内存限制:256 MiB标准输入 / 标准输出
+
+
+
题目描述
+
{{ problem.description }}
+
输入格式
+
{{ problem.input }}
+
输出格式
+
{{ problem.output }}
+
+
+
+ 样例输入
+ 复制
+
+
{{ problem.sampleInput }}
+
+
+
+ 样例输出
+ 复制
+
+
{{ problem.sampleOutput }}
+
+
+
数据范围与提示
+
{{ problem.constraint }}
+
提交说明
+
+ 使用标准输入读取数据,只输出题目要求的结果。你可以在右侧编写代码,并在下方准备自测输入。
+
+
+
+
以下为本题的示例评测记录。
+ 查看
+
+
+
+ {{
+ discussion.title
+ }}
+
+
{{ discussion.author }} · {{ discussion.date }} · 示例文章
+
+
+ 进入讨论社区 →
+
+
+
+
+
+
+
+
+
+
+
填入题目样例
+ 尚未运行。判题服务接入后展示标准输出、错误信息与资源使用情况。
+
+
+
+
+
+ 返回题库
+
+
diff --git a/frontend/src/views/ProblemsView.vue b/frontend/src/views/ProblemsView.vue
new file mode 100644
index 0000000..51ae338
--- /dev/null
+++ b/frontend/src/views/ProblemsView.vue
@@ -0,0 +1,62 @@
+
+
+ 按题单练习
+
+
+ 重置{{ total }} 道题目
+
+
+
+
+
diff --git a/frontend/src/views/RankingsView.vue b/frontend/src/views/RankingsView.vue
new file mode 100644
index 0000000..bdae973
--- /dev/null
+++ b/frontend/src/views/RankingsView.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
NO. {{ index + 1 }} · 示例过题榜
+
{{ user.username }}{{ user.solved }} 道已通过 · {{ user.motto }}
+
+
+
榜单规则
+
持续进步默认按照通过题目数排序
+
+
+
+ 过题数竞赛积分
+
+ {{ row.rank }}{{ row.username[0] }}{{ row.username }}
+
diff --git a/frontend/src/views/SubmissionDetailView.vue b/frontend/src/views/SubmissionDetailView.vue
new file mode 100644
index 0000000..a2e9f1e
--- /dev/null
+++ b/frontend/src/views/SubmissionDetailView.vue
@@ -0,0 +1,63 @@
+
+
+
+ 评测记录/#{{ submission.id }}
+
+
+
+
+
运行时间
+
{{ submission.time ?? '—' }} ms示例数据
+
+
+
内存使用
+
{{ submission.memory === null ? '—' : submission.memory / 1024 }}
+ MiB示例数据
+
+
+
语言
+
{{ submission.language }}工具链版本待接入
+
+
+
提交用户
+
{{
+ submission.username
+ }}{{ submission.created }}
+
+
+ 当前展示评测界面的示例记录,实际用例结果、日志和源码尚未接入。
+ 返回题目返回评测列表
+
diff --git a/frontend/src/views/SubmissionsView.vue b/frontend/src/views/SubmissionsView.vue
new file mode 100644
index 0000000..9b39f10
--- /dev/null
+++ b/frontend/src/views/SubmissionsView.vue
@@ -0,0 +1,75 @@
+
+
+
+ 重置
+
+
+
+
diff --git a/frontend/src/views/TrainingDetailView.vue b/frontend/src/views/TrainingDetailView.vue
new file mode 100644
index 0000000..b77c9be
--- /dev/null
+++ b/frontend/src/views/TrainingDetailView.vue
@@ -0,0 +1,59 @@
+
+
+
+ 训练/{{ training.category }}
+
+ 从第一题开始
+
+
+
+
+
+
- 分类
+ - {{ training.category }}
+
+
+
- 难度
+ - {{ training.level }}
+
+
+
- 题目数量
+ - {{ items.length }} 道
+
+
+
- 训练记录
+ - 账号服务接入后开启
+
+
+ 先写出自己的解题思路,检查数据范围和边界情况。完成后回顾代码,记录遇到的问题。
+
+ 去讨论区交流 →
+
+
返回训练列表
+
diff --git a/frontend/src/views/TrainingsView.vue b/frontend/src/views/TrainingsView.vue
new file mode 100644
index 0000000..482765e
--- /dev/null
+++ b/frontend/src/views/TrainingsView.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
ONE STEP AT A TIME
+
循序渐进,把知识点练扎实。
+
选择适合自己的题单,先理解问题,再尝试独立完成。训练记录将在账号与评测服务接入后开启。
+
+
{ }
+
+
+ {{ c }}
+
+
+
+ {{ training.icon }}
+
+
+ {{ training.category }}
+
+
+ {{ training.title }}
+
+
{{ training.description }}
+
+ {{ training.ids.length }} 道题目示例题单开始训练 →
+
+
+
+
+
+
From 2ae3bfb6fbcc8d19c4980e3f2c7939628377bfff Mon Sep 17 00:00:00 2001
From: kronecker <2026692687@qq.com>
Date: Mon, 5 Oct 2026 01:49:58 +0800
Subject: [PATCH 5/8] feat(frontend): build homepage dashboard and interactive
tag sphere
Present announcements, submission trends, recent problems, contests and solver rankings.
Arrange blue pill labels in a TagCanvas sphere with adaptive sizing, mouse-follow and drag modes.
Allow tag selection to filter the problem list and provide keyboard-accessible links.
---
frontend/public/images/home-banner.svg | 11 +
frontend/public/images/practice-space.svg | 10 +
frontend/src/components/TagSphere.vue | 270 ++++++++++
frontend/src/views/HomeView.vue | 574 ++++++++++++++++++++--
4 files changed, 835 insertions(+), 30 deletions(-)
create mode 100644 frontend/public/images/home-banner.svg
create mode 100644 frontend/public/images/practice-space.svg
create mode 100644 frontend/src/components/TagSphere.vue
diff --git a/frontend/public/images/home-banner.svg b/frontend/public/images/home-banner.svg
new file mode 100644
index 0000000..483f2f0
--- /dev/null
+++ b/frontend/public/images/home-banner.svg
@@ -0,0 +1,11 @@
+
diff --git a/frontend/public/images/practice-space.svg b/frontend/public/images/practice-space.svg
new file mode 100644
index 0000000..626ae81
--- /dev/null
+++ b/frontend/public/images/practice-space.svg
@@ -0,0 +1,10 @@
+
diff --git a/frontend/src/components/TagSphere.vue b/frontend/src/components/TagSphere.vue
new file mode 100644
index 0000000..03cea6f
--- /dev/null
+++ b/frontend/src/components/TagSphere.vue
@@ -0,0 +1,270 @@
+
+
+
+
+
+
+
点击标签查看题目
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue
index feec2ac..b200cf3 100644
--- a/frontend/src/views/HomeView.vue
+++ b/frontend/src/views/HomeView.vue
@@ -1,36 +1,550 @@
-
-
- 开发预览
- 从一道题开始,
积累每一次进步。
-
- KF-OnlineJudge 正在建设中。题目练习、提交评测和个人学习记录将在后续版本开放。
-
-
-
-
-
平台连接
-
- {{ statusText }} · {{ version }}
-
+
+
+
+
KF ONLINEJUDGE / PRACTICE SPACE
+
把想法写成代码,
让思路一步步清晰。
+
+ 从一道题开始,沿着自己的节奏练习。
在这里写代码、做训练,也和同伴一起迎接新的挑战。
+
+
+ 开始解题 浏览训练题单
+
+
+ {{ problems.length }} 道示例题目{{ problemTags.length }} 个算法标签ACM / OI
-
重新检查
-
-
+
+

+
+
+
+
+ 站内公告示例内容
+
+
+
+
+
+ 查看评测
+
+
+ 累计提交544次
+
+
+ 通过提交342次
+
+
+ 统计周期09.28 — 10.04
+
+
+
+
+
+ router.push({ path: '/problems', query: { tag } })"
+ />
+
+
+
+
+ 完整题库
+
+
+ 换一个角度,试试新的解法。按专题练习
+
+
+
+ 全部
+ {{ String(index + 1).padStart(2, '0') }}{{ user.username[0] }}
+
+
{{ user.username }}
+
{{ user.motto }}
+
+ {{ user.solved }}过题
+
+
+
+
+
+
NEXT CHALLENGE
+
给下一场挑战留点时间。
+
+
比赛日历
+
+
+
+ {{ contest.type }}{{ contest.status }}
+
+ {{ contest.title }}
+ {{ contest.start }} · {{ contest.duration }}
+ 查看比赛
+
+ 示例赛程,实际安排以平台发布的信息为准。
+
+
+ 练习之外,也可以一起聊聊。讨论交流 创建团队 关于 KF OJ
+
+
+
{{ selectedNotice.date }} · {{ selectedNotice.category }} · 示例公告
+ {{ selectedNotice.text }}
+
From 1443f0049507c2f1515eb40843e993484ef8472e Mon Sep 17 00:00:00 2001
From: kronecker <2026692687@qq.com>
Date: Mon, 5 Oct 2026 01:50:02 +0800
Subject: [PATCH 6/8] feat(frontend): add account and community page previews
Add profiles, discussions, team creation, authentication forms and the about page.
Validate preview forms and display the available platform health endpoint.
---
frontend/src/router/index.ts | 33 ++++++
frontend/src/views/AboutView.vue | 81 +++++++++++++++
frontend/src/views/AuthView.vue | 100 +++++++++++++++++++
frontend/src/views/CreateTeamView.vue | 69 +++++++++++++
frontend/src/views/DiscussionDetailView.vue | 40 ++++++++
frontend/src/views/DiscussionsView.vue | 77 ++++++++++++++
frontend/src/views/LogoutView.vue | 13 +++
frontend/src/views/ProfileView.vue | 105 ++++++++++++++++++++
frontend/src/views/TeamsView.vue | 61 ++++++++++++
9 files changed, 579 insertions(+)
create mode 100644 frontend/src/views/AboutView.vue
create mode 100644 frontend/src/views/AuthView.vue
create mode 100644 frontend/src/views/CreateTeamView.vue
create mode 100644 frontend/src/views/DiscussionDetailView.vue
create mode 100644 frontend/src/views/DiscussionsView.vue
create mode 100644 frontend/src/views/LogoutView.vue
create mode 100644 frontend/src/views/ProfileView.vue
create mode 100644 frontend/src/views/TeamsView.vue
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index f4524b4..b9da1e3 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -54,6 +54,39 @@ export const router = createRouter({
meta: { title: '排名' },
component: () => import('../views/RankingsView.vue'),
},
+ {
+ path: '/discussions',
+ meta: { title: '讨论' },
+ component: () => import('../views/DiscussionsView.vue'),
+ },
+ {
+ path: '/discussions/:id',
+ meta: { title: '讨论详情' },
+ component: () => import('../views/DiscussionDetailView.vue'),
+ },
+ { path: '/teams', meta: { title: '团队' }, component: () => import('../views/TeamsView.vue') },
+ {
+ path: '/teams/create',
+ meta: { title: '创建团队' },
+ component: () => import('../views/CreateTeamView.vue'),
+ },
+ {
+ path: '/users/:username',
+ meta: { title: '个人主页' },
+ component: () => import('../views/ProfileView.vue'),
+ },
+ { path: '/login', meta: { title: '登录' }, component: () => import('../views/AuthView.vue') },
+ {
+ path: '/register',
+ meta: { title: '注册' },
+ component: () => import('../views/AuthView.vue'),
+ },
+ {
+ path: '/logout',
+ meta: { title: '退出登录' },
+ component: () => import('../views/LogoutView.vue'),
+ },
+ { path: '/about', meta: { title: '关于' }, component: () => import('../views/AboutView.vue') },
{
path: '/:pathMatch(.*)*',
name: 'not-found',
diff --git a/frontend/src/views/AboutView.vue b/frontend/src/views/AboutView.vue
new file mode 100644
index 0000000..a6df682
--- /dev/null
+++ b/frontend/src/views/AboutView.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
CODE · PRACTICE · GROW
+
让思路在代码里落地。
+
+ KF OnlineJudge
+ 是一个正在开发的在线评测项目,关注判题、沙箱、分布式执行与故障恢复,并逐步提供辅助答疑和个性化训练。
+
+
+
KF.
+
+
+
+
+
+
{{ feature.title }}
+
{{ feature.text }}
+
+
+
+
+ 平台 API
+ {{ status }}
+
+
+ 这是实际的 API 存活检查。{{ version && state === 'connected' ? '版本 ' + version : '' }}
+
+
数据库、题库、评测与学习服务将分别接入。
+
+
重新检查
+
+
+ 当前为前端预览版本。题目、比赛、排名和用户信息使用示例数据;页面筛选、导航和本地代码草稿已可使用。
+
+
+ 题目页可以选择语言、切换编辑器主题、保存草稿和填写自测输入。实际评测、账号和团队创建将在服务接入后开放。
+
+
备案信息待填写,平台规则和联系方式将在上线前补充。
+
+
+
diff --git a/frontend/src/views/AuthView.vue b/frontend/src/views/AuthView.vue
new file mode 100644
index 0000000..6f550a7
--- /dev/null
+++ b/frontend/src/views/AuthView.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/views/CreateTeamView.vue b/frontend/src/views/CreateTeamView.vue
new file mode 100644
index 0000000..0b219c1
--- /dev/null
+++ b/frontend/src/views/CreateTeamView.vue
@@ -0,0 +1,69 @@
+
+
+ 返回团队列表公开团队私有团队
+ 公开团队可在团队列表中发现,私有团队通过邀请加入。
需要审核仅接受邀请
+
+ 检查填写创建团队团队服务待接入
+
+
diff --git a/frontend/src/views/DiscussionDetailView.vue b/frontend/src/views/DiscussionDetailView.vue
new file mode 100644
index 0000000..c5a9f42
--- /dev/null
+++ b/frontend/src/views/DiscussionDetailView.vue
@@ -0,0 +1,40 @@
+
+
+
+ 讨论/{{ discussion.category }}
+
+ {{ discussion.category }}
+ {{ discussion.author }}{{ discussion.views }} 次浏览
+
+
+
{{ discussion.content }}
+
+ 查看相关题目 →
+ 返回讨论列表
+
diff --git a/frontend/src/views/DiscussionsView.vue b/frontend/src/views/DiscussionsView.vue
new file mode 100644
index 0000000..3ed69ef
--- /dev/null
+++ b/frontend/src/views/DiscussionsView.vue
@@ -0,0 +1,77 @@
+
+
+ 发起讨论
+
+
+ {{ c }}
+
+
+ {{ discussion.author[0] }}
+
+
+ 置顶{{ discussion.title }}
+
+
+ {{ discussion.category }}{{ discussion.author }}{{ discussion.date }}
+
+
+
+ {{ discussion.replies }} 回复{{ discussion.views }} 浏览
+
+
+
+
+
+ 提问时写清楚题号、语言、报错和已经尝试过的思路。分享题解时,也可以记录从哪里开始卡住,以及后来怎样找到解法。
+
+
+
- {{ c }}
+ - {{ discussions.filter((d) => d.category === c).length }} 篇示例文章
+
+
+
+
+
diff --git a/frontend/src/views/LogoutView.vue b/frontend/src/views/LogoutView.vue
new file mode 100644
index 0000000..59cc64c
--- /dev/null
+++ b/frontend/src/views/LogoutView.vue
@@ -0,0 +1,13 @@
+
+
+
+ 返回首页去登录
+
+
diff --git a/frontend/src/views/ProfileView.vue b/frontend/src/views/ProfileView.vue
new file mode 100644
index 0000000..f310ba8
--- /dev/null
+++ b/frontend/src/views/ProfileView.vue
@@ -0,0 +1,105 @@
+
+
+
+
{{ user.username[0] }}
+
+
PERSONAL PROFILE · 示例用户
+
{{ user.username }}
+
{{ user.motto }}
+
+ 算法爱好者持续练习
+
+
+
+ 查看提交关注
+
+
+
+
+
已通过题目
+
{{ user.solved }}示例统计
+
+
+
总提交数
+
{{ user.submissions }}示例统计
+
+
+
通过率
+
{{ Math.round((user.solved / user.submissions) * 100) }}%示例统计
+
+
+
竞赛积分
+
{{ user.rating }}示例积分
+
+
+
+
提交活动热力图 · 布局示例
+
+
+
+ 少← 提交活跃度 →多
+ 以下题目来自当前示例提交记录。
+
+
+
{{ user.motto }}
+
+
+
- 用户名
+ - {{ user.username }}
+
+
+
- 榜单位置
+ - #{{ rankings.indexOf(user) + 1 }}
+
+
+ 后续结合历史代码、提交结果和题目标签,分析练习情况并生成个性化训练建议。
+
+ 分析服务待接入
+
+
查看排行榜
+
diff --git a/frontend/src/views/TeamsView.vue b/frontend/src/views/TeamsView.vue
new file mode 100644
index 0000000..b1a2cb0
--- /dev/null
+++ b/frontend/src/views/TeamsView.vue
@@ -0,0 +1,61 @@
+
+
+ 创建团队
+
+ 示例团队 · 加入功能待接入
+
+
+
+ {{ team.initials }}
+
+
{{ team.name }}
+
{{ team.description }}
+
+ {{ tag }}
+
+
+ {{ team.members }} 名成员 · 示例申请加入
+
+
+
+
+
+
From 8c3cca6fdef6b694436e2df3832238b9d2171b36 Mon Sep 17 00:00:00 2001
From: kronecker <2026692687@qq.com>
Date: Mon, 5 Oct 2026 01:50:07 +0800
Subject: [PATCH 7/8] feat(frontend): isolate admin app with sidebar navigation
Build a separate admin entrypoint with its own router and sidebar layout.
Add preview sections for problems, contests, users, judge nodes and site settings.
Configure Vite to emit both public and admin HTML entries.
---
frontend/admin/index.html | 12 ++
frontend/src/admin/AdminApp.vue | 39 +++++
frontend/src/admin/AdminView.vue | 272 +++++++++++++++++++++++++++++++
frontend/src/admin/main.ts | 10 ++
frontend/src/admin/navigation.ts | 8 +
frontend/src/admin/router.ts | 12 ++
frontend/vite.config.ts | 9 +
7 files changed, 362 insertions(+)
create mode 100644 frontend/admin/index.html
create mode 100644 frontend/src/admin/AdminApp.vue
create mode 100644 frontend/src/admin/AdminView.vue
create mode 100644 frontend/src/admin/main.ts
create mode 100644 frontend/src/admin/navigation.ts
create mode 100644 frontend/src/admin/router.ts
diff --git a/frontend/admin/index.html b/frontend/admin/index.html
new file mode 100644
index 0000000..ad457f0
--- /dev/null
+++ b/frontend/admin/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
KF 管理控制台
+
+
+
+
+
+
diff --git a/frontend/src/admin/AdminApp.vue b/frontend/src/admin/AdminApp.vue
new file mode 100644
index 0000000..3c81b9f
--- /dev/null
+++ b/frontend/src/admin/AdminApp.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/admin/AdminView.vue b/frontend/src/admin/AdminView.vue
new file mode 100644
index 0000000..c6535b7
--- /dev/null
+++ b/frontend/src/admin/AdminView.vue
@@ -0,0 +1,272 @@
+
+
+ 返回站点
+
+
+
+ 前端预览待接入待接入待接入
+
新建题目草稿
+
+
+
+ 编辑预览查看
+
创建比赛
+
+
+
+ 查看
+
+
+
+ 用户查看主页修改角色
+
实际节点、负载、心跳和任务状态将在协调器接口接入后展示。
+ {{
+ row.status
+ }}暂停接单
+
+
+
+ 保存设置当前编辑只在本页面预览
+
+
+
+
+ 公开草稿
+ 题面编辑、测试数据和版本发布将在题库接口接入后启用。
关闭预览保存题目
+
diff --git a/frontend/src/admin/main.ts b/frontend/src/admin/main.ts
new file mode 100644
index 0000000..49dfc10
--- /dev/null
+++ b/frontend/src/admin/main.ts
@@ -0,0 +1,10 @@
+import { createApp } from 'vue'
+import { registerUi } from '../plugins/ui'
+import { adminRouter } from './router'
+import AdminApp from './AdminApp.vue'
+import '../styles/base.css'
+import '../styles/blueprint.css'
+
+const app = createApp(AdminApp)
+registerUi(app)
+app.use(adminRouter).mount('#admin-app')
diff --git a/frontend/src/admin/navigation.ts b/frontend/src/admin/navigation.ts
new file mode 100644
index 0000000..9d70ebf
--- /dev/null
+++ b/frontend/src/admin/navigation.ts
@@ -0,0 +1,8 @@
+export const adminSections = [
+ { id: 'overview', title: '概览', icon: 'monitor' },
+ { id: 'problems', title: '题目管理', icon: 'problems' },
+ { id: 'contests', title: '比赛管理', icon: 'contests' },
+ { id: 'users', title: '用户管理', icon: 'teams' },
+ { id: 'judges', title: '判题节点', icon: 'connection' },
+ { id: 'settings', title: '站点设置', icon: 'settings' },
+]
diff --git a/frontend/src/admin/router.ts b/frontend/src/admin/router.ts
new file mode 100644
index 0000000..eac06de
--- /dev/null
+++ b/frontend/src/admin/router.ts
@@ -0,0 +1,12 @@
+import { createRouter, createWebHashHistory } from 'vue-router'
+import { adminSections } from './navigation'
+
+export const adminRouter = createRouter({
+ history: createWebHashHistory('/admin/index.html'),
+ scrollBehavior: () => ({ top: 0 }),
+ routes: [{ path: '/:section?', component: () => import('./AdminView.vue') }],
+})
+adminRouter.afterEach((to) => {
+ const section = adminSections.find((item) => item.id === (to.params.section || 'overview'))
+ document.title = `${section?.title ?? '页面不存在'} · KF 管理控制台`
+})
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index cacb854..a5b61e3 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -1,10 +1,19 @@
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
+import { fileURLToPath, URL } from 'node:url'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [vue()],
+ build: {
+ rollupOptions: {
+ input: {
+ main: fileURLToPath(new URL('./index.html', import.meta.url)),
+ admin: fileURLToPath(new URL('./admin/index.html', import.meta.url)),
+ },
+ },
+ },
server: {
port: 5173,
strictPort: true,
From f108e88c47e6d604184d8a34dfd39be06762e159 Mon Sep 17 00:00:00 2001
From: kronecker <2026692687@qq.com>
Date: Mon, 5 Oct 2026 15:26:41 +0800
Subject: [PATCH 8/8] fix(frontend): save pending code drafts before page exit
---
Makefile | 1 +
frontend/README.md | 3 +
frontend/package.json | 1 +
frontend/src/composables/useCodeDraft.ts | 15 ++-
frontend/tests/useCodeDraft.test.ts | 164 +++++++++++++++++++++++
frontend/tsconfig.json | 3 +-
6 files changed, 184 insertions(+), 3 deletions(-)
create mode 100644 frontend/tests/useCodeDraft.test.ts
diff --git a/Makefile b/Makefile
index 441d684..83679b0 100644
--- a/Makefile
+++ b/Makefile
@@ -43,6 +43,7 @@ check-frontend:
cd frontend && $(PNPM) lint
cd frontend && $(PNPM) format:check
cd frontend && $(PNPM) typecheck
+ cd frontend && $(PNPM) test
cd frontend && $(PNPM) build
contracts:
diff --git a/frontend/README.md b/frontend/README.md
index 19dc9a2..3cc7779 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -7,6 +7,7 @@ Vue 3 + TypeScript + Vite + Element Plus,使用 Vue Router 与 Pinia。
pnpm install --frozen-lockfile
pnpm dev
pnpm typecheck
+pnpm test
pnpm build
```
@@ -22,3 +23,5 @@ pnpm build
已有代码按实际需要落位;`components/` 和 `stores/` 是后续扩展位置,不放空状态实现。
API 类型更新执行根目录 `make contracts`;前端通过 `pnpm check:api` 拒绝过期类型。
登录凭据、模型密钥和数据库连接不能进入 `VITE_` 环境变量。
+
+`pnpm test` 验证代码草稿在刷新、切到后台与页面离开时的保存行为,已纳入 `make check-frontend`。
diff --git a/frontend/package.json b/frontend/package.json
index 59b7459..58f7e6c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,6 +12,7 @@
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview --host 127.0.0.1",
"typecheck": "vue-tsc --noEmit",
+ "test": "node --test tests/*.test.ts",
"lint": "eslint . --max-warnings 0",
"format": "prettier --write .",
"format:check": "prettier --check .",
diff --git a/frontend/src/composables/useCodeDraft.ts b/frontend/src/composables/useCodeDraft.ts
index f1ac72a..f1c17c7 100644
--- a/frontend/src/composables/useCodeDraft.ts
+++ b/frontend/src/composables/useCodeDraft.ts
@@ -67,11 +67,22 @@ export function useCodeDraft(problemId: string) {
clearTimeout(timer)
timer = setTimeout(save, 400)
},
- { deep: true },
+ // Mark pending changes before a lifecycle event can run in the same tick.
+ { deep: true, flush: 'sync' },
)
- onScopeDispose(() => {
+ function flushPendingDraft() {
if (state.value === 'changed') save()
+ }
+ function onVisibilityChange() {
+ if (document.visibilityState === 'hidden') flushPendingDraft()
+ }
+ window.addEventListener('pagehide', flushPendingDraft)
+ document.addEventListener('visibilitychange', onVisibilityChange)
+ onScopeDispose(() => {
+ flushPendingDraft()
clearTimeout(timer)
+ window.removeEventListener('pagehide', flushPendingDraft)
+ document.removeEventListener('visibilitychange', onVisibilityChange)
})
return { language, code, state, save }
}
diff --git a/frontend/tests/useCodeDraft.test.ts b/frontend/tests/useCodeDraft.test.ts
new file mode 100644
index 0000000..b2cc75a
--- /dev/null
+++ b/frontend/tests/useCodeDraft.test.ts
@@ -0,0 +1,164 @@
+import assert from 'node:assert/strict'
+import { test, type TestContext } from 'node:test'
+import { effectScope, nextTick } from 'vue'
+import { codeTemplates, useCodeDraft, type Language } from '../src/composables/useCodeDraft.ts'
+
+type SavedDraft = { version: number; language: Language; drafts: Record
}
+
+function browser(t: TestContext) {
+ const page = new EventTarget()
+ const document = Object.assign(new EventTarget(), {
+ visibilityState: 'visible' as DocumentVisibilityState,
+ })
+ const storage = {
+ items: new Map(),
+ writes: 0,
+ failWrites: false,
+ getItem(key: string) {
+ return this.items.get(key) ?? null
+ },
+ setItem(key: string, value: string) {
+ this.writes++
+ if (this.failWrites) throw new Error('Storage unavailable')
+ this.items.set(key, value)
+ },
+ }
+ const globals = { window: page, document, localStorage: storage }
+ const descriptors = Object.keys(globals).map(
+ (name) => [name, Object.getOwnPropertyDescriptor(globalThis, name)] as const,
+ )
+ for (const [name, value] of Object.entries(globals)) {
+ Object.defineProperty(globalThis, name, { configurable: true, writable: true, value })
+ }
+ t.mock.timers.enable({ apis: ['setTimeout'] })
+ const scopes: ReturnType[] = []
+ t.after(() => {
+ for (const scope of scopes) scope.stop()
+ for (const [name, descriptor] of descriptors) {
+ if (descriptor) Object.defineProperty(globalThis, name, descriptor)
+ else Reflect.deleteProperty(globalThis, name)
+ }
+ })
+ return {
+ page,
+ document,
+ storage,
+ open(problemId = '1001') {
+ const scope = effectScope()
+ scopes.push(scope)
+ return { scope, ...scope.run(() => useCodeDraft(problemId))! }
+ },
+ read(problemId = '1001'): SavedDraft | null {
+ return JSON.parse(storage.getItem('kf-oj:code-draft:' + problemId) ?? 'null')
+ },
+ }
+}
+
+test('pagehide saves the latest edit before the debounce and restores it after reload', (t) => {
+ const env = browser(t)
+ const draft = env.open()
+ draft.code.value = '// latest edit before refresh'
+ env.page.dispatchEvent(new Event('pagehide'))
+ assert.equal(env.read()?.drafts.cpp, draft.code.value)
+ assert.equal(draft.state.value, 'saved')
+ t.mock.timers.tick(400)
+ assert.equal(env.storage.writes, 1)
+ draft.scope.stop()
+ assert.equal(env.open().code.value, '// latest edit before refresh')
+})
+
+test('hiding the page flushes pending edits; remaining visible does not', async (t) => {
+ const env = browser(t)
+ const draft = env.open()
+ draft.code.value = '// edited while visible'
+ await nextTick()
+ env.document.dispatchEvent(new Event('visibilitychange'))
+ assert.equal(env.storage.writes, 0)
+ env.document.visibilityState = 'hidden'
+ env.document.dispatchEvent(new Event('visibilitychange'))
+ assert.equal(env.read()?.drafts.cpp, draft.code.value)
+ t.mock.timers.tick(400)
+ assert.equal(env.storage.writes, 1)
+})
+
+test('normal typing still debounces writes and saves the most recent edit', async (t) => {
+ const env = browser(t)
+ const draft = env.open()
+ draft.code.value = '// first edit'
+ await nextTick()
+ t.mock.timers.tick(399)
+ assert.equal(env.storage.writes, 0)
+ draft.code.value = '// second edit'
+ await nextTick()
+ t.mock.timers.tick(399)
+ assert.equal(env.storage.writes, 0)
+ t.mock.timers.tick(1)
+ assert.equal(env.read()?.drafts.cpp, '// second edit')
+ assert.equal(env.storage.writes, 1)
+})
+
+test('leaving a Vue page saves even an edit made in the same tick', (t) => {
+ const env = browser(t)
+ const draft = env.open()
+ draft.code.value = '// latest edit before navigation'
+ draft.scope.stop()
+ assert.equal(env.read()?.drafts.cpp, '// latest edit before navigation')
+ t.mock.timers.tick(400)
+ assert.equal(env.storage.writes, 1)
+})
+
+test('disposing the scope removes page lifecycle listeners', (t) => {
+ const env = browser(t)
+ const removePage = t.mock.method(env.page, 'removeEventListener')
+ const removeDocument = t.mock.method(env.document, 'removeEventListener')
+ env.open().scope.stop()
+ assert.deepEqual(
+ removePage.mock.calls.map((call) => call.arguments[0]),
+ ['pagehide'],
+ )
+ assert.deepEqual(
+ removeDocument.mock.calls.map((call) => call.arguments[0]),
+ ['visibilitychange'],
+ )
+ env.page.dispatchEvent(new Event('pagehide'))
+ env.document.visibilityState = 'hidden'
+ env.document.dispatchEvent(new Event('visibilitychange'))
+ assert.equal(env.storage.writes, 0)
+})
+
+test('pagehide preserves the selected language and keeps problem drafts separate', (t) => {
+ const env = browser(t)
+ const first = env.open('1001')
+ first.code.value = '// C++ draft'
+ first.language.value = 'python'
+ first.code.value = '# Python draft'
+ const second = env.open('1002')
+ second.code.value = '// another problem'
+ env.page.dispatchEvent(new Event('pagehide'))
+ assert.deepEqual(env.read('1001'), {
+ version: 1,
+ language: 'python',
+ drafts: { cpp: '// C++ draft', python: '# Python draft', rust: codeTemplates.rust },
+ })
+ assert.equal(env.read('1002')?.drafts.cpp, '// another problem')
+ first.scope.stop()
+ const restored = env.open('1001')
+ assert.equal(restored.language.value, 'python')
+ assert.equal(restored.code.value, '# Python draft')
+})
+
+test('a failed lifecycle save reports unavailable storage without throwing', (t) => {
+ const env = browser(t)
+ const draft = env.open()
+ env.storage.failWrites = true
+ draft.code.value = '// cannot persist yet'
+ assert.doesNotThrow(() => env.page.dispatchEvent(new Event('pagehide')))
+ assert.equal(draft.state.value, 'unavailable')
+ t.mock.timers.tick(400)
+ assert.equal(env.storage.writes, 1)
+ env.storage.failWrites = false
+ draft.code.value = '// storage recovered'
+ env.page.dispatchEvent(new Event('pagehide'))
+ assert.equal(draft.state.value, 'saved')
+ assert.equal(env.read()?.drafts.cpp, '// storage recovered')
+})
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index a216dce..2f52f31 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -6,6 +6,7 @@
"moduleResolution": "Bundler",
"strict": true,
"noEmit": true,
+ "allowImportingTsExtensions": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
@@ -15,5 +16,5 @@
"noUnusedParameters": true,
"types": ["vite/client", "node"]
},
- "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
+ "include": ["src/**/*.ts", "src/**/*.vue", "tests/**/*.ts", "vite.config.ts"]
}