# GrillKit 源码证据审计报告

- 审计日期：2026-09-07（Asia/Shanghai）
- 审计对象：`https://github.com/GrillKit/grillkit.git`
- 受众：评估或构建 AI 技术面试训练系统的工程与产品团队
- 方法：默认分支源码静态审计 + 不需要 API key、不上传数据、不启动长期服务的有限离线验证
- 证据口径：下文明确区分 **已实现**（可见源码直接支持）、**README 宣称**（文档描述）、**未验证**（本次没有安全执行或缺少运行条件）。路径均相对于仓库根目录；行号对应下述 HEAD。

## 1. 执行摘要

GrillKit 是一个单用户、本地优先的 FastAPI 技术面试训练器。它已经实现了理论问答、编码任务、组合会话、按轮计时、结构化 AI 评分、最多两轮 AI 追问、结果回顾、已知题排除、Whisper 听写、Piper 朗读、SQLite/Alembic 持久化和可选 Judge0 执行层。FastAPI 应用通过 lifespan 管理语音运行时并挂载六组路由（`app/main.py:29-64`）；理论和编码交互分别使用 WebSocket（`app/theory/api/routes.py:81-144`、`app/coding/api/routes.py:85-131`）。

整体架构不是简单聊天壳，而是带显式 session/section/task 聚合、事务边界与阶段切换的领域化实现。创建会话时一次性创建 shell 和所选 section（`app/interview/use_cases/create_session.py:64-135`）；阶段顺序由四种 session mode 决定（`app/interview/domain/session_phases.py:82-97`）；section 完成后激活下一阶段（`app/interview/use_cases/advance_phase.py:24-64`）。

最重要的产品差异是：**公开/隐藏测试的代码框架已实现，但当前内置编码题库没有实际测试用例。** `task_spec` 会持久化 public/hidden tests，并在发往客户端时移除隐藏测试和公开测试期望值（`app/coding/domain/task_spec.py:12-69`）；Run 和 Submit 也分别有执行公开与隐藏测试的流程（`app/coding/use_cases/run_tests.py:30-166`、`app/coding/use_cases/submit_solution.py:113-190`）。但本次对 `data/coding/**/*.yaml` 的可复现统计为：44/44 题均为 `evaluation_mode: ai`，`evaluation_mode: tests`、`public_tests:`、`hidden_tests:` 均为 0。因此 README 的“Run against public tests / Submit for hidden tests”描述（`README.md:19`、`README.md:73-75`）属于通用能力宣称，而非当前内置题库可直接体验的事实；这些题在 Judge0 上实际退化为 compile-only，再交由 AI 评分（`app/coding/use_cases/run_tests.py:91-110`）。

当前不应直接暴露到不可信网络。项目明确没有认证，WebSocket 在 HTTP 下不加密（`SECURITY.md:45-50`）；Compose 将应用发布为宿主机 `8000:8000`（`docker-compose.yml:6-9`），配置写接口、访谈数据和本地明文 API key 因而都依赖部署者的网络隔离。Judge0 server/worker 以 `privileged: true` 运行并使用示例级固定数据库/Redis 密码（`docker-compose.yml:21-74`）。此外，容器入口存在高风险启动缺陷：以默认 root 启动时，在 `gosu` 后直接 `exec` 应用（`docker-entrypoint.sh:10-12`），位于其后的 migration 调用（`docker-entrypoint.sh:15`）不可达；而 FastAPI lifespan 本身不运行 migration（`app/main.py:29-40`）。新容器能否自动建库因此未获源码支持，必须修正或由运维显式执行 `alembic upgrade head`。

结论：GrillKit 的会话领域模型、结构化评价管线、语音运行时抽象和回顾数据模型具有较高复用价值；但在投入共享部署或严肃评测前，应优先补齐认证/CSRF/Origin 防护、修复迁移入口、消除 privileged Judge0 默认部署、把 Monaco 本地化或加 SRI/CSP、为内置题库加入真实 public/hidden tests，并增加不依赖外部服务的端到端 CI 门禁。

## 2. 版本基线

| 项目 | 证据/结果 |
|---|---|
| 远端 URL | `origin = https://github.com/GrillKit/grillkit.git`（`git remote -v`） |
| 默认分支 | `main`；`refs/remotes/origin/HEAD -> origin/main` |
| 当前分支 | `main`，保留默认分支 |
| HEAD | `15a400afb08349ad9b73f31a0c92ba147032164c` |
| commit 日期 | `2026-08-16T16:07:34+03:00` |
| commit 摘要 | `Refactor/fix archetecture mistakes (#31)` |
| 工作树 | 审计前后 `git status --short` 均为空；未修改源码 |
| License | `pyproject.toml:5-6` 声明 Apache-2.0；`LICENSE:1-8` 含 SPDX 标识及 Apache License 2.0 正文 |
| 仓库大小 | 克隆后约 12 MiB；创建未完成的 `.venv` 后约 15 MiB；Git pack 约 7.61 MiB；tracked files 约 3.7 MiB |
| 规模 | `app/` 234 个 Python 文件、约 22,457 行；`tests/` 109 个 test 文件、约 18,889 行、751 个 test 函数（静态计数） |

版本元数据存在漂移：README badge 与 changelog 最新发布为 `2026.8.9`（`README.md:5`、`CHANGELOG.md:17`），源码未安装时 fallback 也是 `2026.8.9`（`app/__init__.py:9-12`），但包清单仍是 `2026.6.12`（`pyproject.toml:1-7`）。构建后的 `importlib.metadata.version("grillkit")` 会优先返回包清单版本，可能导致 UI/API 报告旧版本。

主要直接依赖由 `pyproject.toml:8-22` 给出：FastAPI 0.136.1、Uvicorn 0.47.0、SQLAlchemy 2.0.49、OpenAI SDK 2.38.0、Jinja2 3.1.6、PyYAML 6.0.3、Pydantic 2.13.4、httpx、faster-whisper、huggingface-hub、piper-tts、Alembic。开发依赖含 pytest、pytest-asyncio、mypy、Ruff（`pyproject.toml:24-32`）。Python 要求 `>=3.12`（`pyproject.toml:7`）。

## 3. 架构

### 3.1 服务与边界

**已实现：** 单体 FastAPI + 服务端 Jinja 页面 + 原生浏览器 JavaScript。`create_app()` 挂载静态目录并注册 interview/platform/theory/coding/speech/question_voice 六组 router（`app/main.py:43-64`）。应用 lifespan 只负责创建 `SpeechRuntimeCoordinator`、加载已安装语音资产和关闭时卸载（`app/main.py:29-40`）。

代码按领域拆分为：

- `interview/`：session shell、selection、阶段编排、总评、dashboard/results；
- `theory/`：理论 section、任务轮次、文本/音频提交、评分与追问；
- `coding/`：编码 section、Monaco 状态、Run/Submit、Judge0、编码评分；
- `platform/`：配置、LLM catalog、语音运行时协调；
- `speech/` 与 `question_voice/`：听写和题目朗读；
- `shared/`：SQLAlchemy、UoW、YAML loader、AI/Judge0/Whisper/Piper gateways。

事务由 `UnitOfWork` 显式 commit/rollback/close（`app/shared/infrastructure/uow.py:37-86`），`InterviewUnitOfWork` 延迟构造 interview、theory、coding、run-attempt、known-question repositories（`app/interview/repositories/uow.py:27-73`）。这是可复用的良好边界，但当前同步 SQLAlchemy Session 在 async 路由中使用，网络等待与 SQLite 写锁的交互仍需压测；源码为 SQLite 启用了 WAL 与 30 秒 busy timeout（`app/shared/infrastructure/database.py:27-58`）。

### 3.2 HTTP / WebSocket 面

**已实现：** 主要 HTTP 路由包括 dashboard `/`、setup、config、known questions、results/review、coding run/state、speech model/voice download/status和理论音频提交。理论 WS 为 `/interview/{id}/theory/ws`（`app/theory/api/routes.py:121-144`），编码 WS 为 `/interview/{id}/coding/ws`（`app/coding/api/routes.py:85-131`），听写 WS 为 `/interview/{id}/dictation`（`app/speech/api/dictation.py:46`）。

理论 WS 接受 `answer`、`timeout`、`ping`、`complete`，未知类型返回 error（`app/theory/api/ws_session.py:55-95`）；消息被映射为领域事件并通过 `safe_send_json` 返回（`app/theory/api/routes.py:96-115`）。编码前端由 HTTP Run 与 WS Submit 分离：浏览器 POST `/coding/run`（`static/js/coding_session.js:390-420`），Submit 通过 WS 发送代码（`static/js/coding_session.js:433-461`）。

## 4. 端到端流程

1. 配置：用户在 `/config` 添加一个 OpenAI-compatible 模型并选中。工厂目前只支持 `openai-compatible`（`app/ai/factory.py:13-50`）；catalog 与选中状态写到 `data/llm_models.json`（`app/platform/domain/llm_catalog.py:17-58`、`:180-200`）。
2. 创建：`/setup` 将模式和理论/编码 branch selection 解析为 `SessionSelection`。`CreateInterviewSession` 创建 UUID shell，按选项排除 known IDs，构建题目计划并创建 section（`app/interview/use_cases/create_session.py:84-133`）。
3. 阶段：`theory_only`、`coding_only`、`theory_then_coding`、`coding_then_theory` 映射为固定 section 顺序（`app/interview/domain/session_phases.py:82-97`）。`active_phase()` 激活 pending section 并选择当前可交互阶段（`app/interview/use_cases/advance_phase.py:24-45`）。
4. 理论提交：WS 校验 `question_id` 与非空 `answer_text` 后调用 submission use case（`app/theory/api/ws_session.py:98-132`）；提交持久化答案，调用结构化 evaluator，保存 1–5 分和反馈，并可生成 follow-up task。TheoryEvaluator 的最大追问深度为 2（`app/theory/domain/evaluator.py:41-48`、`:337-349`）。超时路径由服务端校验 timer 到期后记 0 分，而不是只相信客户端（`app/theory/domain/entities.py:111-118`；计时异常定义见 `app/theory/domain/exceptions.py:56-87`）。
5. 编码提交：Monaco 草稿按 interview/task/round 存在 `sessionStorage`（`static/js/coding_editor.js:92-125`）。Run 调 Judge0 并持久化 attempt；Submit 先执行 hidden tests/compile-only、保存代码和摘要，再调用 AI evaluator（`app/coding/use_cases/submit_solution.py:113-190`、`:326-375`）。CodingEvaluator 同样最多两轮追问（`app/coding/domain/evaluator.py:29-33`、`:85-116`）。
6. 完成：总评流程汇总 theory/coding section，确保 section narrative feedback，调用 session evaluator，保存 `overall_feedback` 与 completed 状态，并返回总分/max score（`app/interview/use_cases/complete_session.py:49-154`）。结果页与 theory/coding review 路由已存在（`app/interview/api/results.py:18-94`）。

**未验证：** 本次没有启动浏览器或服务，没有用真实 LLM 跑完整会话，也没有验证断线重连、并发提交、后台最后一轮追问评分与页面跳转的运行时行为。

## 5. 题库与判题

### 5.1 理论 YAML

**已实现：** 理论题库位于 `data/questions/{track}/{level}/{category}.yaml`；loader 使用 `yaml.safe_load`，解析 id/type/difficulty/tags、本地化问题文本、可选代码和 expected points（`app/shared/questions.py:87-136`）。本地化缺失时回退到英语并记录 warning（`app/shared/questions.py:44-84`）。多分类加载按 question id 去重（`app/shared/questions.py:139-164`）。planner 校验 track/level/category、过滤 legacy coding rows、应用排除集合并构建计划（`app/theory/domain/question_planner.py:28-55`、`:134-170`）。

静态统计：84 个理论 YAML、429 个 `id:` 条目；`data/questions/questions_map.yaml` 是额外的汇总映射，不是普通 category 文件。样例 `data/questions/python/junior/basics.yaml:7-30` 展示双语文本、follow_ups 与 expected_points。值得注意的是，loader 没有读取 YAML 的 `follow_ups` 字段（`app/shared/questions.py:113-135`），实际追问由 LLM 动态生成；题库静态 follow-up 文案因此目前是未使用数据。

### 5.2 Monaco、Judge0 与测试可见性

**已实现：** Monaco 0.45.0 从 jsDelivr 动态加载（`static/js/coding_editor.js:8-37`），编辑内容保存在 sessionStorage（`:92-155`），Run 结果渲染前执行 HTML escaping（`:221-290`）。Judge0 gateway 只支持 Python language id 71（`app/shared/infrastructure/gateways/judge0_config.py:7-13`、`:35-50`），向 `/submissions?wait=true` POST source/stdin/CPU/memory/compile-only，并可传 `X-Auth-Token`（`app/shared/infrastructure/gateways/judge0.py:87-165`）。默认 CPU 5 秒、内存 128,000 KB（`app/shared/infrastructure/gateways/judge0_config.py:11-13`）。

对 tests 模式，Run 逐个执行公开测试、严格比较 stdout、首个失败即停止（`app/coding/use_cases/run_tests.py:118-166`、`:232-246`）。Submit 把 hidden_tests 临时映射为 tests 模式执行（`:30-62`），AI prompt 得到的是 hidden test summary，不是直接把测试期望回传浏览器（`app/coding/domain/evaluator_prompts.py:75-86`）。发往客户端的 task spec 会删除 `hidden_tests`，并只保留公开测试名称（`app/coding/domain/task_spec.py:52-69`）。

**README 宣称但当前题库不成立：** README 宣称 Monaco Run 使用公开测试、Submit 使用隐藏测试（`README.md:19`、`:73-75`）。本次静态统计确认 44 个编码题全部为 `evaluation_mode: ai`，0 个 tests 模式，0 个 public_tests，0 个 hidden_tests。`data/coding/python/junior/basics.yaml:7-36` 是典型 AI-only 题。AI 模式 Run 会走 compile-only（`app/coding/use_cases/run_tests.py:91-99`）；没有 hidden tests 时 Submit 也回退到 AI/compile-only（`:46-52`）。因此“隐藏测试失败则最高 3 分”的 prompt 规则（`app/coding/domain/evaluator_prompts.py:8-27`）在当前内置题库不会由真实隐藏测试触发。

## 6. 模型与语音

### 6.1 模型 provider、评分与追问

**已实现：** `AIProvider` 定义 text generate/validate/close，Streaming 和 Audio 为能力接口（`app/ai/base.py:43-128`）。唯一实现 `OpenAICompatibleProvider` 使用官方 AsyncOpenAI SDK，兼容 OpenAI、Ollama、vLLM 等端点（`app/ai/openai_compatible.py:24-57`）；支持普通、stream 和 base64 WAV `input_audio`（`:105-206`、`:208-239`），连接测试调用 `models.list()`（`:241-251`）。

理论与编码评分都将 Pydantic JSON schema 嵌入 system prompt，模型输出经 JSON 解析和 Pydantic 校验；截断或 invalid JSON 最多重试一次，token budget 上限 4096（`app/shared/structured_evaluation.py:14-20`、`:63-103`、`:149-198`；`app/shared/json_parser.py:197-265`）。这比自由文本解析稳健，但仍没有可信执行/规则评分作为 AI 分数的独立校准。候选人的答案、代码、错误输出和题库内容进入模型 prompt，存在 prompt injection 与敏感代码外发风险；使用云模型时 README 的“数据在本地”只能理解为持久化位置，不代表推理内容不离机。README 自己也要求用户选择云或本地 endpoint（`README.md:24`、`:150-166`）。

### 6.2 Whisper / Piper

**已实现：** FastAPI lifespan 的 coordinator 根据配置加载已安装模型，缺失则卸载，不会在启动时自动下载（`app/platform/domain/speech_runtime.py:56-127`）。Whisper 使用 faster-whisper，默认 CPU/int8，可由环境变量覆盖（`app/shared/infrastructure/gateways/whisper.py:19-46`）；模型通过 Hugging Face 下载到 staging、校验后原子 promote（`app/shared/infrastructure/gateways/whisper_model.py:87-162`）。Piper 从 `rhasspy/piper-voices` 下载 `.onnx` 与 `.onnx.json`（`app/shared/infrastructure/gateways/piper_voice.py:38-42`、`:123-192`），本地合成 WAV（`app/shared/infrastructure/gateways/piper.py:82-112`），题目音频以规范化文本 SHA-256 缓存于 locale 目录（`app/shared/infrastructure/gateways/tts_cache.py:20-50`、`:53-95`）。

理论音频答案 HTTP 路由会一次性 `await file.read()`（`app/theory/api/routes.py:35-66`），再校验 WAV 和转 float32（`app/theory/use_cases/submit_answer.py:26-29`）。本次未见路由级上传体积限制；恶意大文件可能造成内存压力。听写/朗读/音频模型直评均未运行验证，且没有下载任何 Whisper/Piper 权重。

## 7. 数据模型与迁移

**已实现：** SQLAlchemy 模型包括：

- `interviews`：UUID、locale、selection JSON、session mode、status、overall feedback、时间戳（`app/shared/infrastructure/models.py:17-62`）；
- `theory_sections` 与 `answers`：一对一 section、每题每轮快照、rubric、答案、score、feedback、timer（`:65-161`）；
- `coding_sections`、`coding_tasks`、`code_run_attempts`：任务 spec/代码/隐藏测试摘要/评分以及每次 Run 的代码和输出（`:164-315`）；
- `known_questions`：`branch + bank_item_id` 复合主键（`:318-334`）。

题目和 task spec 在创建会话时做快照，保证题库更新后历史会话仍可回顾，这是值得复用的设计。代价是用户提交的答案、代码、stdout/stderr、AI 反馈都明文进入数据库。数据库 URL 默认指向 `data/db/grillkit.db`（`app/shared/infrastructure/database.py:19-24`），可通过 `DATABASE_URL` 替换。

Alembic 有从 `20260526_0001` 到 `20260615_0011` 的线性 11 个 revision；最终迁移创建 known_questions（`alembic/versions/20260615_0011_known_questions.py:11-30`）。`run_migrations()` 明确执行 upgrade head（`app/shared/infrastructure/database.py:65-68`）。

**高风险缺陷：** Docker entrypoint 的控制流使 root 默认路径跳过 migration：`chown` 后 `exec gosu ... "$@"`（`docker-entrypoint.sh:10-12`），因此 `python -c ...run_migrations()`（`:15`）只有容器最初就以非 root 身份运行时才执行。Dockerfile 没有 `USER` 指令（`Dockerfile:28-59`），Compose 也未设置 `user:`（`docker-compose.yml:1-19`），故默认路径正是 root。未启动容器验证，但该不可达路径是源码级确定事实。

## 8. 部署需求

**README 宣称：** Docker + Compose；云 provider API key 或本地 OpenAI-compatible server（`README.md:86-101`）。编码模式还需 Judge0 CE（`:118-126`）。语音模型由 UI 下载，Whisper 约 0.5/1.5/3 GB（规格见 `app/shared/speech_models.py:39-64`），Piper 每 locale voice 约 60 MB（`README.md:103-110`）。

**已实现：** 多阶段 Dockerfile 基于 Python 3.12，builder 使用 frozen lock 安装生产依赖；runtime 安装 gosu，复制 app/questions/templates/static/Alembic，并配置 HTTP healthcheck（`Dockerfile:6-23`、`:28-59`）。Compose 默认只起 app；`coding` profile 增加 PostgreSQL、Redis、Judge0 server/worker（`docker-compose.yml:1-77`）。`./data` 被挂载到 `/app/data`，包含 SQLite、config、catalog、模型和缓存（`docker-compose.yml:8-18`；路径定义见 `app/shared/paths.py:7-19`）。

**未验证：** Docker image build、Compose 启动、healthcheck、Judge0 cgroup/privileged 兼容性、非 SQLite 数据库、反向代理 TLS、GPU Whisper、模型下载/加载和冷启动资源消耗均未执行。

## 9. 测试结果

### 9.1 实际执行

| 验证 | 命令/方法 | 结果 |
|---|---|---|
| 仓库身份与更新 | `git remote get-url origin`、`git fetch --prune origin`、默认分支 clone | 成功；origin/默认分支/HEAD 如版本基线 |
| 工作树保护 | 多次 `git status --short` | 空；源码未修改 |
| Python 语法 | `.venv/bin/python -m compileall -q app tests` | **通过（exit 0）** |
| YAML 可解析性 | Ruby Psych 对 `data/questions/**/*.yaml` 与 `data/coding/**/*.yaml` 执行本地 parse | **通过：101 文件** |
| 题库静态一致性 | `rg` 统计 mode/tests 字段 | 44 AI mode；0 tests mode；0 public_tests；0 hidden_tests |
| 规模统计 | `rg --files`、`wc -l` | app 234 Python 文件；tests 109 文件/751 test 函数 |
| 依赖同步 | `uv sync --frozen --extra dev` | **主动中止（exit 130）**；按用户要求不继续等待大型运行时依赖。仅创建 ignored `.venv`，未下载模型权重 |
| pytest | 未执行 | pytest 尚未安装完成 |
| Ruff | 未执行 | Ruff 尚未安装完成 |
| mypy | 未执行 | mypy 尚未安装完成 |

安装前已检查 `pyproject.toml`、`uv.lock`、`Dockerfile`、`docker-entrypoint.sh`、CI；未发现项目自定义 build backend、setup/postinstall hook。同步只访问锁文件指定的 PyPI 制品，没有运行应用、没有真实 API key、没有调用付费 API、没有上传用户数据。`uv.lock` 对制品记录 URL/hash/size；CI 预期执行 frozen sync、Ruff check、format check、mypy、pytest（`.github/workflows/ci.yml:15-30`）。

测试体系本身覆盖面广：`tests/conftest.py:28-42` mock speech startup；`:56-75` 使用内存 SQLite；`:92-193` 伪造 Judge0；`:197-214` 注入 FakeProvider。存在 full lifecycle、theory/coding full-flow、migration、WebSocket、timer、audio、config 和 review 测试文件。但“有测试源码”不等于本次 HEAD 已通过，完整 CI 结果在本报告中明确为未验证。

## 10. 安全风险

按部署在可信单机上的设计假设评估；若对 LAN/互联网开放，风险显著升高。

1. **P0：无认证且默认发布端口。** 项目明确无认证（`SECURITY.md:45-50`），Compose 将 8000 发布到宿主（`docker-compose.yml:6-9`）。任何可达客户端都可能读取历史、创建/完成会话、修改/删除配置、触发 LLM/Judge0/模型下载，造成隐私泄露和费用/资源消耗。
2. **P0：容器迁移不可达。** 默认 root 启动路径在 migration 前 `exec`（`docker-entrypoint.sh:10-15`），可能导致新部署或升级数据库 schema 缺失。虽非传统攻击面，但会造成数据完整性/可用性事故。
3. **P1：Judge0 以 privileged 运行。** server 和 worker 均为 `privileged: true`（`docker-compose.yml:48-68`），执行不可信候选代码的组件拥有高宿主权限；固定 Postgres/Redis 密码也不适合共享网络（`:21-45`）。应采用 Judge0 官方隔离建议、专用主机/VM、网络分段和秘密管理。
4. **P1：API key 明文落盘。** catalog 直接把 `api_key` 写进 JSON（`app/platform/domain/llm_catalog.py:104-117`、`:193-200`）；UI 只做显示掩码，不是静态加密（`app/platform/domain/config.py:101-105`）。应限制文件权限、避免共享 volume 备份泄漏，生产使用 secret store。
5. **P1：CSRF / WebSocket Origin 未防护。** 本次全仓安全控制搜索未发现认证、CSRF token、Origin allowlist、TrustedHost、HTTPSRedirect 或 CSP middleware；WS 连接接受后直接读消息（`app/theory/api/routes.py:96-101`）。在浏览器可访问的网络环境中可能发生跨站 WebSocket/状态变更请求。
6. **P1：用户可配置 base URL 带来 SSRF。** `ProviderFactory` 直接接受并传递 base URL（`app/ai/factory.py:17-48`），连接测试调用远端 `models.list()`（`app/ai/openai_compatible.py:241-251`）。无认证攻击者可借此探测内网；应校验 scheme/host、阻止 link-local/metadata/private ranges，或只允许管理员配置。
7. **P1：第三方 Monaco 运行时代码。** 浏览器动态加载 `https://cdn.jsdelivr.net/.../monaco-editor@0.45.0/...`，无 SRI（`static/js/coding_editor.js:18-35`）。这破坏完全离线使用并引入供应链依赖；应 self-host、锁定哈希并配置 CSP。
8. **P2：音频上传无显式体积上限。** 路由一次性读取整个 UploadFile（`app/theory/api/routes.py:35-60`）。应在代理和应用层限制 Content-Length、采样率、时长和解码后样本数。
9. **P2：敏感内容外发与 prompt injection。** 答案、代码、run stderr 和 hidden test summary进入 LLM prompt（`app/coding/domain/evaluator_prompts.py:89-129`）；OpenAI-compatible provider 会发送至配置 endpoint（`app/ai/openai_compatible.py:105-143`）。应在 UI 明示数据边界，加入 provider 分级/本地模式，裁剪错误输出，并把模型评分视为不可信建议。
10. **P2：同步 wait=true 的执行放大。** Judge0 每个测试都是单独 HTTP submission，串行且遇首败停止（`app/coding/use_cases/run_tests.py:118-153`）；最多 20 次 Run 的限制由环境配置（`app/coding/domain/run_result.py:23-30`、`:104-123`），但缺少用户/来源级限流。共享部署可能被计算 DoS。
11. **P2：隐藏测试只是在 API 序列化层隐藏。** 当前 client serializer 删除 hidden tests（`app/coding/domain/task_spec.py:52-69`），设计正确；但完整 task spec 明文存数据库（`:12-49`）。若未来增加真实 hidden tests，备份/数据库读权限等同于测试泄露，应单独加密或拆表并避免 review/debug 输出。
12. **P3：安全文档与版本漂移。** `SECURITY.md:7-10` 仅列 2026.5.20 supported，而当前 README 为 2026.8.9；应更新支持矩阵与披露策略。

## 11. 可复用设计

- **Session shell + section aggregates：** 把模式、顺序、理论与编码生命周期分离，利于新增系统设计、行为面试等 section（`app/interview/domain/session_phases.py:52-80`）。
- **题目快照而非运行时回查：** 问题文本、rubric、starter code、task spec 均随 session 固化，历史审计稳定（`app/shared/infrastructure/models.py:112-157`、`:212-255`）。
- **领域事件到 transport message：** use case 产出 AnswerSaved/Evaluating/Feedback/Completed，WebSocket 层只映射协议，便于 HTTP/SSE/队列复用（`app/interview/domain/events.py:10-75`、`app/theory/api/ws_protocol.py`）。
- **结构化评分：** Pydantic schema + 解析重试避免自由文本脆弱性（`app/shared/structured_evaluation.py:63-103`）。
- **能力接口：** Streaming/Audio provider 分离，STT/TTS 由 protocol 与 coordinator 注入（`app/ai/base.py:83-128`、`app/platform/domain/speech_runtime.py:15-49`）。
- **双层判题：** deterministic Judge0 信号 + AI rubric 可兼顾算法题和开放式工程题；但必须保证题库真的提供测试。
- **公开/隐藏序列化边界：** server task spec 与 client-safe spec 分开（`app/coding/domain/task_spec.py:12-69`）。
- **Known questions：** 用 branch + bank ID 做轻量掌握度记忆，并在规划时排除（`app/interview/use_cases/create_session.py:93-123`）。
- **模型资产 staging/promote：** 下载到临时目录、验证、原子替换、finally 清理，适合大型本地模型管理（`app/shared/infrastructure/gateways/whisper_model.py:117-142`）。

## 12. 构建同类系统的启示

1. 先定义确定性状态机和持久化事件，再接 LLM。LLM 只负责评价/追问，不应决定当前 task 是否有效、timer 是否到期或 section 是否可进入。
2. 将“题库能力”和“题库内容成熟度”分别验收。支持 public/hidden tests 的代码不能替代一套带真实测试、golden solutions、边界样例和校验工具的题库。
3. 评分应多信号融合：测试通过率、复杂度/静态分析、rubric AI 评分、追问表现和人工校准；隐藏测试失败的分数上限应由服务端规则执行，不只写在 prompt。
4. 默认部署应安全：loopback bind 或强制认证、CSRF/Origin/Host/CSP、TLS 反代模板、请求/上传/任务速率限制、secret store，并把代码沙箱隔离到独立 VM/节点。
5. 模型调用要有数据治理：在调用前展示 endpoint 与将发送的数据，支持本地-only policy、脱敏/截断、token/费用预算、审计日志和幂等提交。
6. 大模型与语音资产应保持可选依赖。当前单一 production dependency 集合会让不使用语音的部署也下载大型运行时；建议拆为 `speech`/`tts` extras 和单独镜像 profile。
7. 对 WebSocket 提交使用 idempotency key / optimistic version，避免重连与重复消息造成重复评分或重复扣费；并把后台评价任务持久化到可靠队列，而非仅依赖进程内 background task。
8. CI 应新增：所有 YAML schema/唯一 ID/locale/rubric 检查、每个 tests-mode 题的 reference solution 对 public+hidden 全通过、错误解法必须失败、client payload 不含 hidden expectations、Docker 新库 migration smoke test、无网络单元测试门禁。

## 13. 已实现、README 宣称、未验证汇总

| 主题 | 已实现 | README 宣称 | 本次状态 |
|---|---|---|---|
| FastAPI/WS | 理论、编码、听写 WS 与 HTTP 页面/API | 实时理论与编码流程 | 源码确认；运行未验证 |
| 会话状态机 | 4 种模式、section 状态、timer、最多 2 追问 | theory/coding/组合、超时 0 分 | 源码确认 |
| 理论题库 | YAML loader、locale fallback、rubric、selection | 多 track/level/topic | 84 文件/429 ID；101 YAML 总体解析通过 |
| Monaco | 动态 CDN Monaco、草稿、Run/Submit UI | 内置代码编辑器 | 源码确认；浏览器未验证 |
| Judge0 | Python、compile/tests、资源上限、run limit | coding profile 可运行 | gateway 源码确认；服务未启动 |
| 公测/隐测 | server 框架和 client stripping | Run public / Submit hidden | 当前 44 题均无测试，README 体验未兑现 |
| AI 评分/追问 | Pydantic structured evaluation、1–5、最多 2 follow-ups | 结构化评分与追问 | 源码确认；真实模型未调用 |
| Whisper/Piper | 下载、加载、听写、合成、缓存 | 离线语音 | 源码确认；模型未下载/运行 |
| Provider | OpenAI-compatible 一个 adapter | OpenAI/Ollama/vLLM/others | adapter 确认；兼容性未实测 |
| 存储/迁移 | SQLite/SQLAlchemy/UoW、11 migrations | data volume 保留历史 | 源码确认；发现 entrypoint 跳过迁移 |
| Docker Compose | app + coding profile | 一键部署 | 配置确认；build/up 未验证 |
| 测试/CI | 109 test files、CI 五步 | 未额外宣称 | compileall/YAML 通过；pytest/Ruff/mypy 未跑 |

## 14. 未完成项与限制

- 按用户要求停止等待大型依赖后，`uv sync --frozen --extra dev` 被主动中止；因此没有完整执行 pytest、Ruff、format check、mypy。
- 未启动 FastAPI、Docker、Compose、Judge0、Postgres 或 Redis；未验证容器 healthcheck 和 migration 实际故障表现。
- 未调用任何 LLM、付费 API 或用户配置 endpoint；模型输出质量、成本、超时、重试与 provider 兼容性未验证。
- 未下载或加载 Whisper/Piper 模型；听写精度、TTS 音质、资源占用与缓存并发未验证。
- 未做动态渗透、依赖漏洞数据库扫描、浏览器 E2E、性能/并发/恢复测试。
- 报告结论限定于 HEAD `15a400afb08349ad9b73f31a0c92ba147032164c` 的可见源码；后续提交可能改变结论。

## 15. 查阅文件清单

完整或重点查阅：

- 根与部署：`README.md`、`ARCHITECTURE.md`、`CHANGELOG.md`、`SECURITY.md`、`LICENSE`、`NOTICE`、`pyproject.toml`、`uv.lock`、`.env.example`、`.gitignore`、`Dockerfile`、`docker-compose.yml`、`docker-entrypoint.sh`、`deploy/judge0.conf`、`.github/workflows/ci.yml`。
- 应用入口/页面：`app/main.py`、`app/templating.py`、`templates/interview.html`、`templates/coding_interview.html`、`templates/config_form.html`、`templates/base.html`。
- 前端：`static/js/coding_editor.js`、`static/js/coding_session.js`、`static/js/coding_complete.js`、`static/js/interview_timer.js`、`static/js/interview_audio_answer.js`、`static/js/dictation.js`、`static/js/session_phases.js`、`static/js/known_questions.js`。
- Interview：`app/interview/api/{routes,setup,results,known_questions,deps}.py`、`domain/{entities,session_phases,evaluation_aggregator,session_evaluation,events,serialization,value_objects}.py`、`use_cases/{create_session,advance_phase,complete_session}.py`、`repositories/{uow,interview,known_questions,mappers}.py`、`queries/{session_page,results_page,loader,dashboard,projection}.py`。
- Theory：`app/theory/api/{routes,ws_session,ws_protocol,audio_answer}.py`、`domain/{entities,evaluator,evaluator_models,evaluator_prompts,question_planner,timer,value_objects}.py`、`use_cases/{create_section,submit_answer,navigate_tasks}.py`、`support/answer_commit.py`、repositories/queries/schemas 相关文件。
- Coding：`app/coding/api/{routes,ws_session,ws_protocol}.py`、`domain/{entities,evaluator,evaluator_models,evaluator_prompts,task_planner,task_spec,test_harness,run_result,value_objects}.py`、`use_cases/{create_section,run_tests,submit_solution,navigate_tasks}.py`、`support/{coding_availability,evaluation_commit,run_result_mapper}.py`、repositories/queries/schemas 相关文件。
- AI/platform：`app/ai/{base,factory,openai_compatible,llm_models,faster_whisper_transcriber,audio_probe}.py`、`app/platform/api/config.py`、`domain/{config,llm_catalog,speech_runtime,speech_settings}.py`、`use_cases/{add_llm_model,save_config,delete_config}.py`。
- Speech/TTS：`app/speech/api/{routes,dictation,dictation_protocol}.py`、`use_cases/dictation.py`、`queries/readiness.py`、`app/question_voice/api/routes.py`、`use_cases/generate_question_audio.py`、`app/shared/infrastructure/gateways/{whisper,whisper_model,whisper_storage,piper,piper_voice,piper_storage,tts_cache}.py`、`audio_wav.py`、`artifact_download.py`、`model_download.py`。
- Shared/data：`app/shared/{questions,coding,structured_evaluation,json_parser,section,timed_task,task_timer,paths}.py`、`infrastructure/{database,models,uow}.py`、`infrastructure/gateways/{judge0,judge0_config,ai_context}.py`、全部 `alembic/versions/*.py`、全部 84 个 `data/questions/**/*.yaml` 与 17 个 `data/coding/**/*.yaml`（结构化解析/字段统计）。
- Tests：`tests/conftest.py`、`tests/fakes.py`、`tests/e2e/test_e2e_full_lifecycle.py`，以及 theory/coding/interview/platform/speech/question_voice/shared 下 109 个 `test_*.py` 的目录与覆盖主题；未逐行阅读全文的测试文件仍通过文件名、测试函数和 fixture 结构纳入覆盖盘点。

## 16. 复现命令摘要

在仓库根目录执行：

```bash
git remote -v
git symbolic-ref refs/remotes/origin/HEAD
git show -s --format='%H%n%cI%n%an%n%s' HEAD
git status --short
du -sh .
git count-objects -vH

find data/questions -name '*.yaml' | wc -l
find data/coding -name '*.yaml' | wc -l
rg -n '^\s*-?\s*id:' data/questions | wc -l
rg -n '^\s*-?\s*id:' data/coding | wc -l
rg 'evaluation_mode: ai' data/coding | wc -l
rg 'evaluation_mode: tests' data/coding | wc -l
rg 'public_tests:' data/coding | wc -l
rg 'hidden_tests:' data/coding | wc -l

.venv/bin/python -m compileall -q app tests
ruby -e 'require "yaml"; fs=Dir["data/questions/**/*.yaml"]+Dir["data/coding/**/*.yaml"]; fs.each{|f| YAML.load_file(f)}; puts fs.length'
```

完整 CI 的预期命令见 `.github/workflows/ci.yml:26-30`，本次未完成：

```bash
uv sync --frozen --extra dev
uv run ruff check .
uv run ruff format --check .
uv run mypy .
uv run pytest
```
