Skip to content
luc1ferxxPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

115 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CS2 Demo Coach

CI

上传一场 CS2 比赛的 .dem 文件,在网页上复盘自己:战术回放、回合、时间轴,以及按回合整理、能直接跳到那一刻的规则建议。

  • 建议来自 9 条确定性规则(backend/app/analysis/rules.py),每条都附带可查看的依据。仓库不调用 OpenAI 或任何大模型;"Coach" 是产品名,不是实现方式。
  • 第一人称视频是可选增强:由我们自己运维的渲染机离线生成短片段。没有视频,复盘照样完整可用。
  • 当前阶段:准备以网站形式开放邀请制内测(Steam 登录、上传配额)。上线门槛见 2D 内测上线计划,进展见 项目进展。

目录

能做什么

场景 说明
比赛库 /dashboard 上传 .dem(分片并行上传,断网自动重试,刷新页面后重新选择同一个文件即可续传;显示进度,可取消,可拖拽)、解析状态、失败原因与重新处理、每场比分(战队名取自 demo)、搜索、按地图和状态筛选、排序、重命名、软归档;永久删除(需确认,不可撤销)
复盘工作区 /demos/{id} 战术地图、回合条、时间轴、播放控制与键盘快捷键;地图、时间轴、回合和建议共用同一个时间与回合状态;复盘位置写入网址,刷新后可恢复;浏览器不保存回放(响应是 private, no-store),快的是 API 这一侧:最近返回过的回放留在 API 内存里,解析完成后和 API 重启时还会在后台预先填好,所以打开比赛(包括第一次)通常不用再花约 1 秒读取和计算
实时名单与道具 播放时两队名单随时间更新:金钱、血量、护甲、武器、携带的道具、到此刻的击杀/死亡、全队装备价值;地图上显示飞行中的道具和烟雾、火、闪光、手雷的效果范围
实时按键显示 回放时显示一名玩家此刻按着的键:W/A/S/D、静步(Shift)、蹲(Ctrl)、跳(空格)和鼠标左右键,按下的键亮起;跟随在地图或名单上点选的玩家,没有点选时跟随正在复盘的玩家,阵亡后隐藏。面板不遮挡地图:宽屏复盘工作台上放在该玩家所在队伍名单的底部,较窄、较矮的屏幕和手机上放在地图下方。按键来自 demo 里的 usercmd 记录,没有这类数据的 demo 不显示(目前只在 BLAST.tv 的 GOTV demo 上验证过);较早上传的比赛在后台重新解析后才会出现。面板的视觉设计改编自 cs2-sandbox(MIT,见 THIRD_PARTY_NOTICES)
回合经济 回合条下标出两队每回合的经济类型(手枪局 / 全起 / 强起 / 半起 / ECO,按冻结时间结束时的装备价值和余钱判断),可按类型和队伍筛选回合;"经济"面板画出每回合两队的装备价值,并统计各经济类型的回合数和胜场。旧比赛没有经济数据时不显示
道具反查 回放区的"道具反查"页:按道具类型、投掷队伍或玩家、回合范围筛选,在地图上拖框选出落点区域,列出落在选区里的每一颗;点列表里的一颗或地图上的轨迹打开这颗道具的投掷分析(见下一行),分析里的"在战术回放里看"回到战术回放、停在出手前 2 秒,并只突出投掷者("显示全部"恢复)。旧比赛在后台补齐道具数据前,这一页会提示稍后刷新
道具投掷分析 在道具反查里点一颗道具,或在战术回放里点一颗飞行中的烟雾弹、闪光弹、手雷或燃烧瓶,右侧换成这颗道具的分析,地图放大到它的轨迹和投掷者出手前后的走位(可切回全图):出手按键(左键扔 / 右键扔 / 左右键一起扔)、扔法(站投、走投、蹲投、跳投、跳蹲投)、出手前后各 1 秒的慢放(0.25x / 0.5x / 1x,按键面板同步),以及"复制站位指令":复制 setpos …; setang … 控制台指令,在开了作弊(sv_cheats 1)的练习服里站到出手点、对准出手视角。按键和扔法依赖 demo 里的 usercmd 记录;站位指令要回放里有出手站位,较早上传的比赛在后台重新解析后才有。面板布局和扔法判定改编自 cs2-sandbox(MIT,见 THIRD_PARTY_NOTICES)
个人复盘 默认用登录的 Steam 账号匹配比赛中的玩家(本地 development 模式默认匹配 xelex),也可切换成其他玩家;只列出该玩家的建议,按回合分组、回合内按严重程度排序,一键跳到最值得回看的一条
建议 按回合分组;可按严重程度、规则和关键词筛选;"查看这一刻"跳到事件前几秒;每条建议可评价"有帮助 / 无关 / 判断不足"。每次阵亡最多一张卡,标出击杀武器和阵亡前后双方人数,输掉的回合另标「回合输了」,站位过远等同一次阵亡的其他原因附在卡片里;建议多于 5 条时,「全部回合」视图(没有筛选和搜索时)顶部先列出「本场最值得回看」的 5 条(按回合输赢、首个阵亡、人数劣势排序),下面的回合分组只列其余的。射击类建议有两种:「移动射击」(用步枪、狙击枪、沙鹰或 R8 连着开的一串枪里,至少 3 枪是边移动或在空中开的,而且这几枪一枪没中)和「第一枪没急停」(交火的第一枪速度明显高于这把枪的稳定线或在空中,结果整串没打中或 2 秒内阵亡),每名玩家每回合最多一条,「第一枪没急停」每人每场最多 3 条,依据每一枪记录的移动速度,有按键记录时还标出开枪时按着的键;冲锋枪、霰弹枪等其他枪不判;较早上传的比赛在后台重新解析并重算建议之后才有
建议评价 评价按事件 ID 保存,重新解析和规则更新后的后台重算之后,建议还在就仍然有效;按规则汇总的评价是调整规则阈值的依据,见 coaching_feedback_v1
第一人称片段 围绕某条建议或当前时刻创建短片段任务,由独立渲染机完成;生产环境默认关闭(RENDER_CLIPS_ENABLED=0)。默认 RENDER_WORKER_MODE=fallback 下,片段任务会立即以 RENDER_WORKER_UNAVAILABLE 失败;external 模式由操作者启用的 Windows 渲染机(CSDM + HLAE + FFmpeg,FFprobe 校验)完成
账户与数据 /account 账户信息、网站保存了哪些数据;删除账户及全部数据(只对 Steam 登录的真实账户开放,所有设备上的登录同时失效)
隐私说明 /privacy 公开页面,不需要登录:保存什么、存在哪里、保存多久、删除后还剩什么;每个页面的页脚都有链接和"与 Valve 无关联"声明

明确不做:

  • 不做 AI 聊天或模型生成的文字;
  • 不控制用户电脑、不读取本地文件、不录屏;
  • 不在 API 或解析容器里运行 CS2 / Steam / OBS / ffmpeg;
  • 不把用户上传的 MP4 当作主流程;
  • 不把 .dem、回放帧或视频存进 PostgreSQL。

浏览器不能直接播放 .dem:它是游戏状态与事件记录,不是视频流。

快速开始

需要 Docker。启动整套本地环境(前端、API、解析 worker、Postgres、Redis):

docker compose up --build

本地为 development 模式,不需要登录。可以用"示例比赛"快速生成一场模拟数据,也可以上传真实 .dem 走完整解析流程。

Compose 里的前端是生产构建(frontend/Dockerfile.preview,next build 后 next start),和线上部署是同一种构建;改了前端代码要重新 --build 才能看到(启动脚本每次都带 --build,代码没变时命中缓存)。next build 会做 lint 和类型检查,有错误时前端镜像构建失败。

已配置好的 Windows 机器可以双击根目录的 Start CS2 Coach.cmd,或运行:

& .\scripts\start-local.ps1        # -SkipRenderer 只启动网页;-PreflightOnly 只检查本机配置

只改前端时,可以只启动后端服务,再单独跑前端开发服务器(两者都占用 3000 端口,不要同时运行 Compose 里的 frontend)。开发服务器用的是开发版 React,每帧开销是生产构建的数倍,判断回放是否流畅请用 Compose 里的生产构建:

docker compose up --build -d api worker postgres redis
cd frontend && npm install && npm run dev

在 Mac 上开发的启动与数据交接见 macos_development_windows_release_v1。

架构

浏览器 ── Next.js 15 前端(App Router)
            │
            ▼
         FastAPI API ──── PostgreSQL   账号、Steam 身份、比赛元数据、任务、建议、评价、上传账本、上传会话、删除任务
            │        ├─── Redis        解析任务队列、登录会话
            │        ├─── 对象存储     .dem 原文件、回放 JSON、视频(开发用本地目录,生产用私有 S3 兼容存储)
            │        └─── 本地暂存盘   未完成上传的分片(命名卷 upload-staging,完成时转存到对象存储)
            ▼
         解析 worker(独立子进程解析:超时、内存上限、环境变量白名单、低权限账户)→ 规则分析 → 写回回放与建议
            ⋮
         独立渲染机(可选,操作者手动启用):领取片段任务 → 渲染 → 回传 MP4
  • 上传流程:浏览器先建上传会话,再把 .dem 按 8 MiB 分片、最多 4 片并行上传到 API 主机的本地暂存盘(每片核对长度和 SHA-256,第 0 片当场检查文件签名),断网时退避重试,刷新后可续传;最后 complete 把各片拼成一个流交给原有的 intake:先流式写入隔离区,同时计算真实长度和 SHA-256,校验通过后才转为正式文件,并在同一个事务里建比赛、解析任务和上传账本。旧的单请求 POST /uploads/demo 保留给 curl 和冒烟脚本,走同一个 intake。见 api_reference_v1。
  • 存储与数据库:所有文件都经过 backend/app/services/storage/ 的统一接口读写;PostgreSQL 只存元数据和存储引用。
  • 任务队列:带租约和数据库对账。worker 崩溃后,任务会自动回到队列。
  • 回放响应缓存:GET /demos/{id}/replay 先做 owner 检查,再按回放存储引用和公开视频状态算出 ETag,带 Cache-Control: private, no-store(和其他登录后的响应一样,浏览器不保存回放),If-None-Match 匹配时返回 304。API 进程在内存里按 LRU 缓存最近返回的 gzip 响应(REPLAY_RESPONSE_CACHE_MB,默认 64,0 关闭),重启即清空;删除比赛后路由直接返回 404。缓存会在后台预热:解析完成、回放升级、视频写入之后(worker 通过 Redis 频道 REPLAY_READY_CHANNEL 通知 API),以及 API 重启时最近完成的 REPLAY_WARM_RECENT 场真实比赛(默认 10),所以第一次打开也直接命中(REPLAY_WARM_ENABLED,默认开)。预热只填同一份内存缓存,不另存任何东西。返回的是公开投影而不是存储的回放:玩家、炸弹和事件的高度 z 取到 0.1,不带存储里重复的 kills/deaths 列表(前端从 events 算击杀),gzip 后小约两成;存储的回放和规则分析的输入保持原精度。见 api_reference_v1。
  • 比分摘要:解析完成时把双方战队名、开局阵营和最终比分存进 demos.match_summary(按玩家帧判定每回合阵营,半场和加时换边都算对),比赛库和复盘页从 matchSummary 读取;此前已完成、或摘要版本低于当前版本(2)的比赛由 worker 空闲时回填,只读 replay 和源 .dem,不改 replay。阵营、队伍和比分的判定规则前后端各实现一份,写在 match_summary.py 和 frontend/lib/match-stats.ts 的注释里,由共享用例 fixtures/match-rules/ 固定。见 api_reference_v1。
  • 后台补算:worker 空闲时(队列里有任务就让出)每轮最多处理一场已完成的比赛,不改比赛状态和时间,也不占上传次数。
    • 回放早于当前契约的比赛,从存储的 .dem 重新解析一次(REPLAY_UPGRADE_ENABLED / REPLAY_UPGRADE_MAX_ATTEMPTS / REPLAY_UPGRADE_RETRY_SECONDS);这一步不重新分析建议。
    • 建议早于当前规则版本 COACHING_RULES_VERSION 的比赛(真实解析的比赛和 Steam 导入,不含示例比赛;有待升级的回放时先等升级完成),用已存的回放重新跑规则、替换建议(COACHING_RECOMPUTE_ENABLED 默认开启,COACHING_RECOMPUTE_MAX_ATTEMPTS 默认 3 次,COACHING_RECOMPUTE_RETRY_SECONDS 默认 900 秒、每失败一次翻倍;次数用完的比赛保留旧建议,下次提升规则版本后重新计数)。id 没变的建议保留评价;不再生成的建议,评价行保留但不显示、不计入汇总。
    • 改了规则输出就提升 COACHING_RULES_VERSION(backend/app/analysis/version.py),已有比赛会在后台逐场重算,见 coaching_feedback_v1。
frontend/        app/ 页面 · components/ 界面组件 · lib/ 客户端与纯函数 helper · public/maps/ 雷达图
backend/         app/api/ 路由 · app/analysis/ 规则 · app/parser/ demoparser2 适配与归一化
                 app/services/ 领域服务(demo_service/、storage/ 为包)· app/workers/ 解析 worker
render-worker/   独立渲染机的 runner 与适配器(fake / manual / CSDM),见 render-worker/README.md
docs/            接口与配置参考、运维手册、验收记录
deploy/          生产 Caddyfile 与 env 模板(配合 docker-compose.prod.yml)
scripts/         verify.sh · rc_check.sh · cloud_preview_smoke.py · Windows 启动脚本 · deploy/ VPS 部署脚本
fixtures/        前后端共用的测试用例(match-rules/:每回合阵营与比分规则;round-economy/:每回合经济类型)

开发与验证

./scripts/verify.sh      # 与 CI 相同:后端编译/测试、render-worker、ruff、mypy、bash -n / shellcheck / 生产 compose 配置、前端测试/lint/类型检查/构建
./scripts/rc_check.sh    # verify.sh + Docker 构建启动 + 健康检查 + 预览冒烟(发布候选用)

verify.sh 不会在第一个失败处停下,最后会汇总所有失败项。它优先使用 .venv/ 里的 Python,也可以用 PYTHON=/path/to/python ./scripts/verify.sh 指定。bash -n 必跑;本机没有 shellcheck 或 docker compose 时对应步骤跳过(CI 的 deploy-kit job 会跑),compose config 只在本地渲染三文件叠加的生产形态,不启动任何东西。最后一步只在本地跑:仓库根目录有 .dem 时,按 worker 的流程解析清单认识的每一场(约 10 秒一场),和 backend/tests/fixtures/real_demo_manifest.json 里的聚合数逐项比对,不认识的跳过(CI 没有 .dem)。rc_check.sh 带 SAMPLE_DEMO_PATH 时也先离线比对这份清单,再上传样本。

单独运行:

cd frontend && npm run lint && npm run typecheck && npm test   # Vitest + Testing Library 组件/页面测试
cd frontend && node lib/demo-library.test.mjs                   # 每个 lib/*.test.mjs 都能直接运行
cd frontend && npm run build

# Windows 上把 .venv/bin/python 换成 .venv/Scripts/python.exe
PYTHONPATH=backend .venv/bin/python -m unittest discover backend/tests
.venv/bin/python -m ruff check .       # 规则与豁免理由见 ruff.toml
.venv/bin/python -m mypy               # 范围与遗留问题见 mypy.ini
  • 测试约定:交互组件和页面状态的改动(加载、轮询、失败、重试)要附带同目录下的 *.test.tsx,用 mock 掉的 @/lib/api 挂载组件;测试数据放在 frontend/lib/test-fixtures/review.ts。前后端必须一致的规则用 fixtures/ 下的共享 JSON 用例固定:fixtures/match-rules/*.json 同时由 backend/tests/test_match_side_rules.py 和 frontend/lib/match-side-rules.test.mjs 逐个运行;fixtures/round-economy/*.json 同时由 backend/tests/test_round_economy.py 和 frontend/lib/round-economy-fixtures.test.mjs 运行,固定 frontend/lib/round-economy.ts 与后端移植 backend/app/analysis/round_economy.py 的经济类型判定。改规则时两边和用例一起改。
  • 真实 demo 清单:backend/tests/fixtures/real_demo_manifest.json 按文件大小和 sha256 前缀记录本地四场 demo 的聚合数(地图、回合/帧/事件数、各事件族及带坐标的数量、道具/状态/按键/射击计数、每条规则的建议数、分析输出的哈希),不含名字、SteamID、坐标或事件内容。升级 demoparser2 或其他解析依赖、改变解析/归一化输出、改地图变换、改分析器,或提升 COACHING_RULES_VERSION / REPLAY_CONTRACT_VERSION / MATCH_SUMMARY_VERSION 时,运行 PYTHONPATH=backend .venv/bin/python -m app.cli.real_demo_manifest update,审过 diff 后和引起变化的改动一起提交;版本号记在每条清单里,只提升版本不重新生成,检查会按设计失败。
  • 依赖锁:Docker 镜像和 CI 都从 backend/requirements.lock 安装(pip install --require-hashes,每个直接和传递依赖都固定版本和哈希,目标 Linux / CPython 3.12)。backend/requirements.txt 仍是手工维护的输入,改了它就用锁文件头部的 uv pip compile 命令重新生成。本地 Windows 的 .venv 照旧装 requirements.txt:锁文件按 Linux 解析(含 uvloop、不含 colorama),在 Windows 上装不上。
  • 手动检查:界面改动还需要在浏览器里走一遍比赛库和复盘页,清单见 release_candidate_qa_v1。
  • 更多命令:常用 API 调用(上传、状态轮询、回放、建议、评价、删除、渲染任务)见 API Reference 和 AGENTS.md。

配置

完整列表(106 项)见 Configuration Reference。最常用的:

变量 默认值 说明
AUTH_MODE 未设置(必填) development / test / production
AUTH_PROVIDER 未设置(production 必填) steam,或兼容路径 oidc
STEAM_LOGIN_ALLOWLIST 未设置 内测邀请名单:逗号分隔的 Steam ID64,或 * 表示所有 Steam 账号;production + Steam 登录时必填。修改后需要重建 API 容器(up -d),docker compose restart 不会读取新值
DEMO_UPLOAD_DAILY_LIMIT / DEMO_ACTIVE_PARSE_LIMIT / PARSE_QUEUE_GLOBAL_LIMIT 10 / 2 / 50 仅 production:每人滚动 24 小时上传数(按上传账本计,删除比赛不退还)、每人同时处理中的比赛数、全站处理中的比赛数;0 表示不限
RENDER_CLIPS_ENABLED 0 仅 production:为 1 时才开放生成和重试第一人称片段
FRONTEND_PUBLIC_URL / BACKEND_PUBLIC_URL http://localhost:3000 / :8000 production 必须是同源 HTTPS
NEXT_PUBLIC_API_BASE_URL http://localhost:8000 浏览器访问 API 和媒体的地址
NEXT_PUBLIC_PRIVACY_CONTACT / NEXT_PUBLIC_DATA_REGION 未设置 构建时写进前端,显示在 /privacy:隐私问题和删除请求的联系方式(邮箱、网址或文字;VPS 部署必填,deploy.sh 会检查)和服务器所在地区;未设置时分别说明"本站没有公开联系方式"和显示"海外 VPS,具体地区由站长部署时选定"
DATABASE_URL / REDIS_URL 本地默认值 API 与 worker。production 的 REDIS_URL 必须带密码:preview/production 的 compose 用必填的 REDIS_PASSWORD 拼出 redis://:<密码>@redis:6379/0
ARTIFACT_STORAGE_BACKEND / OBJECT_STORAGE_BUCKET local / 未设置 production 必须是 s3 和一个私有 bucket
STEAM_WEB_API_KEY 未设置 仅服务端使用,production 必填
STEAM_CREDENTIAL_ENCRYPTION_KEY 仓库内的开发 key production 必须换成 32 随机字节的 URL-safe base64,例如 python -c "import os,base64;print(base64.urlsafe_b64encode(os.urandom(32)).decode())"
RENDER_WORKER_MODE / RENDER_WORKER_TOKEN fallback / dev-render-worker-token production 必须更换 token
MAX_DEMO_UPLOAD_BYTES 1073741824 单个 .dem 的实际字节上限
UPLOAD_PART_BYTES / UPLOAD_SESSION_TTL_SECONDS / UPLOAD_SESSION_GLOBAL_LIMIT / UPLOAD_STAGING_MIN_FREE_BYTES 8388608 / 86400 / 6 / 5368709120 分片上传:分片大小、会话从创建算起的硬过期时间、全站同时未完成的上传数、暂存盘至少保留的余量(后两项是资源限制,所有模式都生效)。暂存目录 UPLOAD_STAGING_ROOT(默认 /data/upload-staging)在 API 主机的本地磁盘上
DEV_USER_ID dev-user 仅 development/test

production 启动检查:以下任何一项不满足,服务都会拒绝启动:

  • 显式的登录方式;
  • HTTPS、__Host- cookie、前后端同源、精确的 CORS;
  • 真实的 Steam Web API key;
  • Steam 登录时的 STEAM_LOGIN_ALLOWLIST;
  • 非开发用的加密 key 和渲染 token;
  • 私有的 S3 兼容存储;
  • 带密码的 REDIS_URL。

凭证与数据:

  • production 凭证不能出现在 NEXT_PUBLIC_*、源码、日志、提交的 env 文件或浏览器响应里。
  • 真实 .dem、视频、回放缓存和本地 .env 必须留在 git 忽略的目录中。

部署

  • 生产(单台海外 VPS):docker-compose.prod.yml 叠加在 base 和 preview 之上,加入 Caddy(自动 HTTPS、同源路由);postgres 和 redis 只在 internal 的 backend 网络上,worker 容器有内存上限 WORKER_MEM_LIMIT(默认 3g,不用 swap)。scripts/deploy/ 提供初始化、部署/回滚、生产冒烟、每日备份、恢复演练和健康巡检。步骤见 vps_deploy_v1。
    • 部署与回滚:deploy.sh 在动任何运行中的服务之前先预检:目标提交不认识数据库里已应用的迁移时直接拒绝(跨迁移回滚必须先 restore.sh … --no-start 恢复迁移前的 dump,会丢掉那之后的改动),目标带新迁移时先自动跑 backup.sh --db-only,dump 名记进 deploy/.deploy-history(--skip-backup 仅在备份桶故障时作最后手段)。启动后依次等 /health、/health/worker(worker 容器要连续运行 95 秒以上)和生产冒烟。通过的部署把镜像打成 cs2coach-backend:<sha>(api 和 worker 共用)和 cs2coach-frontend:<sha>,保留最近 3 次;回滚到其中之一时直接用这两个镜像启动,不重新构建。
    • 告警:watch.sh(cs2coach-watch.timer,每 5 分钟)检查 /health、/health/worker、Docker、容器重启次数、磁盘占用和最新数据库备份的年龄,只在状态变化时向 ALERT_WEBHOOK_URL 发一行(ntfy、Telegram、Server酱),全部正常时 ping DEADMAN_PING_URL;backup.sh 成功后 ping BACKUP_PING_URL。告警内容只有检查名、状态和简短原因,不含账户、SteamID、文件名或 IP;三个地址都可留空。
    • 解析日志:worker 每个任务结束输出一行 JSON("event":"job_done",含任务类型、结果、errorCode、各步耗时、源文件字节数、建议数和解析子进程峰值内存 peakRssMiB),只有不透明的任务 id,没有文件名、账户或路径;parseS 和 peakRssMiB 也写进任务 metadata 的 parseStats。子进程异常退出时,它 stderr 的最后 2 KiB 只打进 worker 日志(Parse child exited with <code>; end of its stderr:),不会进入用户可见的错误信息。
  • 预览环境:docker compose -f docker-compose.yml -f docker-compose.preview.yml up --build,使用 production 模式和生产构建的前端;Redis 带密码,必须先设置 REDIS_PASSWORD(十六进制,如 openssl rand -hex 24)。必填变量、冒烟命令与回滚步骤见 cloud_preview_deploy_v1。
  • 冒烟测试:scripts/cloud_preview_smoke.py 会读取 /auth/me 返回的能力开关,跳过 production 隐藏的模拟数据和片段步骤。对 production 预览需要提供已登录的会话 cookie 和一份真实 .dem,样本和浏览器一样走分片上传会话;每次运行都会占用该账号滚动 24 小时上传配额(DEMO_UPLOAD_DAILY_LIMIT)中的一次。
  • 上线检查清单:deployment_readiness_v1 和 release_candidate_qa_v1。

在正式开放之前,上线计划 中还有这些没有完成:解析器的 CPU、磁盘与输出隔离(目前有超时、内存上限、环境变量白名单和独立低权限账户),数据库迁移,自动部署(目前是手动运行 scripts/deploy/deploy.sh,已有迁移预检和迁移前自动备份),监控(目前只有 watch.sh 的状态告警和 dead-man ping,没有指标),在真实部署上完成一次备份恢复演练,存储孤儿清理,以及 CI 上的真实 demo 门禁(真实 demo 清单只在本地 verify.sh 和 rc_check.sh 里比对)。

安全与隐私边界

  • 身份:production 使用 Steam OpenID 2.0,验证 SteamID64 后映射到不透明的账号 ID,会话保存在 Redis,通过 HttpOnly cookie 传递。会话按 1 小时的空闲窗口滑动续期(AUTH_SESSION_TTL_SECONDS):请求时发现剩余不到一半,就把 Redis 记录和 cookie 的有效期续回一个窗口,不使用时最多 1 小时后过期;但从登录起最长 24 小时(AUTH_SESSION_MAX_AGE_SECONDS),到时要重新登录;续期不改签发时间,所以删除账户时的"全部会话失效"照常生效。不在邀请名单里的账号不会被创建;从名单移除后,已有会话立即失效。
  • 数据隔离:所有用户接口都按账号隔离;私有媒体在每次读取时都会重新校验会话和归属,不存在公开的静态文件目录。
  • 开发工具:模拟数据、手动 MP4、模拟渲染和 API 文档页在 production 一律返回 404;前端根据 /auth/me 返回的能力开关隐藏对应入口。
  • 防滥用:
    • 上传配额在接收文件之前就会检查(分片上传在建会话时预检,complete 时权威检查);
    • 分片上传:每个账号同时只有一个未完成的上传,全站同时最多 UPLOAD_SESSION_GLOBAL_LIMIT 个,暂存盘余量不足时拒绝新上传;同时读取的分片数有全站上限,整文件 intake 同一时间只有一个;
    • 分片 PUT 只认这次上传的令牌(数据库只存其哈希,Caddy 日志删掉这个请求头),是唯一不检查会话 cookie 的写请求,但仍要求 Origin 是站点源;
    • 并发的上传和重试通过加锁排队,不能绕过同时处理数的上限;
    • 解析在子进程里运行,默认超时 20 分钟、内存上限 4 GB;production 的 worker 容器另有 WORKER_MEM_LIMIT 的上限。子进程的环境是白名单(只有 PATH、PYTHONPATH、OPENBLAS_NUM_THREADS、LANG、TMPDIR、HOME),拿不到数据库、存储、Redis、Steam 和渲染 token 等任何密钥;worker 以 root 运行时,子进程切到镜像里的低权限账户 parser(PARSE_CHILD_USER),工作区归它所有、源文件对它可读,读不到 worker 进程的环境。
  • 生产网络:postgres 和 redis 只在 internal 的 backend 网络上,只有 api 和 worker 连得到,面向公网的 caddy 和 frontend 连不到;Redis 要密码(REDIS_PASSWORD),因为登录会话以未签名的形式存在 Redis 里,能写 Redis 就能伪造任何人的会话。
  • Steam 比赛授权:与登录分离。Game Authentication Code 和分享码在服务端以 AES-256-GCM 加密保存,不会回传给浏览器。
  • 开发模式:可以用 X-Dev-User-Id 模拟不同用户,production 忽略这个请求头(不会用它选中任何账号),只认登录会话。

隐私:下面的事实与公开的 /privacy 页面(frontend/app/privacy/page.tsx)一致。改动数据的收集、保存或删除方式时,两边要一起改,同时更新 data_deletion_v1。

  • 范围:邀请制的 CS2 比赛复盘网站,只做 2D 回放和规则建议。不使用 AI 大模型,没有广告,也没有统计分析。
  • 登录:
    • Steam 登录只返回 SteamID64,网站拿不到 Steam 密码。
    • 服务器用 Steam Web API 读取公开昵称和头像地址并保存,每次登录更新。
    • 未受邀的账号不会被保存,但登录时仍会向 Steam 查询一次公开资料。
  • 上传的比赛:
    • 原始 .dem 按原样保存(用于重新解析),另外保存解析出的回放数据和建议。回放数据包括每名玩家的位置、血量、金钱、武器、携带的道具和按键记录(移动、静步、蹲、跳和鼠标左右键)、开枪记录(每一枪的时间、移动速度和武器),以及每颗道具的轨迹、落点和投掷者出手那一刻的站位与视角,全部来自 .dem 本身;较早上传的比赛会在后台用已保存的 .dem 重新解析一次来补上。规则更新后,网站会在后台用已保存的回放数据重新计算建议,不收集新的数据;有的建议可能因此不再显示,用户对它的评价仍随比赛保存,直到删除这场比赛或账户。
    • .dem 里包含同场所有玩家的 SteamID64、游戏内昵称、位置和击杀记录,只对上传者本人可见。其他玩家如希望移除,可以联系站长。
    • 另外保存的只有:用户对建议的评价(有帮助 / 无关 / 判断不足),以及改过的比赛名。
    • 上传还没完成时,已收到的分片连同规范化后的文件名和大小暂存在网站服务器的本地磁盘上(命名卷 upload-staging),上传完成时转存为比赛文件;放弃、从开始上传起 24 小时后仍未完成,或删除账户时删除。
  • Steam 比赛记录(可选):只有用户主动关联时才保存,内容是加密后的游戏验证码和比赛分享码。每次点"同步"才向 Valve 查询;断开关联即删除。
  • Cookie 和浏览器存储:
    • 只有两个必需的 cookie:__Host-cs2_session 保持登录,不使用时最多 1 小时后过期,使用中自动续期,但从登录起最长 24 小时;__Host-cs2_steam_state 只在登录过程中存在 5 分钟。没有统计或广告 cookie。
    • localStorage 只存两项:"你在比赛里选的玩家"偏好(含本人 SteamID64),以及未完成上传的续传记录(文件名、大小、文件修改时间和上传编号,上传完成或放弃后清除)。删除账户时两项都在当前浏览器清除。
  • 日志:服务器访问日志记录 IP 地址、浏览器标识和访问的页面地址(含搜索词),用于排查故障和防滥用。日志按大小轮转(每个服务最多约 50 MB),不按时间删除。
  • 存放位置和第三方:
    • 网站跑在一台海外 VPS 上,地区由 NEXT_PUBLIC_DATA_REGION 写在隐私页。比赛文件和备份在 Cloudflare R2 私有存储桶。
    • 经手数据的第三方只有:VPS 服务商、Cloudflare(存储)、Valve/Steam(登录、公开资料、用户主动开启的比赛记录同步)、Let's Encrypt(只签发 HTTPS 证书)。不出售,也不共享给其他人。
  • 保存与删除:
    • 账户和比赛一直保存,直到用户删除;登录会话不使用时最多 1 小时后过期,使用中自动续期,从登录起最长保留 24 小时;未完成的上传最多保留 24 小时。
    • 可以永久删除单场比赛(DELETE /demos/{id}),也可以在 /account 删除账户和全部数据(DELETE /auth/account,仅 production),所有设备上的登录同时失效。
    • 删除立即作用于数据库和存储,存储清理失败会自动重试。API 为加快打开速度在内存里暂存的回放响应只在内存里,删除后读不到,重启后也不保留。每日备份里的副本最多再保留 30 天;从备份恢复后,站长会重做那之后的删除。
    • 用户自己无法删除时(同场其他玩家的移除请求、已被移出邀请名单的用户),站长用运维命令 python -m app.cli.delete_data 代为删除,走同一套删除流程(见 data_deletion_v1)。
    • 邀请名单(受邀者的 SteamID64)保存在服务器配置里,删除账户不会改动它;隐私页和"账户与数据"页都写明了。
    • 删除比赛不会恢复当天的上传次数。
  • 与 Valve 无关联:每个页面的页脚都写明"本站与 Valve Corporation 无关联。Counter-Strike、CS2 和 Steam 是 Valve 的商标。"隐私页的联系方式由 NEXT_PUBLIC_PRIVACY_CONTACT 配置。

细节见 steam_auth_accounts_v1、production_auth_owner_private_media_v1、object_storage_safe_artifact_intake_v1 和 data_deletion_v1。

已知限制

  • 建议是否真的有用还没有经过真人验证。 规则阈值来自 Dust II 上的手工调整;评价功能已上线,要靠内测数据来校正。
  • 只有六张地图有雷达底图:Dust II、Mirage、Inferno、Ancient、Nuke、Anubis 的雷达图都由本项目按坐标配置里的同一个变换从导航网格渲染,玩家位置与底图天然对齐,六张图都标为已校准(ff6759f 起);其他地图没有底图,按单场比赛的站位范围推算坐标。
  • 不能自动下载比赛:Valve 没有公开个人比赛的下载接口,仓库也没有获得许可的来源,所以只能手动上传 .dem。相关导入代码已经写好并有测试,但在这个版本里走不到。
  • 解析是单 worker 串行执行:崩溃恢复是自动的,但有分钟级延迟。租约为 60 秒;如果 Redis 数据全部丢失,排队中的任务约 5 分钟(PARSE_REDISPATCH_AFTER_SECONDS)后重新投递,正在解析的任务要等 PARSE_RECLAIM_AFTER_SECONDS(默认 30 分钟)。
  • CI 不解析真实 .dem:真实文件只在本地校验:verify.sh 在仓库根目录有 .dem 时比对真实 demo 清单,rc_check.sh 带样本时先比对清单再走上传冒烟。
  • 并行分片不一定更快:站点走 HTTP/2 / HTTP/3,浏览器把并行的分片复用在同一条连接上,提速可能远小于并行数。上线后用 API 日志里每个会话一行的 Upload session completed: bytes=… seconds=… 测速,再决定分片大小和并行数。
  • 健康检查很粗:/health 在数据库、Redis 或 worker 配置任一项检查失败时返回 HTTP 503 {"status":"degraded"}(正常为 200 {"status":"ok"}),但不说明是哪一项;production 下要看 docker compose logs api 才能定位。解析 worker 不在 /health 里(故意的:caddy 和 frontend 依赖 api 的健康检查启动,worker 挂掉不能把整站拖下),由同样粗粒度的 /health/worker 报告:心跳 90 秒内为 200,否则 503。
  • 数据覆盖有限:
    • 解析帧是采样数据;炸弹和道具事件尽量提取,不保证完整;
    • 没有视线信息(帧里没有玩家朝向;只在道具出手那一刻记录了投掷者的视角)。
    • 道具投掷分析的站位指令只还原出手点和视角:边走边扔或跳起扔的道具,照着站好原地扔也会落偏,面板会提示。
    • 实时按键显示依赖 demo 里的 usercmd 数据,只在 BLAST.tv 的 GOTV demo 上验证过;匹配、FACEIT、第一人称 POV 等来源可能没有这类数据,这时不显示按键面板。
  • 第一人称片段:只在一台本地 Windows 渲染机上验收过;排队超过 RENDER_CLIP_QUEUE_TIMEOUT_SECONDS(默认 30 分钟)的任务会被标记失败,可以重试。
  • 单场失败比赛可以无限次重新处理;Redis 客户端没有设置超时。
  • 解析器隔离还不完整:子进程有超时、内存上限、环境变量白名单和独立的低权限账户,但还没有 CPU、磁盘和输出的限制;api 和 worker 容器本身仍以 root 运行(改成非 root 要先迁移命名卷的属主,留作后续)。
  • Steam 登录尚未在真实部署中验证:真实 HTTPS 回调、publisher key 资格、Game Authentication Code 行为和限流,都还需要在一次真实部署上冒烟。
  • 删除追不回备份:已删除的数据在备份里最多再保留 30 天。从备份恢复数据库会把备份之后删除的数据带回来,要按 VPS 部署 第 7 步用 python -m app.cli.delete_data 重做这些删除。
  • 删除账户后可以重新注册:仍在邀请名单里的 Steam 账号重新登录,会得到一个全新的空账户,上传次数也从零算起;要禁止此人登录,只能把他移出 STEAM_LOGIN_ALLOWLIST。移出名单不会删除他的数据,需要时先用运维命令代为删除。

路线图

当前方向是网站优先:先以邀请制内测验证建议是否真的有用,Windows 桌面安装包暂缓(见 desktop_distribution_v1)。按顺序:

  1. 完成上线计划剩下的阶段:解析器资源隔离(已有环境变量白名单和低权限账户,还缺 CPU/磁盘/输出限制和非 root 容器),数据库迁移,自动部署与回滚(已有迁移预检、迁移前自动备份和按提交打标签的镜像),监控告警(已有 watch.sh 状态告警,还没有指标),备份恢复;配置 HTTPS 入口,并在真实 HTTPS 部署上跑一次 Steam 登录冒烟。
  2. 支持真正删除比赛和账号,补上隐私条款(已完成:永久删除比赛和账户、/privacy 隐私说明页,见 data_deletion_v1);去掉第三方雷达图(已完成:原来的 MIT/GPL 第三方雷达图已换成本项目从 CS2 导航网格渲染的图,见 scripts/maps);这些图由 Valve 的游戏数据派生,能否公开分发还没有单独确认,扩大开放前要确认。
  3. 邀请少量玩家内测,用评价数据调整规则阈值、去重和排序;支持更多地图。
  4. 视内测反馈,再决定是否扩大开放、是否恢复第一人称片段。

文档索引

参考:

上线与运维:

进展与决策:

验收记录:

阶段目标(历史记录):docs/*_goal.md,包括存储边界、解析质量与可靠性、规则分析 V3、回合复盘、渲染片段、可观测性等。当前事实以代码和近期验收记录为准。

致谢

雷达图由本项目用 scripts/maps/build_radars.py 从运营者自己的 CS2 安装里的导航网格渲染(包点取自地图自带的 func_bomb_target 触发器,包点里的站立高度取自地图的碰撞数据),不含第三方或 Valve 的雷达美术素材,但图形由 Valve 的游戏数据派生,公开分发的条款尚未单独确认;导航网格的读取参考了 awpy(MIT)。Nuke 的 overview 参数来自 CS Demo Manager,Inferno 与 Anubis 的 overview 参数取自游戏 game/csgo/pak01_dir.vpk 里的 resource/overviews。详见 ATTRIBUTION.md。

复盘页实时按键面板的视觉设计改编自 bugkingZHT/cs2-sandbox 的 KeyboardOverlay.vue,道具投掷分析面板的布局和扔法判定规则改编自同一仓库的 GrenadeAnalyzeOverlay.vue 和 useGrenadeAnalyzer.ts(MIT,Copyright (c) 2026 huN7er),都用 React 重新实现;许可原文见 THIRD_PARTY_NOTICES.md。

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages