Reorganize repository into pc web shared monorepo

Separate the local recognition, web publishing, and shared data paths while preserving direct script execution and existing site content.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-29 14:29:08 +08:00
co-authored by Cursor
parent 96a9312194
commit 9c5aa5b610
280 changed files with 1451 additions and 450 deletions
+2 -2
View File
@@ -36,7 +36,7 @@ jobs:
PYTHON=python3 PYTHON=python3
command -v python3 >/dev/null || PYTHON=python command -v python3 >/dev/null || PYTHON=python
if [ -n "${{ github.event.inputs.day }}" ]; then if [ -n "${{ github.event.inputs.day }}" ]; then
"$PYTHON" notify_site_traffic.py --day "${{ github.event.inputs.day }}" "$PYTHON" web/notify_site_traffic.py --day "${{ github.event.inputs.day }}"
else else
"$PYTHON" notify_site_traffic.py "$PYTHON" web/notify_site_traffic.py
fi fi
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
PYTHON=python3 PYTHON=python3
command -v python3 >/dev/null || PYTHON=python command -v python3 >/dev/null || PYTHON=python
command -v node >/dev/null || { echo "node/npx required for wrangler deploy"; exit 1; } command -v node >/dev/null || { echo "node/npx required for wrangler deploy"; exit 1; }
"$PYTHON" -m pip install -q -r requirements.txt -r requirements-web.txt "$PYTHON" -m pip install -q -r web/requirements.txt
- name: Refresh daily tier - name: Refresh daily tier
env: env:
@@ -38,4 +38,4 @@ jobs:
set -e set -e
PYTHON=python3 PYTHON=python3
command -v python3 >/dev/null || PYTHON=python command -v python3 >/dev/null || PYTHON=python
"$PYTHON" refresh_web.py --tier daily "$PYTHON" web/refresh_web.py --tier daily
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
PYTHON=python3 PYTHON=python3
command -v python3 >/dev/null || PYTHON=python command -v python3 >/dev/null || PYTHON=python
command -v node >/dev/null || { echo "node/npx required for wrangler deploy"; exit 1; } command -v node >/dev/null || { echo "node/npx required for wrangler deploy"; exit 1; }
"$PYTHON" -m pip install -q -r requirements.txt -r requirements-web.txt "$PYTHON" -m pip install -q -r web/requirements.txt
- name: Refresh patch tier - name: Refresh patch tier
env: env:
@@ -36,4 +36,4 @@ jobs:
set -e set -e
PYTHON=python3 PYTHON=python3
command -v python3 >/dev/null || PYTHON=python command -v python3 >/dev/null || PYTHON=python
"$PYTHON" refresh_web.py --tier patch "$PYTHON" web/refresh_web.py --tier patch
+2 -2
View File
@@ -23,7 +23,7 @@ jobs:
PYTHON=python3 PYTHON=python3
command -v python3 >/dev/null || PYTHON=python command -v python3 >/dev/null || PYTHON=python
command -v node >/dev/null || { echo "node/npx required for wrangler deploy"; exit 1; } command -v node >/dev/null || { echo "node/npx required for wrangler deploy"; exit 1; }
"$PYTHON" -m pip install -q -r requirements.txt -r requirements-web.txt "$PYTHON" -m pip install -q -r web/requirements.txt
- name: Refresh weekly tier - name: Refresh weekly tier
env: env:
@@ -36,4 +36,4 @@ jobs:
set -e set -e
PYTHON=python3 PYTHON=python3
command -v python3 >/dev/null || PYTHON=python command -v python3 >/dev/null || PYTHON=python
"$PYTHON" refresh_web.py --tier weekly "$PYTHON" web/refresh_web.py --tier weekly
+27 -23
View File
@@ -13,10 +13,10 @@ Thumbs.db
oss_upload.pid oss_upload.pid
# Runtime / debug artifacts (regenerable) # Runtime / debug artifacts (regenerable)
preview/ pc/preview/
results/ pc/results/
failures/ pc/failures/
samples/raw/ pc/samples/raw/
_tools/ _tools/
_tmp_* _tmp_*
_cmp_* _cmp_*
@@ -24,27 +24,31 @@ plans/
*_log.txt *_log.txt
# Regenerable data caches / reports (re-fetch or re-run scripts) # Regenerable data caches / reports (re-fetch or re-run scripts)
data/matchups.json shared/data/matchups.json
data/synergies.json shared/data/synergies.json
data/stratz_matchups.json shared/data/stratz_matchups.json
data/stratz_hero_meta.json shared/data/relations_audit.json
data/stratz_matchup_tops.json shared/data/relations_import_report.json
data/item_counter_stats.json shared/data/relations_patch_*
data/hero_stats.json web/data/stratz_hero_meta.json
data/hero_matches.json web/data/stratz_matchup_tops.json
data/pro_matches.json web/data/item_counter_stats.json
data/pro_builds.json web/data/hero_stats.json
data/relations_audit.json web/data/hero_matches.json
data/relations_import_report.json web/data/pro_matches.json
data/relations_patch_* web/data/pro_builds.json
web/data/_jugg_times_sample.json
# Large regenerable CDN media (fetch scripts) # Large regenerable CDN media (fetch scripts)
assets/ability_videos/ web/assets/ability_videos/
assets/ability_icons/ web/assets/ability_icons/
assets/hero_portraits/ web/assets/hero_portraits/
assets/item_icons/ web/assets/item_icons/
# Curated streamer highlight clips (large; sync via OSS, not git) # Curated streamer highlight clips (large; sync via OSS, not git)
assets/streamer_videos/ web/assets/streamer_videos/
# Static site export (export_relations_site.py) # Static site export (export_relations_site.py)
dist/ web/dist/
# Local probes / one-off diagnostics
_probe_item_fears_api.py
+168 -137
View File
@@ -8,183 +8,214 @@ Windows 上的 Dota 2 **天梯选将识别**工具:用 GSI 感知选将阶段
- 中文名:上分帝 - 中文名:上分帝
- 英文名 / 仓库目录:Climperor / `climperor` - 英文名 / 仓库目录:Climperor / `climperor`
- 模板来源:**仅** Steam CDN 官方头像(`templates/cdn/` - 模板来源:**仅** Steam CDN 官方头像(`pc/templates/cdn/`
- **已放弃** real 实拍模板库补充;不要再引入 `templates/real/` 或入库流水线 - **已放弃** real 实拍模板库补充;不要再引入 `templates/real/` 或入库流水线
**上分帝 Web**`web/relations/`)是独立子项目:英雄克制/搭档、排行、主播、走势、机制、物品、版本等内容站点,与局内选将识别解耦。文档与注释统称「上分帝 Web」,勿再称「关系预览」;历史路径/脚本名(如 `serve_relations.py``dist/relations/`)暂保留。 **上分帝 Web**`web/`)是独立子项目:英雄克制/搭档、排行、主播、走势、机制、物品、版本等内容站点,与局内选将识别解耦。文档与注释统称「上分帝 Web」,勿再称「关系预览」;历史脚本名(如 `serve_relations.py``dist/relations/`)暂保留。
## Monorepo 布局
```
climperor/
├── pc/ 局内选将识别(GSI + 截屏 + OpenCV + overlay
├── web/ 上分帝 Web(前端 frontend/ + 数据流水线 + 部署运维)
└── shared/ 共享包(英雄表/关系/标签/HTTP/路径常量),pc 与 web 都依赖它
```
**跨目录 import 约定**`pc/``web/``shared/` 下的每个脚本顶部都有相同两行引导(`parents[1]` 恒为仓库根),之后即可 `from shared import ...` / `from shared.paths import ...`;运行方式不变,一律在仓库根执行 `python web/xxx.py``python pc/xxx.py``python shared/xxx.py`。**shared 不反向依赖 pc/ 或 web/**`shared/grid.py` 的 cv2/numpy 为懒加载,Web 侧与 CI 无需 opencv。路径常量单一来源是 `shared/paths.py``pc/common.py` re-export PC 所需常量,PC 侧 `from common import ...` 调用不变。
## 目录与模块 ## 目录与模块
### pc/(局内选将识别)
| 路径 | 职责 | | 路径 | 职责 |
|------|------| |------|------|
| `common.py` | 配置 IO、槽位几何、裁切、NCC 匹配、天梯遮罩、CDN 模板加载 | | `pc/common.py` | 配置 IO、槽位几何、裁切、NCC 匹配、天梯遮罩、CDN 模板加载re-export `shared.paths` 常量 |
| `recognize.py` | 单帧识别;`recognize_image()` 供会话复用 | | `pc/recognize.py` | 单帧识别;`recognize_image()` 供会话复用 |
| `draft_session.py` | 整局选将跟踪、改判、皮肤规避策略 | | `pc/draft_session.py` | 整局选将跟踪、改判、皮肤规避策略 |
| `gsi_watch.py` / `gsi_setup.py` | GSI 监听与 cfg 安装 | | `pc/gsi_watch.py` / `pc/gsi_setup.py` | GSI 监听与 cfg 安装 |
| `http_utils.py` | 所有 fetch 脚本共用的 HTTP 请求、图标下载、Valve datafeed 加载(统一 UA / 超时;429/5xx 指数退避重试 | | `pc/fetch_cdn_templates.py` | 拉取 CDN 头像 + 生成 `shared/data/heroes.json`(含基础属性/血蓝;保留已有 `aliases` |
| `fetch_patches.py` | 拉取近一年(默认 365 天,`--days`/`--since`)版本列表 + 逐版本 `patchnotes` 详情 → `data/patches.json`;构建 id→名称/图标的 `lookup` 并下载引用到的物品/技能图标(`--no-icons` 跳过;`--force` 重抓全量;`--check` 只比对列表与本地 detailsstdout JSON | | `pc/recommend.py` | 定位局分路过滤;克/搭/补全网格标记;调用 `draft_archetypes` 做推进/全球流/缺口画像与短文案 |
| `requirements-web.txt` | Web 刷新额外依赖(`oss2`);Gitea Actions 与 `requirements.txt` 一并 `pip install` | | `pc/draft_archetypes.py` | 规则阵容画像(推进/全球流/敌我缺口)+ `analysis` / `reasons` 文案(不接 AI |
| `fetch_cdn_templates.py` | 拉取 CDN 头像 + 生成 `data/heroes.json`(含基础属性/血蓝;保留已有 `aliases` | | `pc/autocalibrate.py` / `pc/calibrate.py` | ROI 自动 / 手动标定 |
| `relations.py` | 定性克制/搭档边读写与名称解析 | | `pc/capture.py` | 屏幕捕获 |
| `import_relations_xlsx.py` | 从选将笔记 xlsx 种子导入 `data/relations.json` | | `pc/overlay.py` | 顶栏角色标签 + 网格「克/搭/补」方标 + 阵容分析横条(点击穿透) |
| `fetch_stratz.py` | 拉取 STRATZ 对位优势 / 搭档协同 → `data/stratz_matchups.json` / `data/synergies.json`(需 token`--mode matchups\|synergies`;供 `audit_relations.py` | | `pc/evaluate.py` | `pc/samples/labels.json` 批量评测 |
| `fetch_stratz_meta.py` | 拉取 STRATZ 各勋章段位 `winWeek`(近 N 周 pick/win + 同段位最近 1 周分路,`positionIds`+ 对位 Top → `data/stratz_hero_meta.json` / `data/stratz_matchup_tops.json`(需 token**仅上分帝 Web**;勿进 recommend / relations | | `pc/roles.py` / `pc/modes.py` | 位置字、模式字(禁用网格在 `shared/grid.py` |
| `fetch_matchups.py` | 拉取 OpenDota 对位优势 → `data/matchups.json`(供 `audit_relations.py` 交叉审计 | | `pc/config.json` | 相对坐标、阈值、GSI、recommend 参数(`relations_path` 指向 `shared/data/relations.json` |
| `audit_relations.py` | OpenDota+STRATZ 交叉审计 `relations.json`,列出缺失英雄 | | `pc/templates/cdn/` | 全英雄 CDN 模板(需提交或由脚本生成) |
| `recommend.py` | 定位局分路过滤;克/搭/补全网格标记;调用 `draft_archetypes` 做推进/全球流/缺口画像与短文案 | | `pc/templates/roles/` / `pc/templates/modes/` | 位置 / 模式辅助模板 |
| `draft_archetypes.py` | 规则阵容画像(推进/全球流/敌我缺口)+ `analysis` / `reasons` 文案(不接 AI | | `pc/assets/role_icons/` | Valve 选人筛选角色图标(`filter_*`,品红 chroma 去黑边 |
| `serve_relations.py` | 上分帝 Web 本地开发服务(`web/relations/`;改 `data/*.json` 后刷新;`/streamer-video/` 提供主播高光 mp4,支持 HTTP Range | | `pc/requirements.txt` | PC 侧依赖(opencv/numpy/mss/openpyxl |
| `export_relations_site.py` | 导出上分帝 Web 为纯静态站点 → `dist/relations/`data.json 快照 + 前端 + 图片;`SITE_VERSION` 常量与 `web/relations/config.js` 同步;`--ability-video-base` / `--static-asset-base``config.js` 指向 OSS;设 `--static-asset-base` 时不拷贝图标进 dist`--with-videos` 可选本地拷贝技能/主播视频,生产部署勿用) |
| `deploy_relations.py` | 一键部署上分帝 Web 静态站点到 Cloudflare Pages(导出 + 资产预检 + `wrangler` 直传 + 绑域名;默认 OSS base 指向 `climperor` 桶的视频与静态图;凭据经 keyzoo 注入或 env | ### shared/(共享包)
| `refresh_web.py` | 上分帝 Web 数据分层刷新编排(`daily`/`weekly`/`patch`/`all`);摘要比对后有变更才 OSS upload + `deploy_relations.py``--dry-run` / `--skip-deploy` / `--skip-oss` / `--force-deploy``daily`stats/排行/比赛/pro/主播 + patch check`weekly`STRATZ meta + 物品;`patch`:仅 check`has_new` 时详情 + abilities/商店/meta/fears |
| `notify_site_traffic.py` | 上分帝 Web 日报 → 飞书 webhook(访问估数 + Gitea `web-daily`/`weekly`/`patch` 运行结论 + Pages 当日生产部署;`--dry-run` 只打印卡片) | | 路径 | 职责 |
| `.gitea/workflows/site-traffic-notify.yml` | 每日 09:00 CST 跑 `notify_site_traffic.py`Secrets`CLOUDFLARE_EMAIL` / `CLOUDFLARE_API_KEY` / `FEISHU_WEBHOOK_URL`;内置 `GITEA_TOKEN` 用于查询同仓 Actions | |------|------|
| `.gitea/workflows/web-daily.yml` | 每日 06:00 CST`refresh_web.py --tier daily`self-hosted;需 CF + OSS SecretsSTRATZ 可选) | | `shared/paths.py` | 全部路径常量单一来源(`ROOT`/`SHARED_DATA`/`HEROES_JSON`/`TEMPLATES_CDN`/`DATA`/`WEB_FRONTEND`/各 Web 资产目录);纯常量、零第三方依赖 |
| `.gitea/workflows/web-weekly.yml` | 周二 07:00 CST`refresh_web.py --tier weekly`(需 `STRATZ_API_TOKEN` + CF + OSS | | `shared/grid.py` | 英雄表 `hero_table()`、选人网格布局与禁用读取(cv2/numpy 懒加载,Web/CI 侧只取表不触发 |
| `.gitea/workflows/web-patch.yml` | 每 6 小时:`refresh_web.py --tier patch``fetch_patches.py --check`,有新版本才拉详情与连带;需 CF + OSS) | | `shared/relations.py` | 定性克制/搭档边读写与名称解析;默认路径 `shared/data/relations.json` |
| `_cf_status.py` | 只读查询 Cloudflare Pages 项目 / 部署 / 自定义域名状态(凭据经 keyzoo 注入 | | `shared/hero_tags.py` | 中文定位 tags(核心/辅助/…/幻象 |
| `_oss_ability_videos.py` | 阿里云 OSS 桶 `climperor` 建桶 / CORS / 同步 `assets/ability_videos/``ability-video/`(凭据经 keyzoo `digitevents/voson-RAM` 注入);`_oss_static_assets.py` 同步图标等静态图;`_oss_fix_public.py` / `_oss_launch_upload.py` 为配套辅助 | | `shared/http_utils.py` | 所有 fetch 脚本共用的 HTTP 请求、图标下载、Valve datafeed 加载(统一 UA / 超时;429/5xx 指数退避重试) |
| `fetch_hero_portraits.py` | 拉取官网横版头像 → `assets/hero_portraits/`(上分帝 Web | | `shared/import_relations_xlsx.py` | 从选将笔记 xlsx 种子导入 `shared/data/relations.json` |
| `fetch_hero_items.py` | 拉取 OpenDota 热门装备 → `data/hero_items.json` + `assets/item_icons/` | | `shared/fetch_stratz.py` | 拉取 STRATZ 对位优势 / 搭档协同 → `shared/data/stratz_matchups.json` / `shared/data/synergies.json`(需 token`--mode matchups\|synergies`;供 `audit_relations.py` |
| `fetch_hero_stats.py` | 拉取 OpenDota 各段位场次/胜场 → `data/hero_stats.json`(**仅上分帝 Web**;勿写入 relations/heroes,勿进 recommend | | `shared/fetch_matchups.py` | 拉取 OpenDota 对位优势 → `shared/data/matchups.json`(供 `audit_relations.py` 交叉审计 |
| `fetch_hero_matches.py` | 拉取同英雄近期比赛 + 终局出装/加点 → `data/hero_matches.json``--source league\|public\|both`;合并后保留最近 N 场胜局,默认 10;天梯需传奇及以上;公开列表过滤 bot/Turbo、仅 ranked lobby`--public-region china` 优先国服;`--workers` 详情并发 + 凑满即停;可选 `OPENDOTA_API_KEY`;增量补缺失/不足 N 场;`--enrich-item-times` 补购买时间;**仅上分帝 Web**;勿进 recommend | | `shared/audit_relations.py` | OpenDota+STRATZ 交叉审计 `relations.json`,列出缺失英雄 |
| `fetch_leaderboards.py` | 拉取 Valve Immortal 四区榜 Top100 → `data/leaderboards.json`(**仅上分帝 Web「排行」**;无 MMR/account_id;勿进 recommend | | `shared/data/heroes.json` | 英雄表(id/key/attr/roles/aliases/abbr/tags + 基础属性/初始血蓝等;由 `pc/fetch_cdn_templates.py` 生成 |
| `fetch_streamers.py` | 从抖音主页补全 `data/streamers.json` 的昵称/签名/关注粉丝获赞/头像(手工名单 + 直播间/主页 URL;支持 `v.douyin.com` 短链;失败保留旧值;**不**探测开播/`is_live`;**仅上分帝 Web「主播」**;勿进 recommend;进 `refresh_web` daily | | `shared/data/relations.json` | 定性克制边 + 搭档边(可提交;手改 JSON,Web 站点只读 |
| `fetch_item_shop.py` | 官网商店 11 列目录(dota2.com.cn/itemscategory+ 合成图 → `data/item_shop.json` + 图标 |
| `fetch_items_meta.py` | Valve/OpenDota 装备描述 → 机制标签 → `data/items_meta.json``%token%` 用 special_values 填数;查询类 tags 共用 `mechanic_tags.py` | ### web/(上分帝 Web
| `mechanic_tags.py` | 上分帝 Web「机制」页共用标签(弱/强驱散、缠绕/缴械/沉默/锁闭/眩晕/妖术/破坏、睡眠/恐惧/嘲讽/致盲/束缚、隐身/虚无/吹风)+ 中文 labels;语义为「施加该效果」 |
| `loc_format.py` | Valve 文案共用:去 HTML、填充 `%token%` / `{s:token}` | | 路径 | 职责 |
| `fetch_hero_abilities.py` | Valve herodata 技能/魔晶/神杖/天赋 + 驱散汇总 → `data/hero_abilities.json`;每条 ability 另有「施加」类 `tags`(与 `dispellable` 区分;`--tags-only` 可只重算);`has_scepter`/`has_shard` 只信 Valve 显式 flagValve 移除升级后残留的 `scepter_loc`/`shard_loc` 文案会被清空;逐级相同的 `cast_points`/`channel_times` 合并为单值(施法前摇/吟唱时间);可选 `--icons` / `--icons-only` 缓存 Steam CDN 技能图标 → `assets/ability_icons/`(先天用共用 `innate.png`,不拉 CDN | |------|------|
| `fetch_ability_videos.py` | 官网技能演示 webm/mp4Steam CDN;限速)→ `assets/ability_videos/`A 杖/魔晶赋予技能(`granted_by_scepter`/`granted_by_shard`CDN 名用 `<hero>_aghanims_scepter`/`<hero>_aghanims_shard`,本地文件名仍用 ability key | | `web/fetch_patches.py` | 拉取近一年(默认 365 天,`--days`/`--since`)版本列表 + 逐版本 `patchnotes` 详情 → `web/data/patches.json`;构建 id→名称/图标的 `lookup` 并下载引用到的物品/技能图标(`--no-icons` 跳过;`--force` 重抓全量;`--check` 只比对列表与本地 detailsstdout JSON |
| `fetch_item_counter_stats.py` | OpenDota Explorer 近场次聚合:对阵英雄时敌方终局装备购买率/胜率,与同窗口该装备全局队伍基线作差 → `data/item_counter_stats.json`(可再生成缓存;观测证据,不作因果结论;weekly 软失败) | | `web/requirements.txt` | Web 刷新依赖(`oss2`);Gitea Actions 仅装它 |
| `item_fears.py` | 规则推导「英雄怕的装备」→ `data/hero_item_fears.json`;可选读取 `item_counter_stats.json`,对全部英雄的已有机制候选小幅调序;统计发现的新组合须经机制复核后写入 overrides | | `web/fetch_stratz_meta.py` | 拉取 STRATZ 各勋章段位 `winWeek`(近 N 周 pick/win + 同段位最近 1 周分路,`positionIds`+ 对位 Top → `web/data/stratz_hero_meta.json` / `web/data/stratz_matchup_tops.json`(需 token**仅上分帝 Web**;勿进 recommend / relations |
| `autocalibrate.py` / `calibrate.py` | ROI 自动 / 手动标定 | | `web/serve_relations.py` | 上分帝 Web 本地开发服务(`web/frontend/`;改 `web/data/*.json` 后刷新;`/streamer-video/` 提供主播高光 mp4,支持 HTTP Range |
| `capture.py` | 屏幕捕获 | | `web/export_relations_site.py` | 导出上分帝 Web 为纯静态站点 → `web/dist/relations/`data.json 快照 + 前端 + 图片;`SITE_VERSION` 常量与 `web/frontend/config.js` 同步;`--ability-video-base` / `--static-asset-base``config.js` 指向 OSS;设 `--static-asset-base` 时不拷贝图标进 dist`--with-videos` 可选本地拷贝技能/主播视频,生产部署勿用) |
| `overlay.py` | 顶栏角色标签 + 网格「克/搭/补」方标 + 阵容分析横条(点击穿透 | | `web/deploy_relations.py` | 一键部署上分帝 Web 静态站点到 Cloudflare Pages(导出 + 资产预检 + `wrangler` 直传 + 绑域名;默认 OSS base 指向 `climperor` 桶的视频与静态图;凭据经 keyzoo 注入或 env |
| `evaluate.py` | 按 `samples/labels.json` 批量评测 | | `web/refresh_web.py` | 上分帝 Web 数据分层刷新编排(`daily`/`weekly`/`patch`/`all`);摘要比对后有变更才 OSS upload + `deploy_relations.py``--dry-run` / `--skip-deploy` / `--skip-oss` / `--force-deploy``daily`stats/排行/比赛/pro/主播/主播开播探测 + patch check`weekly`STRATZ meta + 物品;`patch`:仅 check`has_new` 时详情 + abilities/商店/meta/fears |
| `roles.py` / `grid.py` / `modes.py` | 位置字、禁用网格、模式字 | | `web/notify_site_traffic.py` | 上分帝 Web 日报 → 飞书 webhook(访问估数 + Gitea `web-daily`/`weekly`/`patch` 运行结论 + Pages 当日生产部署;`--dry-run` 只打印卡片) |
| `config.json` | 相对坐标、阈值、GSI、recommend 参数 | | `.gitea/workflows/site-traffic-notify.yml` | 每日 09:00 CST 跑 `web/notify_site_traffic.py`Secrets`CLOUDFLARE_EMAIL` / `CLOUDFLARE_API_KEY` / `FEISHU_WEBHOOK_URL`;内置 `GITEA_TOKEN` 用于查询同仓 Actions |
| `data/heroes.json` | 英雄表(id/key/attr/roles/aliases/abbr/tags + 基础属性/初始血蓝等;由 `fetch_cdn_templates.py` 生成 | | `.gitea/workflows/web-daily.yml` | 每日 06:00 CST`web/refresh_web.py --tier daily`self-hosted;需 CF + OSS SecretsSTRATZ 可选 |
| `data/patches.json` | 近一年版本列表 + 逐版本详情(`patches`/`lookup`/`details`;由 `fetch_patches.py` 生成,Web 版本页只读 | | `.gitea/workflows/web-weekly.yml` | 周二 07:00 CST`web/refresh_web.py --tier weekly`(需 `STRATZ_API_TOKEN` + CF + OSS |
| `data/relations.json` | 定性克制边 + 搭档边(可提交;手改 JSON,Web 站点只读 | | `.gitea/workflows/web-patch.yml` | 每 6 小时:`web/refresh_web.py --tier patch``fetch_patches.py --check`,有新版本才拉详情与连带;需 CF + OSS |
| `data/hero_grid_order.json` | Web 站点四列网格顺序 | | `web/_cf_status.py` | 只读查询 Cloudflare Pages 项目 / 部署 / 自定义域名状态(凭据经 keyzoo 注入) |
| `data/hero_items.json` | 核心成品装备缓存(Web 站点只读;由 `fetch_hero_items.py` 生成) | | `web/_oss_ability_videos.py` | 阿里云 OSS 桶 `climperor` 建桶 / CORS / 同步 `web/assets/ability_videos/``ability-video/`(凭据经 keyzoo `digitevents/voson-RAM` 注入);`_oss_static_assets.py` 同步图标等静态图;`_oss_fix_public.py` / `_oss_launch_upload.py` 为配套辅助 |
| `data/hero_stats.json` | 英雄各段位 pick/win + `totals` + `window_*`OpenDota **近约 7 天**;Web「走势」Tab:胜率/上场率/场次;冠绝与超凡样本合并展示;由 `fetch_hero_stats.py` 生成;**不**参与局内推荐 | | `web/fetch_hero_portraits.py` | 拉取官网横版头像 → `web/assets/hero_portraits/`(上分帝 Web |
| `data/hero_matches.json` | 同英雄近期比赛列表 + 终局出装/加点 + 可选购买时间(OpenDota;`--source league\|public\|both`Web「近期比赛」Tab;由 `fetch_hero_matches.py` 生成;**不**参与局内推荐) | | `web/fetch_hero_items.py` | 拉取 OpenDota 热门装备 → `web/data/hero_items.json` + `web/assets/item_icons/` |
| `data/leaderboards.json` | Valve Immortal 四区 Top100`china`/`europe`/`americas`/`se_asia`;仅排名+昵称等;由 `fetch_leaderboards.py` 生成;Web「排行」选手榜只读;**不**参与局内推荐 | | `web/fetch_hero_stats.py` | 拉取 OpenDota 各段位场次/胜场 → `web/data/hero_stats.json`(**仅上分帝 Web**;勿写入 relations/heroes,勿进 recommend |
| `data/streamers.json` | 主播目录(手工 `live_url`/profile URL + 常用英雄 + 可选精选视频 `video`/`video_title` + 可选 `video_fit`/`video_crop`/`video_aspect` 控制裁切 + 抖音 profile 补全;`platform_meta`;首版仅抖音;Web 卡片滚播视频、有 `live_url` 时点头像进直播并显示动效(**角标暂以 `live_url` 代理,非真实在播**;将来 `is_live`);由 `fetch_streamers.py` 补全;Web「主播」只读;**不**参与局内推荐 | | `web/fetch_hero_matches.py` | 拉取同英雄近期比赛 + 终局出装/加点 → `web/data/hero_matches.json``--source league\|public\|both`;合并后保留最近 N 场胜局,默认 10;天梯需传奇及以上;公开列表过滤 bot/Turbo、仅 ranked lobby`--public-region china` 优先国服;`--workers` 详情并发 + 凑满即停;可选 `OPENDOTA_API_KEY`;增量补缺失/不足 N 场;`--enrich-item-times` 补购买时间;**仅上分帝 Web**;勿进 recommend |
| `data/stratz_hero_meta.json` | STRATZ 各勋章段位近 N 周 pick/win`weeks`/`latest`+ **同段位最近 1 周分路**`winWeek`+`positionIds`+ `meta_board`(由 `fetch_stratz_meta.py` 生成;Web 英雄详情「走势」优先 + 顶层「走势」`#/trends` 近 N 周榜;**不**参与局内推荐 | | `web/fetch_leaderboards.py` | 拉取 Valve Immortal 四区榜 Top100 → `web/data/leaderboards.json`(**仅上分帝 Web「排行」**;无 MMR/account_id;勿进 recommend |
| `data/stratz_matchup_tops.json` | STRATZ 对位/协同 Topcounters/countered/synergies;由 `fetch_stratz_meta.py` 生成;Web「对位」Tab;与定性 `relations.json` 分开展示;**不**参与局内推荐 | | `web/fetch_streamers.py` | 从抖音主页补全 `web/data/streamers.json` 的昵称/签名/关注粉丝获赞/头像(手工名单 + 直播间/主页 URL;支持 `v.douyin.com` 短链;失败保留旧值;**不**探测开播(由 `fetch_streamer_live.py` 负责);**仅上分帝 Web「主播」**;勿进 recommend;进 `refresh_web` daily |
| `data/item_shop.json` | 商店 11 列目录(官网 basic/upgrade;由 `fetch_item_shop.py` 生成;物品页只读) | | `web/fetch_streamer_live.py` | 探测主播真实在播状态回写 `web/data/streamers.json``is_live`/`live_probed_at`:抖音解析直播间 SSR 页 `roomStore.roomInfo.room.status`(2 在播 / 4 下播;预热 cookie + ~1s 间隔;web_rid 校验),B 站走 `Room/get_info``live_status==1` 在播,轮播算下播);软失败保留旧 `is_live`、始终 exit 0`--ids a,b` 限范围、`--dry-run` 只打印;仅 Web;进 `refresh_web` daily |
| `data/items_meta.json` | 成品装备描述与机制标签(由 `fetch_items_meta.py` 生成) | | `web/frontend/functions/api/live-status.js` | Pages Function `GET /api/live-status`:访问触发的在播探测(逻辑同 `fetch_streamer_live.py`),Cache API 固定键合并请求(5 分钟新鲜窗口,**无 KV**、无需 wrangler 配置);抖音从数据中心 IP 失败属预期 → 单主播沿用旧缓存标 `stale`,全失败回陈旧快照(`stale-override`)或空表(`error`),永不 500`context.waitUntil` 写缓存;导出时 `export_relations_site.py` 拷贝 `functions/` |
| `data/item_tag_overrides.json` | 装备标签手工加减(合并进 items_meta) | | `web/fetch_item_shop.py` | 官网商店 11 列目录(dota2.com.cn/itemscategory+ 合成图 → `web/data/item_shop.json` + 图标 |
| `data/ability_tag_overrides.json` | 技能「施加」类 tags 手工加减(合并进 hero_abilities;仅 Web 机制页 | | `web/fetch_items_meta.py` | Valve/OpenDota 装备描述 → 机制标签 → `web/data/items_meta.json``%token%` 用 special_values 填数;查询类 tags 共用 `mechanic_tags.py` |
| `data/hero_fear_overrides.json` | 英雄→害怕装备手工加减(合并进 item_fears) | | `web/mechanic_tags.py` | 上分帝 Web「机制」页共用标签(弱/强驱散、缠绕/缴械/沉默/锁闭/眩晕/妖术/破坏、睡眠/恐惧/嘲讽/致盲/束缚、隐身/虚无/吹风)+ 中文 labels;语义为「施加该效果」 |
| `data/hero_abilities.json` | 英雄技能与机制汇总(由 `fetch_hero_abilities.py` 生成;含 ability `tags` | | `web/loc_format.py` | Valve 文案共用:去 HTML、填充 `%token%` / `{s:token}` |
| `data/item_counter_stats.json` | OpenDota 对阵装备观测证据缓存(对阵购买率/条件胜率减同装备全局基线;可再生成;不进 recommend / relations | | `web/fetch_hero_abilities.py` | Valve herodata 技能/魔晶/神杖/天赋 + 驱散汇总 → `web/data/hero_abilities.json`;每条 ability 另有「施加」类 `tags`(与 `dispellable` 区分;`--tags-only` 可只重算);`has_scepter`/`has_shard` 只信 Valve 显式 flagValve 移除升级后残留的 `scepter_loc`/`shard_loc` 文案会被清空;逐级相同的 `cast_points`/`channel_times` 合并为单值(施法前摇/吟唱时间);可选 `--icons` / `--icons-only` 缓存 Steam CDN 技能图标 → `web/assets/ability_icons/`(先天用共用 `innate.png`,不拉 CDN |
| `data/hero_item_fears.json` | 英雄怕的装备(规则推导;Web「怕」行) | | `web/fetch_ability_videos.py` | 官网技能演示 webm/mp4Steam CDN;限速)→ `web/assets/ability_videos/`A 杖/魔晶赋予技能(`granted_by_scepter`/`granted_by_shard`CDN 名用 `<hero>_aghanims_scepter`/`<hero>_aghanims_shard`,本地文件名仍用 ability key |
| `web/relations/` | 上分帝 Web 前端静态资源(`index.html` / `config.js` / `app.js` / `style.css` / `router.js`);`config.js``SITE_VERSION``ABILITY_VIDEO_BASE``STATIC_ASSET_BASE`;版本页底部显示 `v{SITE_VERSION}` | | `web/fetch_item_counter_stats.py` | OpenDota Explorer 近场次聚合:对阵英雄时敌方终局装备购买率/胜率,与同窗口该装备全局队伍基线作差 → `web/data/item_counter_stats.json`(可再生成缓存;观测证据,不作因果结论;weekly 软失败) |
| `web/relations/router.js` | Hash 路由:`parseHash` / `serializeHash` / `installRouter` / `syncStateToUrl`;状态↔URL 双向同步(顶层标签 `heroes\|rankings\|streamers\|trends\|mechanics\|items\|patches` / 英雄 + 子标签 `skills\|core\|fears\|trends\|matchups\|matches\|streamers\|patches` / Immortal 地区 `#/rankings[/region]` / 主播目录 `#/streamers` / 近 8 周走势榜 `#/trends[/bracket][?sort=pr]`(默认按胜率) / 机制查询 `#/mechanics[/{effect}]`(默认 `basic_dispel`) / 物品 / 版本 / 标签筛选 / 搜索;旧 `stats` / `#/rankings/meta` 别名兼容) | | `web/item_fears.py` | 规则推导「英雄怕的装备」→ `web/data/hero_item_fears.json`;可选读取 `item_counter_stats.json`,对全部英雄的已有机制候选小幅调序;统计发现的新组合须经机制复核后写入 overrides |
| `templates/cdn/` | 全英雄 CDN 模板(需提交或由脚本生成 | | `web/data/patches.json` | 近一年版本列表 + 逐版本详情(`patches`/`lookup`/`details`;由 `fetch_patches.py` 生成,Web 版本页只读 |
| `templates/roles/` / `templates/modes/` | 位置 / 模式辅助模板 | | `web/data/hero_grid_order.json` | Web 站点四列网格顺序 |
| `assets/role_icons/` | Valve 选人筛选角色图标(`filter_*`,品红 chroma 去黑边 | | `web/data/hero_items.json` | 核心成品装备缓存(Web 站点只读;由 `fetch_hero_items.py` 生成 |
| `assets/hero_portraits/` | 官网横版头像(上分帝 Web;默认 wide 面部构图,非匹配模板 | | `web/data/hero_stats.json` | 英雄各段位 pick/win + `totals` + `window_*`OpenDota **近约 7 天**;Web「走势」Tab:胜率/上场率/场次;冠绝与超凡样本合并展示;由 `fetch_hero_stats.py` 生成;**不**参与局内推荐 |
| `assets/attr_icons/` | 官网主属性图标(力量/敏捷/智力/全才,上分帝 Web 用 | | `web/data/hero_matches.json` | 同英雄近期比赛列表 + 终局出装/加点 + 可选购买时间(OpenDota;`--source league\|public\|both`Web「近期比赛」Tab;由 `fetch_hero_matches.py` 生成;**不**参与局内推荐 |
| `assets/rank_icons/` | 天梯勋章图标(OpenDota `rank_icon_1..8` 先锋→冠绝;走势段位选择器 | | `web/data/leaderboards.json` | Valve Immortal 四区 Top100`china`/`europe`/`americas`/`se_asia`;仅排名+昵称等;由 `fetch_leaderboards.py` 生成;Web「排行」选手榜只读;**不**参与局内推荐 |
| `assets/item_icons/` | Steam CDN 装备图标(上分帝 Web常用装备 | | `web/data/streamers.json` | 主播目录(手工 `live_url`/profile URL + 常用英雄 + 可选精选视频 `video`/`video_title` + 可选 `video_fit`/`video_crop`/`video_aspect` 控制裁切 + 抖音 profile 补全;`platform_meta`;首版仅抖音;Web 卡片滚播视频、有 `live_url` 时点头像进直播间;**「直播」角标/动效只信 `is_live` 真实探测**(线上由访客触发的 `/api/live-status` Pages Function 实时合并刷新,边缘缓存 5 分钟;`data.json``is_live` 为 daily 兜底,接口不可用时前端回退;`live_url` 仅作点击入口);由 `fetch_streamers.py` 补全;Web「主播」只读;**不**参与局内推荐 |
| `assets/item_cat_icons/` | 官网商店分类图标(`itemcat_*.png`,物品页列头 | | `web/data/stratz_hero_meta.json` | STRATZ 各勋章段位近 N 周 pick/win`weeks`/`latest`+ **同段位最近 1 周分路**`winWeek`+`positionIds`+ `meta_board`(由 `fetch_stratz_meta.py` 生成;Web 英雄详情「走势」优先 + 顶层「走势」`#/trends` 近 N 周榜;**不**参与局内推荐 |
| `assets/ability_icons/` | Steam CDN 技能图标(上分帝 Web;按需缓存);`innate.png` 先天共用图标;`talent_tree.png` 天赋树触发图标 | | `web/data/stratz_matchup_tops.json` | STRATZ 对位/协同 Topcounters/countered/synergies;由 `fetch_stratz_meta.py` 生成;Web「对位」Tab;与定性 `relations.json` 分开展示;**不**参与局内推荐) |
| `assets/ui_icons/` | dota2.com.cn 通用 UI 图标(`cooldown.png` 等,技能详情底栏冷却图标)+ 平台 logo(`platform_douyin.png`,主播页 | | `web/data/item_shop.json` | 商店 11 列目录(官网 basic/upgrade;由 `fetch_item_shop.py` 生成;物品页只读 |
| `assets/streamer_avatars/` | 主播头像缓存(由 `fetch_streamers.py` 写入;上分帝 Web「主播」 | | `web/data/items_meta.json` | 成品装备描述与机制标签(由 `fetch_items_meta.py` 生成 |
| `assets/streamer_videos/` | 主播精选高光 mp4(手工放入;上分帝 Web「主播」卡片;勿提交,线上走 OSS `streamer-video/` | | `web/data/item_tag_overrides.json` | 装备标签手工加减(合并进 items_meta |
| `assets/ability_videos/` | 官网技能演示视频(webm/mp4;勿提交,由脚本拉取;线上由阿里云 OSS `climperor``ability-video/` 前缀托管 | | `web/data/ability_tag_overrides.json` | 技能「施加」类 tags 手工加减(合并进 hero_abilities;仅 Web 机制页 |
| `web/data/hero_fear_overrides.json` | 英雄→害怕装备手工加减(合并进 item_fears |
| `web/data/hero_abilities.json` | 英雄技能与机制汇总(由 `fetch_hero_abilities.py` 生成;含 ability `tags` |
| `web/data/item_counter_stats.json` | OpenDota 对阵装备观测证据缓存(对阵购买率/条件胜率减同装备全局基线;可再生成;不进 recommend / relations |
| `web/data/hero_item_fears.json` | 英雄怕的装备(规则推导;Web「怕」行) |
| `web/frontend/` | 上分帝 Web 前端静态资源(`index.html` / `config.js` / `app.js` / `style.css` / `router.js` / `functions/`);`config.js``SITE_VERSION``ABILITY_VIDEO_BASE``STATIC_ASSET_BASE`;版本页底部显示 `v{SITE_VERSION}` |
| `web/frontend/router.js` | Hash 路由:`parseHash` / `serializeHash` / `installRouter` / `syncStateToUrl`;状态↔URL 双向同步(顶层标签 `heroes\|rankings\|streamers\|trends\|mechanics\|items\|patches` / 英雄 + 子标签 `skills\|core\|fears\|trends\|matchups\|matches\|streamers\|patches` / Immortal 地区 `#/rankings[/region]` / 主播目录 `#/streamers` / 近 8 周走势榜 `#/trends[/bracket][?sort=pr]`(默认按胜率) / 机制查询 `#/mechanics[/{effect}]`(默认 `basic_dispel`) / 物品 / 版本 / 标签筛选 / 搜索;旧 `stats` / `#/rankings/meta` 别名兼容) |
| `web/assets/hero_portraits/` | 官网横版头像(上分帝 Web;默认 wide 面部构图,非匹配模板) |
| `web/assets/attr_icons/` | 官网主属性图标(力量/敏捷/智力/全才,上分帝 Web 用) |
| `web/assets/rank_icons/` | 天梯勋章图标(OpenDota `rank_icon_1..8` 先锋→冠绝;走势段位选择器) |
| `web/assets/item_icons/` | Steam CDN 装备图标(上分帝 Web常用装备) |
| `web/assets/item_cat_icons/` | 官网商店分类图标(`itemcat_*.png`,物品页列头) |
| `web/assets/ability_icons/` | Steam CDN 技能图标(上分帝 Web;按需缓存);`innate.png` 先天共用图标;`talent_tree.png` 天赋树触发图标 |
| `web/assets/ui_icons/` | dota2.com.cn 通用 UI 图标(`cooldown.png` 等,技能详情底栏冷却图标)+ 平台 logo(`platform_douyin.png`,主播页) |
| `web/assets/streamer_avatars/` | 主播头像缓存(由 `fetch_streamers.py` 写入;上分帝 Web「主播」) |
| `web/assets/streamer_videos/` | 主播精选高光 mp4(手工放入;上分帝 Web「主播」卡片;勿提交,线上走 OSS `streamer-video/` |
| `web/assets/ability_videos/` | 官网技能演示视频(webm/mp4;勿提交,由脚本拉取;线上由阿里云 OSS `climperor``ability-video/` 前缀托管) |
运行时产物(**勿提交**,见 `.gitignore`): 运行时产物(**勿提交**,见 `.gitignore`):
- `samples/raw/<matchid>/` — GSI 会话截图与 `gsi.jsonl`;手动 `capture.py` 可写在 `raw/` 根下 - `pc/samples/raw/<matchid>/` — GSI 会话截图与 `gsi.jsonl`;手动 `capture.py` 可写在 `raw/` 根下
- `preview/` — 标定 / sheet 预览 - `pc/preview/` — 标定 / sheet 预览
- `dist/` — 静态站点导出(`export_relations_site.py` - `web/dist/` — 静态站点导出(`export_relations_site.py`
- `results/` — 每局 JSON - `pc/results/` — 每局 JSON
- `failures/``--truth` 调试用错识裁切 - `pc/failures/``--truth` 调试用错识裁切
- `data/matchups.json` / `synergies.json` / `stratz_matchups.json` / `relations_audit.json` 等 — 可再生成缓存与报告 - `shared/data/matchups.json` / `synergies.json` / `stratz_matchups.json` / `relations_audit.json` 等 — 可再生成缓存与报告
- `assets/ability_videos/` — 官网技能演示(体积大,可再拉取) - `web/data/hero_stats.json` / `hero_matches.json` / `stratz_*.json` 等 — 可再生成缓存
- `assets/streamer_videos/`主播精选高光(体积大,手工放入后 OSS 同步 - `web/assets/ability_videos/`官网技能演示(体积大,可再拉取
- `web/assets/streamer_videos/` — 主播精选高光(体积大,手工放入后 OSS 同步)
## 数据流 ## 数据流
``` ```
Dota 2 GSI → gsi_watch.py (:3223) Dota 2 GSI → pc/gsi_watch.py (:3223)
→ 全量 payload → samples/raw/<matchid>/gsi.jsonl(可关) → 全量 payload → pc/samples/raw/<matchid>/gsi.jsonl(可关)
→ DraftSession 轮询截屏 → samples/raw/<matchid>/ → DraftSession 轮询截屏 → pc/samples/raw/<matchid>/
→ recognize_image (CDN 模板 + 可选天梯遮罩) → recognize_image (CDN 模板 + 可选天梯遮罩)
→ roles(分路字)/ grid(禁用 + 单元格)/ modes → roles(分路字)/ shared.grid(禁用 + 单元格)/ modes
→ recommend 克/搭/补(relations + draft_archetypes 规则画像,本人槽位只信 GSI) → recommend 克/搭/补(relations + draft_archetypes 规则画像,本人槽位只信 GSI)
→ overlay 角色标签 + 网格克/搭/补 + 阵容分析条(可选) → overlay 角色标签 + 网格克/搭/补 + 阵容分析条(可选)
→ results/draft_*.json + 终端时间线 pc/results/draft_*.json + 终端时间线
``` ```
## 技术约束(修改前必读) ## 技术约束(修改前必读)
- **合规**:仅 GSI + 屏幕截图;**禁止**读进程内存、注入、绕过 VAC。 - **合规**:仅 GSI + 屏幕截图;**禁止**读进程内存、注入、绕过 VAC。
- **GSI 范围**:普通玩家视角拿不到双方 pick;GSI 只作阶段触发、`team_slot`、本人 `hero`、本机 `accountid`/`steamid`。本人顶栏槽位**只信** GSI,不用截屏名字亮度猜测。 - **GSI 范围**:普通玩家视角拿不到双方 pick;GSI 只作阶段触发、`team_slot`、本人 `hero`、本机 `accountid`/`steamid`。本人顶栏槽位**只信** GSI,不用截屏名字亮度猜测。
- **克制 / 搭档数据**:机制克制/搭档只用定性边存 `data/relations.json`(克制有向 + 搭档无向 + 理由),**不要**用胜率/场次表达机制克制,也**不要**写入 `data/heroes.json`。Web 英雄页三视图:克制 / 被克制 / 搭档。OpenDota 段位胜率/场次单独存 `data/hero_stats.json`(近约 7 天窗;上场率 = 出场/(Σ出场/10);冠绝样本过小时与超凡合并),**仅**上分帝 Web 英雄详情「走势」**无 STRATZ 时兜底**。STRATZ 周胜率/分路/`meta_board``data/stratz_hero_meta.json`(三卡与分路均取 **当前选中勋章 · 最近 1 周** `winWeek`;近 8 周列表从新到旧;分路在 8 周列表上方),对位 Top 存 `data/stratz_matchup_tops.json`(英雄详情「走势」与顶层「走势」榜 `#/trends` 优先用 STRATZ;「对位」Tab 只读),**禁止**合并进 relations/heroes,禁止 `recommend.py` 读取。同英雄近期比赛出装/加点单独存 `data/hero_matches.json`**仅** Web「近期比赛」Tab,禁止进 recommend。Valve Immortal 四区榜单独存 `data/leaderboards.json`,**仅** Web「排行」页选手榜,禁止进 recommend。主播目录单独存 `data/streamers.json`(手工名单 + 抖音 profile 补全;首版仅抖音),**仅** Web 顶层「主播」与英雄详情「主播」Tab,禁止进 recommend;由 `refresh_web` daily 软失败刷新粉丝等字段(不阻断整档)。 - **克制 / 搭档数据**:机制克制/搭档只用定性边存 `shared/data/relations.json`(克制有向 + 搭档无向 + 理由),**不要**用胜率/场次表达机制克制,也**不要**写入 `shared/data/heroes.json`。Web 英雄页三视图:克制 / 被克制 / 搭档。OpenDota 段位胜率/场次单独存 `web/data/hero_stats.json`(近约 7 天窗;上场率 = 出场/(Σ出场/10);冠绝样本过小时与超凡合并),**仅**上分帝 Web 英雄详情「走势」**无 STRATZ 时兜底**。STRATZ 周胜率/分路/`meta_board``web/data/stratz_hero_meta.json`(三卡与分路均取 **当前选中勋章 · 最近 1 周** `winWeek`;近 8 周列表从新到旧;分路在 8 周列表上方),对位 Top 存 `web/data/stratz_matchup_tops.json`(英雄详情「走势」与顶层「走势」榜 `#/trends` 优先用 STRATZ;「对位」Tab 只读),**禁止**合并进 relations/heroes,禁止 `pc/recommend.py` 读取。同英雄近期比赛出装/加点单独存 `web/data/hero_matches.json`**仅** Web「近期比赛」Tab,禁止进 recommend。Valve Immortal 四区榜单独存 `web/data/leaderboards.json`,**仅** Web「排行」页选手榜,禁止进 recommend。主播目录单独存 `web/data/streamers.json`(手工名单 + 抖音 profile 补全;首版仅抖音),**仅** Web 顶层「主播」与英雄详情「主播」Tab,禁止进 recommend;由 `refresh_web` daily 软失败刷新粉丝等字段(不阻断整档)。
- **装备机制 / 怕的装备**:标签与技能汇总来自 Valve/OpenDota 自动抽取 + `item_tag_overrides.json``item_fears.py` 规则映射到英雄弱点。`fetch_item_counter_stats.py` 的对阵购买率/胜率差仅作为观测证据:须减去同装备全局基线,对全部英雄的已有机制候选只允许小幅调序;统计发现的新组合即使同时满足 `games≥100`、购买率提升 `≥3pp`、条件胜率差 `≥1.5pp`、两项双比例检验 `z≥1.96`,仍须确认机制成立后手工写入 overrides,禁止直接把高相关当因果克制。大段技能文案只进 `data/hero_abilities.json` / `items_meta.json`**不要**塞进 `heroes.json`。本阶段仅上分帝 Web 展示,**不对局内出装推荐**。核心装「使用率」为 `hero_items` 列表内相对热度归一化,非绝对出场率、无段位维度。技能/物品「施加」类 tags(机制查询页)与技能 `dispellable`(效果能否被驱散)严格区分;**禁止**写入 recommend / relations。 - **装备机制 / 怕的装备**:标签与技能汇总来自 Valve/OpenDota 自动抽取 + `item_tag_overrides.json``item_fears.py` 规则映射到英雄弱点。`fetch_item_counter_stats.py` 的对阵购买率/胜率差仅作为观测证据:须减去同装备全局基线,对全部英雄的已有机制候选只允许小幅调序;统计发现的新组合即使同时满足 `games≥100`、购买率提升 `≥3pp`、条件胜率差 `≥1.5pp`、两项双比例检验 `z≥1.96`,仍须确认机制成立后手工写入 overrides,禁止直接把高相关当因果克制。大段技能文案只进 `web/data/hero_abilities.json` / `items_meta.json`**不要**塞进 `heroes.json`。本阶段仅上分帝 Web 展示,**不对局内出装推荐**。核心装「使用率」为 `hero_items` 列表内相对热度归一化,非绝对出场率、无段位维度。技能/物品「施加」类 tags(机制查询页)与技能 `dispellable`(效果能否被驱散)严格区分;**禁止**写入 recommend / relations。
- **机制查询页**:顶层 `#/mechanics[/{effect}]`;侧栏弱/强驱散 + 核心控制;结果为施加该效果的技能与物品。数据边界:仅 Web。 - **机制查询页**:顶层 `#/mechanics[/{effect}]`;侧栏弱/强驱散 + 核心控制;结果为施加该效果的技能与物品。数据边界:仅 Web。
- **模板策略**:只维护 CDN 层。皮肤问题用会话策略(选人可改判、决策 `allow_revise=False`、best 帧偏 HERO_SELECTION),不要为皮肤加模板库,也不要复活 real 双层库。 - **模板策略**:只维护 CDN 层。皮肤问题用会话策略(选人可改判、决策 `allow_revise=False`、best 帧偏 HERO_SELECTION),不要为皮肤加模板库,也不要复活 real 双层库。
- **顶栏时机**:皮肤在**全员选完后**才上顶栏;本机进决策时别人可能还在选——须保持 `strategy_tail` 视觉,禁止「一进 STRATEGY 就永久停读」。 - **顶栏时机**:皮肤在**全员选完后**才上顶栏;本机进决策时别人可能还在选——须保持 `strategy_tail` 视觉,禁止「一进 STRATEGY 就永久停读」。
- **坐标**:一律相对坐标(相对屏幕高 / 相对中心),勿写死像素分辨率。 - **坐标**:一律相对坐标(相对屏幕高 / 相对中心),勿写死像素分辨率。
- **宁可不认,不可乱认**`min_score` + `min_margin` 双门控;不确定就 `null` - **宁可不认,不可乱认**`min_score` + `min_margin` 双门控;不确定就 `null`
- **平台**:面向 Windows;截屏依赖无边框/窗口模式。 - **平台**:面向 Windows;截屏依赖无边框/窗口模式。
- **上分帝 Web 定时刷新**Gitea Actionsself-hosted)跑 `refresh_web.py`;易变 STRATZ/stats/主播粉丝等 **不回写 git**(job 内现拉现部署)。数据-only 刷新不 bump `SITE_VERSION`。技能视频与手工 `relations.json` 不进定时。禁止把 STRATZ / hero_stats / matches / leaderboards / streamers 写入 recommend。 - **上分帝 Web 定时刷新**Gitea Actionsself-hosted)跑 `web/refresh_web.py`;易变 STRATZ/stats/主播粉丝等 **不回写 git**(job 内现拉现部署)。数据-only 刷新不 bump `SITE_VERSION`。技能视频与手工 `relations.json` 不进定时。禁止把 STRATZ / hero_stats / matches / leaderboards / streamers 写入 recommend。
## 开发命令 ## 开发命令
```powershell ```powershell
pip install -r requirements.txt pip install -r pc/requirements.txt
pip install -r requirements-web.txt # 可选:本机 refresh_web / OSSCI 会装) pip install -r web/requirements.txt # 可选:本机 refresh_web / OSSCI 会装)
python fetch_cdn_templates.py python pc/fetch_cdn_templates.py
python import_relations_xlsx.py # 可选:从选将笔记 xlsx 导入关系 python shared/import_relations_xlsx.py # 可选:从选将笔记 xlsx 导入关系
python fetch_hero_portraits.py # 官网横版头像(上分帝 Web python web/fetch_hero_portraits.py # 官网横版头像(上分帝 Web
python fetch_hero_items.py # OpenDota 热门装备缓存(上分帝 Web) python web/fetch_hero_items.py # OpenDota 热门装备缓存(上分帝 Web)
python fetch_hero_stats.py # OpenDota 各段位胜率/场次(上分帝 Web 走势兜底) python web/fetch_hero_stats.py # OpenDota 各段位胜率/场次(上分帝 Web 走势兜底)
python fetch_stratz_meta.py # STRATZ 周胜率/分路/对位 Top(走势/对位/Meta;需 token python web/fetch_stratz_meta.py # STRATZ 周胜率/分路/对位 Top(走势/对位/Meta;需 token
python fetch_hero_matches.py # 同英雄近期比赛出装/加点(上分帝 Web;可 --heroes antimage--workers 6--enrich-item-times python web/fetch_hero_matches.py # 同英雄近期比赛出装/加点(上分帝 Web;可 --heroes antimage--workers 6--enrich-item-times
python fetch_leaderboards.py # Valve Immortal 四区 Top100(排行页) python web/fetch_leaderboards.py # Valve Immortal 四区 Top100(排行页)
python fetch_streamers.py # 抖音主播主页补全(主播页;手工名单;亦由 daily 定时软失败刷新) python web/fetch_streamers.py # 抖音主播主页补全(主播页;手工名单;亦由 daily 定时软失败刷新)
python fetch_item_shop.py # 商店分类目录(物品页 python web/fetch_streamer_live.py # 探测真实在播状态回写 is_live(主播页角标;daily--dry-run 只打印
python fetch_items_meta.py # 装备描述 + 机制标签 python web/fetch_item_shop.py # 商店分类目录(物品页)
python fetch_hero_abilities.py # 英雄技能 / 驱散汇总 python web/fetch_items_meta.py # 装备描述 + 机制标签
python fetch_hero_abilities.py --tags-only # 只重算技能「施加」类 tags python web/fetch_hero_abilities.py # 英雄技能 / 驱散汇总
python fetch_hero_abilities.py --icons-only # 缓存技能图标到 assets/ability_icons/ python web/fetch_hero_abilities.py --tags-only # 只重算技能「施加」类 tags
python fetch_ability_videos.py # 官网技能演示(限速;默认 webm) python web/fetch_hero_abilities.py --icons-only # 缓存技能图标到 web/assets/ability_icons/
python fetch_item_counter_stats.py # OpenDota 对阵装备相对全局基线(可再生成缓存 python web/fetch_ability_videos.py # 官网技能演示(限速;默认 webm
python item_fears.py # 英雄怕的装备 python web/fetch_item_counter_stats.py # OpenDota 对阵装备相对全局基线(可再生成缓存)
python fetch_patches.py # 近一年版本日志 + 详情(版本页) python web/item_fears.py # 英雄怕的装备
python fetch_patches.py --check # 只比对列表与本地 detailsstdout JSONhas_new python web/fetch_patches.py # 近一年版本日志 + 详情(版本页
python refresh_web.py --tier patch --skip-deploy --skip-oss # 本机试跑某档(daily/weekly/patch/all python web/fetch_patches.py --check # 只比对列表与本地 detailsstdout JSONhas_new
python serve_relations.py # 上分帝 Web 本地开发服务(英雄 / 排行 / 主播 / 走势 / 机制 / 物品 / 版本 python web/refresh_web.py --tier patch --skip-deploy --skip-oss # 本机试跑某档(daily/weekly/patch/all
python export_relations_site.py # 导出静态站点到 dist/relations/(可部署 Pages python web/serve_relations.py # 上分帝 Web 本地开发服务(英雄 / 排行 / 主播 / 走势 / 机制 / 物品 / 版本
python export_relations_site.py --ability-video-base https://climperor.oss-cn-shanghai.aliyuncs.com --static-asset-base https://climperor.oss-cn-shanghai.aliyuncs.com python web/export_relations_site.py # 导出静态站点到 web/dist/relations/(可部署 Pages
python deploy_relations.py # 部署 dist/relations 到 Cloudflare Pages(凭据经 keyzoo 注入或 env;默认 dota2.refining.dev;视频与图标走 OSS python web/export_relations_site.py --ability-video-base https://climperor.oss-cn-shanghai.aliyuncs.com --static-asset-base https://climperor.oss-cn-shanghai.aliyuncs.com
python notify_site_traffic.py --dry-run # 上分帝 Web 日活摘要(飞书;需 CF + webhook env / keyzoo python web/deploy_relations.py # 部署 web/dist/relations 到 Cloudflare Pages(凭据经 keyzoo 注入或 env;默认 dota2.refining.dev;视频与图标走 OSS
python _oss_static_assets.py upload # 图标/portrait 变更后同步 OSSkeyzoo voson-RAM python web/notify_site_traffic.py --dry-run # 上分帝 Web 日活摘要(飞书;需 CF + webhook env / keyzoo
python _cf_status.py # 只读查 CF Pages 部署 / 自定义域名状态 python web/_oss_static_assets.py upload # 图标/portrait 变更后同步 OSSkeyzoo voson-RAM
python web/_cf_status.py # 只读查 CF Pages 部署 / 自定义域名状态
# Gitea Actionsself-hosted):web-daily / web-weekly / web-patchSecrets 见 README「定时刷新」与各 workflow 头注释 # Gitea Actionsself-hosted):web-daily / web-weekly / web-patchSecrets 见 README「定时刷新」与各 workflow 头注释
python gsi_setup.py --check python pc/gsi_setup.py --check
python gsi_watch.py --once python pc/gsi_watch.py --once
python recognize.py samples/raw/<>.png --sheet python pc/recognize.py pc/samples/raw/<>.png --sheet
python evaluate.py python pc/evaluate.py
``` ```
## 修改原则 ## 修改原则
- 只改任务所需逻辑,避免无关重构与大范围格式化。 - 只改任务所需逻辑,避免无关重构与大范围格式化。
- 代码标识符、日志、错误信息、注释用**英文**;用户可见摘要可用中文。 - 代码标识符、日志、错误信息、注释用**英文**;用户可见摘要可用中文。
- 改匹配阈值或裁切时,用 `evaluate.py` / 标注帧验证,并更新 `CHANGELOG.md` 与必要时的 `DESIGN.md` - 改匹配阈值或裁切时,用 `pc/evaluate.py` / 标注帧验证,并更新 `CHANGELOG.md` 与必要时的 `DESIGN.md`
- 改 GSI cfg 时同步核对 `gsi_setup.py` 与 Dota `gamestate_integration` 目录。 - 改 GSI cfg 时同步核对 `pc/gsi_setup.py` 与 Dota `gamestate_integration` 目录。
- 改 Web 路由形态(URL 段 / query 参数 / 默认值)时同步 `web/relations/router.js``parseHash` / `serializeHash`)与 `app.js``applyPatch` 校验;新增可路由状态维度时在两处都加,并在 `syncStateToUrl` 调用点(含搜索 debounce)接好。Hash 路由不命中后端,`serve_relations.py` 无需改;`export_relations_site.py` 的导出文件列表须含 `router.js` - 改 Web 路由形态(URL 段 / query 参数 / 默认值)时同步 `web/frontend/router.js``parseHash` / `serializeHash`)与 `app.js``applyPatch` 校验;新增可路由状态维度时在两处都加,并在 `syncStateToUrl` 调用点(含搜索 debounce)接好。Hash 路由不命中后端,`web/serve_relations.py` 无需改;`web/export_relations_site.py` 的导出文件列表须含 `router.js`
-`data/heroes.json` 结构时同步 `grid.py`(依赖 `attr` / `name_loc`);`roles``overlay.py` 使用;`aliases` 为中文口语/俗称(勿与 `name_loc` 重复),重跑 `fetch_cdn_templates.py` 会按 `key` 合并保留;`tags` 为中文定位(核心/辅助/…/幻象,由 `roles`+幻想系推导),上分帝 Web 筛选 + 局内 `draft_archetypes` 缺口/推进画像共用;基础属性/血蓝等数值字段供上分帝 Web 详情条,勿塞机制文案。 -`shared/data/heroes.json` 结构时同步 `shared/grid.py`(依赖 `attr` / `name_loc`);`roles``pc/overlay.py` 使用;`aliases` 为中文口语/俗称(勿与 `name_loc` 重复),重跑 `pc/fetch_cdn_templates.py` 会按 `key` 合并保留;`tags` 为中文定位(核心/辅助/…/幻象,由 `roles`+幻想系推导),上分帝 Web 筛选 + 局内 `draft_archetypes` 缺口/推进画像共用;基础属性/血蓝等数值字段供上分帝 Web 详情条,勿塞机制文案。
- 发版上分帝 Web 时同步 bump `export_relations_site.py``SITE_VERSION``web/relations/config.js` 的同名变量,以及 `index.html``style.css`/`config.js`/`router.js`/`app.js``?v=` 缓存戳;并写 `CHANGELOG.md` - 发版上分帝 Web 时同步 bump `web/export_relations_site.py``SITE_VERSION``web/frontend/config.js` 的同名变量,以及 `index.html``style.css`/`config.js`/`router.js`/`app.js``?v=` 缓存戳;并写 `CHANGELOG.md`
- 不要重新引入 real 模板双层库、`cdn_penalty``build_library.py` - 不要重新引入 real 模板双层库、`cdn_penalty``build_library.py`
## 文档分工 ## 文档分工
+31
View File
@@ -4,6 +4,37 @@
## [Unreleased] ## [Unreleased]
### Changed
- 仓库重组为 `pc/`(局内识别)、`web/`(上分帝 Web)与 `shared/`(共享包)三层
monorepo;保留脚本直跑方式,站点内容与 `SITE_VERSION` 不变。
## [0.5.61] - 2026-07-29
### Added
- 上分帝 Web「主播」访问触发的实时在播状态:新增 Pages Function
`GET /api/live-status``web/frontend/functions/api/live-status.js`),首位访客触发
全量探测,经边缘 Cache API 合并请求(5 分钟新鲜窗口内共享结果,无需 KV);
探测逻辑与 `fetch_streamer_live.py` 一致(抖音 SSR `roomStore` + B 站
`Room/get_info`);单个探测失败沿用旧缓存值并标 `stale`,全失败回陈旧快照或空表,
永不 500。前端每页加载至多请求一次,合并 `is_live` 后就地更新「直播」角标/动效;
接口不可用时回退 `data.json` 的 daily 探测值。本地开发 `serve_relations.py`
提供空 stub 避免 404。
## [0.5.60] - 2026-07-29
### Added
- 上分帝 Web「主播」真实在播探测:`fetch_streamer_live.py`(抖音解析直播间 SSR 页
`roomStore` 状态;B 站 `Room/get_info`)回写 `data/streamers.json`
`is_live` / `live_probed_at`,探测失败保留旧值;进 `refresh_web --tier daily`
### Changed
- 主播卡片「直播」角标与脉冲动效只在 `is_live` 探测为真时显示(此前以 `live_url`
存在作代理,不准确);头像在有 `live_url` 时仍可点击进直播间,未开播也可访问。
## [0.5.59] - 2026-07-29 ## [0.5.59] - 2026-07-29
### Added ### Added
+30 -29
View File
@@ -158,26 +158,27 @@ GSI 不含可靠 lobby 类型时,用右下金色勋章检测(`has_ranked_ove
``` ```
climperor/ climperor/
├── config.json ├── pc/ # 局内选将识别(GSI + 截屏 + OpenCV
├── data/ # heroes / relations / items / abilities / fears … │ ├── common.py / recognize.py / draft_session.py / gsi_watch.py
├── common.py / recognize.py / draft_session.py ├── recommend.py / draft_archetypes.py / roles.py / modes.py / overlay.py
├── gsi_watch.py / gsi_setup.py / overlay.py │ ├── config.json / templates/ / assets/role_icons/ / samples/
├── recommend.py / relations.py / roles.py / grid.py / modes.py │ └── requirements.txt
├── http_utils.py / loc_format.py / hero_tags.py / item_fears.py ├── web/ # 上分帝 Web(前端、数据流水线、部署)
├── fetch_cdn_templates.py / fetch_hero_portraits.py / fetch_hero_items.py ├── frontend/ # 原 web/relations/
├── fetch_item_shop.py / fetch_items_meta.py / fetch_hero_abilities.py │ ├── data/ / assets/ / dist/
├── fetch_ability_videos.py / fetch_stratz.py / audit_relations.py ├── fetch_*.py / item_fears.py / mechanic_tags.py / loc_format.py
├── fetch_patches.py / refresh_web.py / import_relations_xlsx.py / serve_relations.py │ ├── serve_relations.py / export_relations_site.py / deploy_relations.py
├── export_relations_site.py / deploy_relations.py │ ├── refresh_web.py / notify_site_traffic.py / _cf_* / _oss_* / _gitea_*
│ └── requirements.txt
├── shared/ # 只被 pc/web 依赖,绝不反向依赖
│ ├── paths.py / grid.py / relations.py / hero_tags.py / http_utils.py
│ ├── import_relations_xlsx.py / audit_relations.py / fetch_matchups.py / fetch_stratz.py
│ └── data/heroes.json / data/relations.json
├── .gitea/workflows/ # site-traffic-notify + web-daily/weekly/patch ├── .gitea/workflows/ # site-traffic-notify + web-daily/weekly/patch
── autocalibrate.py / calibrate.py / capture.py / evaluate.py ── README.md / AGENTS.md / DESIGN.md / CHANGELOG.md
├── web/relations/ # 上分帝 Web(英雄/排行/物品/版本)
├── templates/cdn|roles|modes/
├── assets/ # portraits / icons / videos(部分 gitignore
└── samples/labels.json # 评测真值;raw/<matchid>/ 见 .gitignore
``` ```
运行时目录 `preview/``results/``failures/``samples/raw/`(含按对局子目录)不入库。上分帝 Web 易变数据(STRATZ meta、hero_stats 等)可由 Gitea Actions 定时拉取后直部 Pages,不必入库。 运行时目录 `pc/preview/``pc/results/``pc/failures/``pc/samples/raw/`(含按对局子目录)不入库。上分帝 Web 易变数据(STRATZ meta、hero_stats 等)可由 Gitea Actions 定时拉取后直部 Pages,不必入库。
### 主要配置 ### 主要配置
@@ -197,7 +198,7 @@ climperor/
| `recommend.min_enemies` | 开始推荐所需敌方锁人数 | 1 | | `recommend.min_enemies` | 开始推荐所需敌方锁人数 | 1 |
| `recommend.min_heroes_for_gaps` | 缺口/补位注入所需锁人数 | 2 | | `recommend.min_heroes_for_gaps` | 缺口/补位注入所需锁人数 | 2 |
| `recommend.archetypes` | 推进/全球流/缺口规则画像 | true | | `recommend.archetypes` | 推进/全球流/缺口规则画像 | true |
| `recommend.relations_path` | 定性关系文件 | `data/relations.json` | | `recommend.relations_path` | 定性关系文件 | `shared/data/relations.json` |
| `recommend.role_tags` | 分路→角色标签过滤(1–5 号位;非定位局不过滤) | 见 config.json | | `recommend.role_tags` | 分路→角色标签过滤(1–5 号位;非定位局不过滤) | 见 config.json |
| `overlay.enabled` | 选将顶栏角色标签悬浮层 | true | | `overlay.enabled` | 选将顶栏角色标签悬浮层 | true |
| `overlay.y_gap_rel` / `icon_h_rel` / `icon_gap_rel` | 标签相对屏幕高的间距与尺寸 | 0.008 / 0.016 / 0.002 | | `overlay.y_gap_rel` / `icon_h_rel` / `icon_gap_rel` | 标签相对屏幕高的间距与尺寸 | 0.008 / 0.016 / 0.002 |
@@ -211,13 +212,13 @@ climperor/
``` ```
Dota 2 (-gamestateintegration) Dota 2 (-gamestateintegration)
→ POST → gsi_watch.py :3223 → POST → pc/gsi_watch.py :3223
→ samples/raw/{matchid}/gsi.jsonl(可选) pc/samples/raw/{matchid}/gsi.jsonl(可选)
→ DraftSession 轮询 recognize + roles/grid/modes → DraftSession 轮询 recognize + roles/grid/modes
→ results/draft_*.json pc/results/draft_*.json
``` ```
要点:按 `matchid` 去重;截图进 `samples/raw/{matchid}/`;识别在工作线程; 要点:按 `matchid` 去重;截图进 `pc/samples/raw/{matchid}/`;识别在工作线程;
未标定时降级为只截图。 未标定时降级为只截图。
天梯 AP 选人按 2/2/1 成批揭晓,本轮结束前互不可见——因此必须在 天梯 AP 选人按 2/2/1 成批揭晓,本轮结束前互不可见——因此必须在
@@ -230,26 +231,26 @@ Dota 2 (-gamestateintegration)
- **触发**:敌方至少锁定 1 人(`min_enemies`)后开始;本人锁人后清空标记。 - **触发**:敌方至少锁定 1 人(`min_enemies`)后开始;本人锁人后清空标记。
- **分路**:定位匹配画面字(`roles.py`+ GSI `team_slot` 定位本人;不做截屏认自己。有分路则按 `role_tags` 过滤候选;非定位局(读不到分路字)则全英雄表。 - **分路**:定位匹配画面字(`roles.py`+ GSI `team_slot` 定位本人;不做截屏认自己。有分路则按 `role_tags` 过滤候选;非定位局(读不到分路字)则全英雄表。
- **关系模型**:机制克制/搭档(`data/relations.json`),不用胜率/场次——版本会变,机制边相对稳。Web 英雄详情「走势」Tab 优先 STRATZ(`data/stratz_hero_meta.json`):勋章条切换 8 档 → **最近 1 周**三卡(胜率/上场率/场次)→ 1~5 号位分路胜率 → **近 8 周**逐周列表(日期、胜率、周环比、区间缩放横条、场次;新→旧);OpenDota(`data/hero_stats.json`,近约 7 天)作兜底。「对位」Tab 展示 STRATZ 数值 Top`data/stratz_matchup_tops.json`),与网格定性「克/怕/搭」分开展示;均不进局内推荐。 - **关系模型**:机制克制/搭档(`shared/data/relations.json`),不用胜率/场次——版本会变,机制边相对稳。Web 英雄详情「走势」Tab 优先 STRATZ(`web/data/stratz_hero_meta.json`):勋章条切换 8 档 → **最近 1 周**三卡(胜率/上场率/场次)→ 1~5 号位分路胜率 → **近 8 周**逐周列表(日期、胜率、周环比、区间缩放横条、场次;新→旧);OpenDota(`web/data/hero_stats.json`,近约 7 天)作兜底。「对位」Tab 展示 STRATZ 数值 Top`web/data/stratz_matchup_tops.json`),与网格定性「克/怕/搭」分开展示;均不进局内推荐。
- **阵容画像(规则,不接 AI)**:[`draft_archetypes.py`](draft_archetypes.py) 识别敌方推进/全球流、敌我 tag 缺口(缺控制/爆发/输出/先手);输出一句 `analysis`(如 `敌:缺控制·偏推进 | 我:缺爆发`)与每英雄短 `reasons` - **阵容画像(规则,不接 AI)**:[`pc/draft_archetypes.py`](pc/draft_archetypes.py) 识别敌方推进/全球流、敌我 tag 缺口(缺控制/爆发/输出/先手);输出一句 `analysis`(如 `敌:缺控制·偏推进 | 我:缺爆发`)与每英雄短 `reasons`
- **纳入** - **纳入**
- **克**:关系克制边;推进/全球流应对表(如美杜莎对推进);惩戒敌方缺口 - **克**:关系克制边;推进/全球流应对表(如美杜莎对推进);惩戒敌方缺口
- **搭**:与已锁己有搭档边 - **搭**:与已锁己有搭档边
- **补**:填我方缺口(`min_heroes_for_gaps` 默认 2 才注入缺口类标记) - **补**:填我方缺口(`min_heroes_for_gaps` 默认 2 才注入缺口类标记)
- **打分**:关系边为主;阵容应对 / 缺口小幅加分;敌方 tags 软加分仅排序。 - **打分**:关系边为主;阵容应对 / 缺口小幅加分;敌方 tags 软加分仅排序。
- **展示**:网格左上角全量相关格标「克」(青)/「搭」(琥珀)/「补」(紫灰);叠加层横条显示阵容分析;原因进日志与 `results` JSON,不写在每个格子旁。 - **展示**:网格左上角全量相关格标「克」(青)/「搭」(琥珀)/「补」(紫灰);叠加层横条显示阵容分析;原因进日志与 `results` JSON,不写在每个格子旁。
- **上分帝 Web**`serve_relations.py` 本地开发服务;仿选将网格查看克制/被克制/搭档;数据手改 `data/relations.json` 或从 xlsx 导入。 - **上分帝 Web**`web/serve_relations.py` 本地开发服务;仿选将网格查看克制/被克制/搭档;数据手改 `shared/data/relations.json` 或从 xlsx 导入。
另有顶级 **排行**、**走势**`#/trends[/bracket]`,近 8 周高胜率/上场率榜)、**物品**、**版本** 页;目录见 `data/item_shop.json` / `data/patches.json` / `data/leaderboards.json` 另有顶级 **排行**、**走势**`#/trends[/bracket]`,近 8 周高胜率/上场率榜)、**物品**、**版本** 页;目录见 `web/data/item_shop.json` / `web/data/patches.json` / `web/data/leaderboards.json`
英雄详情子标签含 **走势**(见上;`#/heroes/<key>/trends`)、**对位**(克制/被克/搭档三列 STRATZ Top`#/heroes/<key>/matchups`)、**近期比赛**`#/heroes/<key>/matches`)。排行页为 Immortal 四区选手榜。 英雄详情子标签含 **走势**(见上;`#/heroes/<key>/trends`)、**对位**(克制/被克/搭档三列 STRATZ Top`#/heroes/<key>/matchups`)、**近期比赛**`#/heroes/<key>/matches`)。排行页为 Immortal 四区选手榜。
- **Web Hash 路由**:状态(顶层标签 / 选中英雄 + 详情子标签 / 选中物品 / 选中版本 / - **Web Hash 路由**:状态(顶层标签 / 选中英雄 + 详情子标签 / 选中物品 / 选中版本 /
标签筛选 / 搜索)双向同步进 URL(`#/heroes/axe/core?tags=核心&q=axe``#/heroes/axe/trends` 等)。选 Hash 标签筛选 / 搜索)双向同步进 URL(`#/heroes/axe/core?tags=核心&q=axe``#/heroes/axe/trends` 等)。选 Hash
而非 Path 路由:静态导出(GitHub Pages)无需 rewrite、`serve_relations.py` 零改动、 而非 Path 路由:静态导出(GitHub Pages)无需 rewrite、`web/serve_relations.py` 零改动、
相对路径子站亦可用。写 URL 用 `history.pushState`/`replaceState`(静默,不触发 相对路径子站亦可用。写 URL 用 `history.pushState`/`replaceState`(静默,不触发
`hashchange`,无回环);前进/后退靠 `hashchange` 监听重 parse + 校验 + render。 `hashchange`,无回环);前进/后退靠 `hashchange` 监听重 parse + 校验 + render。
搜索 debounce 300ms + `replaceState` 防刷历史栈;坏链接丢弃该项不崩。路由逻辑集中在 搜索 debounce 300ms + `replaceState` 防刷历史栈;坏链接丢弃该项不崩。路由逻辑集中在
`web/relations/router.js``app.js` 只在 `main()``installRouter` + 各 state 变更点 `web/frontend/router.js``app.js` 只在 `main()``installRouter` + 各 state 变更点
`syncStateToUrl` `syncStateToUrl`
- **不做**:避用标、Steam 登录页、爬 Dotabuff;关系**不**写入 `data/heroes.json`;不接实时 AI。 - **不做**:避用标、Steam 登录页、爬 Dotabuff;关系**不**写入 `shared/data/heroes.json`;不接实时 AI。
--- ---
+55 -52
View File
@@ -21,53 +21,56 @@ Dota 2 **天梯选将识别**:从选人 / 决策时间截图中识别双方 10
## 安装 ## 安装
```powershell ```powershell
pip install -r requirements.txt pip install -r pc/requirements.txt
python fetch_cdn_templates.py # 一次性:data/heroes.json(含基础属性/血蓝)+ templates/cdn/ python pc/fetch_cdn_templates.py # 一次性:shared/data/heroes.json(含基础属性/血蓝)+ pc/templates/cdn/
# 可选:从选将笔记 xlsx 导入定性克制/搭档 # 可选:从选将笔记 xlsx 导入定性克制/搭档
# python import_relations_xlsx.py path\to\dota2.xlsx # python shared/import_relations_xlsx.py path\to\dota2.xlsx
``` ```
仓库为 monorepo`pc/`(局内选将识别)、`web/`(上分帝 Web)、`shared/`(共享包)。脚本一律在仓库根执行,如 `python pc/gsi_watch.py``python web/serve_relations.py`
## 上分帝 Web(游戏外) ## 上分帝 Web(游戏外)
上分帝 Web 是独立子项目(`web/relations/`):英雄克制/搭档、排行、主播、物品、版本等内容站点,与局内选将识别解耦。 上分帝 Web 是独立子项目(`web/`):英雄克制/搭档、排行、主播、物品、版本等内容站点,与局内选将识别解耦。
```powershell ```powershell
python fetch_hero_portraits.py # 官网竖版头像 → assets/hero_portraits/ python web/fetch_hero_portraits.py # 官网竖版头像 → web/assets/hero_portraits/
python fetch_hero_items.py # OpenDota 热度 + Valve 中文名 + Steam 图标 → data/hero_items.json python web/fetch_hero_items.py # OpenDota 热度 + Valve 中文名 + Steam 图标 → web/data/hero_items.json
python fetch_hero_stats.py # OpenDota 各段位胜率/场次 → data/hero_stats.jsonWeb 走势兜底) python web/fetch_hero_stats.py # OpenDota 各段位胜率/场次 → web/data/hero_stats.jsonWeb 走势兜底)
python fetch_stratz_meta.py # STRATZ 周胜率/分路/对位 Top → stratz_hero_meta.json(需 token;见 .env.example python web/fetch_stratz_meta.py # STRATZ 周胜率/分路/对位 Top → stratz_hero_meta.json(需 token;见 .env.example
python fetch_hero_matches.py # 同英雄近期比赛出装/加点(含购买时间)→ data/hero_matches.json python web/fetch_hero_matches.py # 同英雄近期比赛出装/加点(含购买时间)→ web/data/hero_matches.json
# 既有行补时间:python fetch_hero_matches.py --enrich-item-times [--heroes juggernaut] # 既有行补时间:python web/fetch_hero_matches.py --enrich-item-times [--heroes juggernaut]
python fetch_item_shop.py # 官网商店 11 列目录 → data/item_shop.json python web/fetch_item_shop.py # 官网商店 11 列目录 → web/data/item_shop.json
python fetch_items_meta.py # 装备描述 + 机制标签 → data/items_meta.json python web/fetch_items_meta.py # 装备描述 + 机制标签 → web/data/items_meta.json
python fetch_hero_abilities.py # 英雄技能 / 驱散汇总 → data/hero_abilities.json python web/fetch_hero_abilities.py # 英雄技能 / 驱散汇总 → web/data/hero_abilities.json
python fetch_ability_videos.py # 官网技能演示 webm(限速)→ assets/ability_videos/ python web/fetch_ability_videos.py # 官网技能演示 webm(限速)→ web/assets/ability_videos/
python fetch_item_counter_stats.py # OpenDota 对阵购买率/胜率相对全局基线 → 可再生成缓存 python web/fetch_item_counter_stats.py # OpenDota 对阵购买率/胜率相对全局基线 → 可再生成缓存
python item_fears.py # 规则推导「怕的装备」→ data/hero_item_fears.json python web/item_fears.py # 规则推导「怕的装备」→ web/data/hero_item_fears.json
python fetch_patches.py # 近一年版本日志 + 详情 → data/patches.json(版本页) python web/fetch_patches.py # 近一年版本日志 + 详情 → web/data/patches.json(版本页)
python fetch_leaderboards.py # Valve Immortal 四区 Top100 → data/leaderboards.json(排行页) python web/fetch_leaderboards.py # Valve Immortal 四区 Top100 → web/data/leaderboards.json(排行页)
python fetch_streamers.py # 抖音主播主页补全 → data/streamers.json(主播页;手工名单) python web/fetch_streamers.py # 抖音主播主页补全 → web/data/streamers.json(主播页;手工名单)
python serve_relations.py # 本地开发服务 http://127.0.0.1:8765(改 JSON 后刷新 python web/fetch_streamer_live.py # 探测真实在播状态 → is_live/live_probed_at(主播页角标;--dry-run 只打印
python web/serve_relations.py # 本地开发服务 http://127.0.0.1:8765(改 JSON 后刷新)
``` ```
Web 站点带 **Hash 路由**`#/heroes/axe/core``#/heroes/axe/trends``#/heroes/axe/matchups``#/heroes/axe/matches``#/trends/legend``#/rankings``#/items/black_king_bar``#/patches/7.41``#/patches` = 最新,`#/rankings` = 中国区);英雄页的定位标签筛选与搜索框也进 URL(`?tags=核心&q=axe`)。刷新保状态、可分享 / 深度链接、浏览器前进后退还原。`python export_relations_site.py` 导出的静态站点同样支持深度链接(可部署 GitHub Pages 或 Cloudflare Pages)。 Web 站点带 **Hash 路由**`#/heroes/axe/core``#/heroes/axe/trends``#/heroes/axe/matchups``#/heroes/axe/matches``#/trends/legend``#/rankings``#/items/black_king_bar``#/patches/7.41``#/patches` = 最新,`#/rankings` = 中国区);英雄页的定位标签筛选与搜索框也进 URL(`?tags=核心&q=axe`)。刷新保状态、可分享 / 深度链接、浏览器前进后退还原。`python web/export_relations_site.py` 导出的静态站点同样支持深度链接(可部署 GitHub Pages 或 Cloudflare Pages)。
### 部署到 Cloudflare Pages ### 部署到 Cloudflare Pages
```powershell ```powershell
python deploy_relations.py python web/deploy_relations.py
# 串联:导出 → 资产完整性预检 → wrangler 直传 → 绑定自定义域名 # 串联:导出 → 资产完整性预检 → wrangler 直传 → 绑定自定义域名
# 凭据经 keyzoo 注入(refining/cloudflare 的 Global API Key); # 凭据经 keyzoo 注入(refining/cloudflare 的 Global API Key);
# 或手动设 $env:CLOUDFLARE_EMAIL / $env:CLOUDFLARE_API_KEY 后运行 # 或手动设 $env:CLOUDFLARE_EMAIL / $env:CLOUDFLARE_API_KEY 后运行
# 默认项目 climperor-relations、域名 dota2.refining.dev(可用 --project-name / --domain 覆盖) # 默认项目 climperor-relations、域名 dota2.refining.dev(可用 --project-name / --domain 覆盖)
# 技能演示视频与静态图标默认走阿里云 OSS 桶 climperorPages 只部署 ~2MB HTML/JS/data # 技能演示视频与静态图标默认走阿里云 OSS 桶 climperorPages 只部署 ~2MB HTML/JS/data
python _oss_static_assets.py upload # patch/新英雄后同步图标到 OSS python web/_oss_static_assets.py upload # patch/新英雄后同步图标到 OSS
python _cf_status.py # 部署后只读核对(项目 / 部署 / 域名状态) python web/_cf_status.py # 部署后只读核对(项目 / 部署 / 域名状态)
``` ```
### 定时刷新(Gitea Actions ### 定时刷新(Gitea Actions
仓库 [`.gitea/workflows/`](.gitea/workflows/) 在 **self-hosted** runner(与 `site-traffic-notify` 同一 Mac mini)上分层拉数;`refresh_web.py` 比对 JSON/图标摘要,**有变更才** `_oss_static_assets.py upload` + `deploy_relations.py`。数据-only 刷新**不** bump `SITE_VERSION``data.json` / `index.html` 已不缓存)。 仓库 [`.gitea/workflows/`](.gitea/workflows/) 在 **self-hosted** runner(与 `site-traffic-notify` 同一 Mac mini)上分层拉数;`web/refresh_web.py` 比对 JSON/图标摘要,**有变更才** `_oss_static_assets.py upload` + `deploy_relations.py`。数据-only 刷新**不** bump `SITE_VERSION``data.json` / `index.html` 已不缓存)。
| Workflow | 时间(CST | 档位 | 拉取内容 | | Workflow | 时间(CST | 档位 | 拉取内容 |
|----------|-------------|------|----------| |----------|-------------|------|----------|
@@ -75,17 +78,17 @@ python _cf_status.py # 部署后只读核对(项目 / 部署 / 域名状态
| `web-weekly` | 周二 07:00 | `weekly` | `stratz_meta``hero_items``items_meta`、OpenDota 对阵装备证据、`item_fears` | | `web-weekly` | 周二 07:00 | `weekly` | `stratz_meta``hero_items``items_meta`、OpenDota 对阵装备证据、`item_fears` |
| `web-patch` | 每 6 小时 | `patch` | `fetch_patches.py --check`;仅 `has_new` 时拉详情 + abilities/商店/items_meta/fears | | `web-patch` | 每 6 小时 | `patch` | `fetch_patches.py --check`;仅 `has_new` 时拉详情 + abilities/商店/items_meta/fears |
**版本检测:** `--check` 只请求 Valve 版本列表,与本地 `data/patches.json``details` 比对,stdout 一行 JSON`has_new` / `new_versions` / `latest`)。无新版本则跳过详情与部署。 **版本检测:** `--check` 只请求 Valve 版本列表,与本地 `web/data/patches.json``details` 比对,stdout 一行 JSON`has_new` / `new_versions` / `latest`)。无新版本则跳过详情与部署。
**本机试跑:** **本机试跑:**
```powershell ```powershell
pip install -r requirements.txt -r requirements-web.txt pip install -r web/requirements.txt
python fetch_patches.py --check python web/fetch_patches.py --check
python refresh_web.py --tier patch --dry-run python web/refresh_web.py --tier patch --dry-run
python refresh_web.py --tier patch --skip-deploy --skip-oss python web/refresh_web.py --tier patch --skip-deploy --skip-oss
# 真实拉数+部署(需 CF / OSS env 或 keyzoo): # 真实拉数+部署(需 CF / OSS env 或 keyzoo):
# python refresh_web.py --tier daily # python web/refresh_web.py --tier daily
``` ```
**Runner 要求:** `python3`(或 `python`)、`node`/`npx`wrangler)、出网访问 OpenDota / STRATZ / Valve / Cloudflare / 阿里云 OSS。 **Runner 要求:** `python3`(或 `python`)、`node`/`npx`wrangler)、出网访问 OpenDota / STRATZ / Valve / Cloudflare / 阿里云 OSS。
@@ -94,32 +97,32 @@ python refresh_web.py --tier patch --skip-deploy --skip-oss
| Secret | 用途 | | Secret | 用途 |
|--------|------| |--------|------|
| `CLOUDFLARE_EMAIL` / `CLOUDFLARE_API_KEY` | 部署 Pages(与 `site-traffic-notify` 共用;可用 `_gitea_actions_secrets.py` 写入) | | `CLOUDFLARE_EMAIL` / `CLOUDFLARE_API_KEY` | 部署 Pages(与 `site-traffic-notify` 共用;可用 `web/_gitea_actions_secrets.py` 写入) |
| `STRATZ_API_TOKEN` | `web-weekly`(及 daily 内若触发连带时不需要) | | `STRATZ_API_TOKEN` | `web-weekly`(及 daily 内若触发连带时不需要) |
| `OSS_ACCESS_KEY_ID` / `OSS_ACCESS_KEY_SECRET` | 图标变更时上传 OSS | | `OSS_ACCESS_KEY_ID` / `OSS_ACCESS_KEY_SECRET` | 图标变更时上传 OSS |
手工数据(`relations.json`、overrides)与技能视频(GB 级)**不**进定时;新英雄仍需本机 `fetch_cdn_templates.py` + `fetch_hero_portraits.py`。冒烟:Gitea Actions 对 `web-patch`**Run workflow** 手工数据(`shared/data/relations.json`、overrides)与技能视频(GB 级)**不**进定时;新英雄仍需本机 `pc/fetch_cdn_templates.py` + `web/fetch_hero_portraits.py`。冒烟:Gitea Actions 对 `web-patch`**Run workflow**
仿局内选将四列网格(图标来自 [dota2.com/heroes](https://www.dota2.com/heroes) 竖版裁切):点英雄高亮克制 / 被克制 / 搭档,下方显示定位,并以标签页切换 **技能**(可点选魔晶/神杖升级与天赋树)、**核心装备**、**被克装备**、**走势**(STRATZ 优先:勋章条 + 最近 1 周三卡 + 分路 + 近 8 周列表;OpenDota 兜底)、**对位**STRATZ 克制/被克/搭档 Top)、**近期比赛**(终局出装/加点)、**主播**(常玩该英雄的收录主播)、**版本变更**(该英雄近一年各版本改动)。 仿局内选将四列网格(图标来自 [dota2.com/heroes](https://www.dota2.com/heroes) 竖版裁切):点英雄高亮克制 / 被克制 / 搭档,下方显示定位,并以标签页切换 **技能**(可点选魔晶/神杖升级与天赋树)、**核心装备**、**被克装备**、**走势**(STRATZ 优先:勋章条 + 最近 1 周三卡 + 分路 + 近 8 周列表;OpenDota 兜底)、**对位**STRATZ 克制/被克/搭档 Top)、**近期比赛**(终局出装/加点)、**主播**(常玩该英雄的收录主播)、**版本变更**(该英雄近一年各版本改动)。
英雄页上方网格固定高度;点选技能 / 核心装备 / 被克装备后,详情显示在下方(装备含合成,非弹窗)。 英雄页上方网格固定高度;点选技能 / 核心装备 / 被克装备后,详情显示在下方(装备含合成,非弹窗)。
顶部另有 **排行** 页:Valve Immortal 四区榜各 Top 100(中国 / 欧洲 / 美洲 / 东南亚);数据来自 `data/leaderboards.json``python fetch_leaderboards.py`)。 顶部另有 **排行** 页:Valve Immortal 四区榜各 Top 100(中国 / 欧洲 / 美洲 / 东南亚);数据来自 `web/data/leaderboards.json``python web/fetch_leaderboards.py`)。
顶部另有 **主播** 页:手工收录直播间/主页(首版仅抖音),抖音式卡片展示昵称、签名、关注/粉丝/获赞、常用英雄与可选精选高光视频(滚入视口中部自动播放;有 `live_url` 时点头像进直播间并显示直播动效;**角标不代表真实在播**,待接入 `is_live`);数据来自 `data/streamers.json``python fetch_streamers.py` 补全;亦由 `refresh_web --tier daily` 每日软失败刷新)。视频文件放 `assets/streamer_videos/`gitignore),本地开发经 `/streamer-video/` 提供,部署前用 `_oss_static_assets.py upload` 同步至 OSS `streamer-video/` 顶部另有 **主播** 页:手工收录直播间/主页(首版仅抖音),抖音式卡片展示昵称、签名、关注/粉丝/获赞、常用英雄与可选精选高光视频(滚入视口中部自动播放;有 `live_url` 时点头像进直播间;「直播」角标与动效只在真实探测在播时显示(线上由访客触发的 `/api/live-status` 边缘函数实时刷新,边缘缓存 5 分钟合并请求;`data.json``fetch_streamer_live.py` daily 探测值兜底);数据来自 `web/data/streamers.json``python web/fetch_streamers.py` 补全;亦由 `web/refresh_web --tier daily` 每日软失败刷新)。视频文件放 `web/assets/streamer_videos/`gitignore),本地开发经 `/streamer-video/` 提供,部署前用 `web/_oss_static_assets.py upload` 同步至 OSS `streamer-video/`
顶部另有 **走势** 页:近 8 周各段位高胜率 / 上场率榜(`#/trends[/bracket]`STRATZ 周胜率数据)。 顶部另有 **走势** 页:近 8 周各段位高胜率 / 上场率榜(`#/trends[/bracket]`STRATZ 周胜率数据)。
顶部另有 **物品** 页:对齐 [官网商店物品](https://www.dota2.com.cn/items/index.htm) 的 **11 列竖排**(基础分类 5 列 + 合成分类 6 列),点选后详情显示在右侧(含合成组件)。目录来自 `data/item_shop.json``python fetch_item_shop.py`)。 顶部另有 **物品** 页:对齐 [官网商店物品](https://www.dota2.com.cn/items/index.htm) 的 **11 列竖排**(基础分类 5 列 + 合成分类 6 列),点选后详情显示在右侧(含合成组件)。目录来自 `web/data/item_shop.json``python web/fetch_item_shop.py`)。
顶部另有 **版本** 页:默认展示最新版本完整改动(综合 / 物品 / 中立物品 / 英雄),下拉切换近一年其它版本;数据来自 `data/patches.json``python fetch_patches.py`,近一年窗口可用 `--days` / `--since` 调)。 顶部另有 **版本** 页:默认展示最新版本完整改动(综合 / 物品 / 中立物品 / 英雄),下拉切换近一年其它版本;数据来自 `web/data/patches.json``python web/fetch_patches.py`,近一年窗口可用 `--days` / `--since` 调)。
数据在 `data/relations.json`(英雄关系,不用胜率);选将全网格「克/搭/补」推荐读同一份文件,并用 `draft_archetypes.py` 规则识别推进/全球流与敌我缺口(不接 AI)。热门装备来自 OpenDota 统计。**走势**优先 STRATZ`data/stratz_hero_meta.json`,按勋章近 8 周 + 最近 1 周分路;`python fetch_stratz_meta.py`,需 token),OpenDota 各段位胜率/上场率/场次在 `data/hero_stats.json`(近约 7 天兜底;不进推荐)。**对位**数值 Top 在 `data/stratz_matchup_tops.json`(与网格定性克/搭分开展示)。被克装备以描述标签 + 规则为主;`fetch_item_counter_stats.py` 另算敌方终局装备相对同装备全局基线的购买率提升/胜率差,对全部英雄的已有候选小幅调序;装备卡有数据时显示对阵该英雄的敌方队伍终局装备出现率。统计显著的新组合仍须确认机制成立后写入 `hero_fear_overrides.json`,避免“高相关但不克制”的误报。可改 `data/item_tag_overrides.json` 后重跑 `fetch_items_meta.py``item_fears.py`。**尚无对局内出装推荐**。 数据在 `shared/data/relations.json`(英雄关系,不用胜率);选将全网格「克/搭/补」推荐读同一份文件,并用 `pc/draft_archetypes.py` 规则识别推进/全球流与敌我缺口(不接 AI)。热门装备来自 OpenDota 统计。**走势**优先 STRATZ`web/data/stratz_hero_meta.json`,按勋章近 8 周 + 最近 1 周分路;`python web/fetch_stratz_meta.py`,需 token),OpenDota 各段位胜率/上场率/场次在 `web/data/hero_stats.json`(近约 7 天兜底;不进推荐)。**对位**数值 Top 在 `web/data/stratz_matchup_tops.json`(与网格定性克/搭分开展示)。被克装备以描述标签 + 规则为主;`web/fetch_item_counter_stats.py` 另算敌方终局装备相对同装备全局基线的购买率提升/胜率差,对全部英雄的已有候选小幅调序;装备卡有数据时显示对阵该英雄的敌方队伍终局装备出现率。统计显著的新组合仍须确认机制成立后写入 `web/data/hero_fear_overrides.json`,避免“高相关但不克制”的误报。可改 `web/data/item_tag_overrides.json` 后重跑 `web/fetch_items_meta.py``web/item_fears.py`。**尚无对局内出装推荐**。
## 快速开始(GSI 自动跟踪) ## 快速开始(GSI 自动跟踪)
```powershell ```powershell
python gsi_setup.py # 写入 Dota GSI 配置 python pc/gsi_setup.py # 写入 Dota GSI 配置
# Steam → Dota 2 → 属性 → 启动项加上 -gamestateintegration,重启游戏 # Steam → Dota 2 → 属性 → 启动项加上 -gamestateintegration,重启游戏
python gsi_watch.py python pc/gsi_watch.py
``` ```
进入英雄选择后自动跟踪;敌方锁 ≥1 人后按关系与阵容画像在英雄网格标出 **克 / 搭 / 补**,并显示一句阵容分析(需 `data/relations.json`;定位局按分路过滤)。凑齐 10 人或超时后写出 `results/draft_<时间戳>.json`(该目录已 gitignore)。 进入英雄选择后自动跟踪;敌方锁 ≥1 人后按关系与阵容画像在英雄网格标出 **克 / 搭 / 补**,并显示一句阵容分析(需 `shared/data/relations.json`;定位局按分路过滤)。凑齐 10 人或超时后写出 `pc/results/draft_<时间戳>.json`(该目录已 gitignore)。
会话截图按对局落在 `samples/raw/<matchid>/`;默认同时把每包 GSI JSON 追加到同目录 `gsi.jsonl``--no-dump-gsi` 可关)。 会话截图按对局落在 `pc/samples/raw/<matchid>/`;默认同时把每包 GSI JSON 追加到同目录 `gsi.jsonl``--no-dump-gsi` 可关)。
> 主菜单收不到 GSI 是正常的:客户端**第一次载入对局后**才开始推送。看到 `[gsi] connected` 才算链路通。 > 主菜单收不到 GSI 是正常的:客户端**第一次载入对局后**才开始推送。看到 `[gsi] connected` 才算链路通。
@@ -128,30 +131,30 @@ python gsi_watch.py
### 截图 ### 截图
```powershell ```powershell
python capture.py # 单张 → samples/raw/ python pc/capture.py # 单张 → pc/samples/raw/
python capture.py --loop 300 3 # 每 3 秒一张,持续 300 秒 python pc/capture.py --loop 300 3 # 每 3 秒一张,持续 300 秒
``` ```
GSI 跟踪时自动写入 `samples/raw/<matchid>/draft_*.png`(含 milestone / best)。 GSI 跟踪时自动写入 `pc/samples/raw/<matchid>/draft_*.png`(含 milestone / best)。
需无边框窗口或窗口模式(独占全屏可能黑帧)。 需无边框窗口或窗口模式(独占全屏可能黑帧)。
### 标定 ROI(一次性) ### 标定 ROI(一次性)
```powershell ```powershell
python autocalibrate.py samples/raw/<matchid>/<某帧>.png python pc/autocalibrate.py pc/samples/raw/<matchid>/<某帧>.png
# 失败时退路:python calibrate.py samples/raw/<matchid>/<某帧>.png # 失败时退路:python pc/calibrate.py pc/samples/raw/<matchid>/<某帧>.png
``` ```
### 识别 / 评测 ### 识别 / 评测
```powershell ```powershell
python recognize.py samples/raw/<matchid>/<>.png python pc/recognize.py pc/samples/raw/<matchid>/<>.png
python recognize.py samples/raw/<matchid>/<>.png --sheet python pc/recognize.py pc/samples/raw/<matchid>/<>.png --sheet
python recognize.py samples/raw/<matchid>/<>.png --truth hero1,...,hero10 python pc/recognize.py pc/samples/raw/<matchid>/<>.png --truth hero1,...,hero10
python evaluate.py # 按 samples/labels.json 批量评测 python pc/evaluate.py # 按 pc/samples/labels.json 批量评测
``` ```
辅助脚本:`roles.py`(位置字)、`grid.py`(禁用名单)、`modes.py`(模式字)。 辅助脚本:`pc/roles.py`(位置字)、`shared/grid.py`(禁用名单)、`pc/modes.py`(模式字)。
## 截图要求 ## 截图要求

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

+4 -1
View File
@@ -12,9 +12,12 @@ Writes slot geometry into config.json and preview/autocalibrate_check.png.
""" """
import sys import sys
import time
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import time
import cv2 import cv2
import numpy as np import numpy as np
+4
View File
@@ -11,6 +11,10 @@ and stored as resolution-independent relative coordinates.
""" """
import sys import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import time import time
import cv2 import cv2
+5 -2
View File
@@ -10,11 +10,14 @@ Notes:
- Frames are saved as PNG at native resolution, named cap_HHMMSS.png. - Frames are saved as PNG at native resolution, named cap_HHMMSS.png.
""" """
import json
import sys import sys
import time
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import json
import time
import cv2 import cv2
import mss import mss
import numpy as np import numpy as np
+16 -26
View File
@@ -1,36 +1,26 @@
"""Shared helpers: config IO, slot geometry, crop preprocessing.""" """PC-side helpers: config IO, slot geometry, crop preprocessing, NCC matching.
ROOT is the pc/ directory: every runtime path built from it (samples/,
preview/, results/, failures/, templates/, assets/role_icons/) stays inside
the PC subproject. Shared locations (heroes.json, CDN templates) are
re-exported from shared.paths so existing ``from common import X`` call
sites keep working.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import json import json
from pathlib import Path
import cv2 import cv2
import numpy as np import numpy as np
ROOT = Path(__file__).parent from shared.paths import HEROES_JSON, TEMPLATES_CDN # noqa: F401 (re-export)
ROOT = Path(__file__).resolve().parent
CONFIG_PATH = ROOT / "config.json" CONFIG_PATH = ROOT / "config.json"
DATA = ROOT / "data"
HEROES_JSON = DATA / "heroes.json"
TEMPLATES_CDN = ROOT / "templates" / "cdn"
# Landscape cards from dota2.com/heroes (Steam CDN heroes/{key}.png); UI only.
HERO_PORTRAITS = ROOT / "assets" / "hero_portraits"
# Primary-attribute icons from dota2.com (dota_react/icons/hero_*.png); UI only.
ATTR_ICONS = ROOT / "assets" / "attr_icons"
# Item icons from Steam CDN (dota_react/items/{key}.png); Climperor web site only.
ITEM_ICONS = ROOT / "assets" / "item_icons"
# Shop category header icons from dota2.com.cn/items/images/itemcat_*.png.
ITEM_CAT_ICONS = ROOT / "assets" / "item_cat_icons"
# Ability icons from Steam CDN (dota_react/abilities/{key}.png); Climperor web site only.
ABILITY_ICONS = ROOT / "assets" / "ability_icons"
# Generic UI glyphs from dota2.com.cn (herostatic/icons/*.png); Climperor web site only.
UI_ICONS = ROOT / "assets" / "ui_icons"
# Rank medal icons (OpenDota rank_icon_0..8); Climperor web trends tab only.
RANK_ICONS = ROOT / "assets" / "rank_icons"
# Streamer avatars cached by fetch_streamers.py; Climperor web「主播」only.
STREAMER_AVATARS = ROOT / "assets" / "streamer_avatars"
# Curated Douyin highlight clips for streamer cards; Climperor web「主播」only.
STREAMER_VIDEOS = ROOT / "assets" / "streamer_videos"
# Official ability demo clips from dota2.com (dota_react/abilities/{hero}/{ability}.webm).
ABILITY_VIDEOS = ROOT / "assets" / "ability_videos"
def load_config() -> dict: def load_config() -> dict:
+1 -1
View File
@@ -108,7 +108,7 @@
"min_enemies": 1, "min_enemies": 1,
"min_heroes_for_gaps": 2, "min_heroes_for_gaps": 2,
"archetypes": true, "archetypes": true,
"relations_path": "data/relations.json", "relations_path": "shared/data/relations.json",
"role_tags": { "role_tags": {
"1": ["Carry"], "1": ["Carry"],
"2": ["Carry", "Nuker", "Escape"], "2": ["Carry", "Nuker", "Escape"],
@@ -6,10 +6,15 @@ and short Chinese analysis / reason strings for recommend marks.
from __future__ import annotations from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from collections import Counter from collections import Counter
from typing import Iterable from typing import Iterable
from hero_tags import TAG_ORDER from shared.hero_tags import TAG_ORDER
# Strong push cores: one hit can flag push even before 2+ Pusher tags. # Strong push cores: one hit can flag push even before 2+ Pusher tags.
PUSH_CORE = frozenset({ PUSH_CORE = frozenset({
+7 -2
View File
@@ -30,12 +30,17 @@ Skinned portraits are not templated (too many variants). Instead:
Once ten heroes are confirmed, vision stops and only GSI is waited on for self. Once ten heroes are confirmed, vision stops and only GSI is waited on for self.
""" """
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import time import time
import mss import mss
from capture import grab_frame, is_dota_foreground, raw_dir_for_match, save_frame from capture import grab_frame, is_dota_foreground, raw_dir_for_match, save_frame
from grid import bans, hero_table, read_grid from shared.grid import bans, hero_table, read_grid
from modes import detect_mode, load_mode_templates from modes import detect_mode, load_mode_templates
from recognize import recognize_image from recognize import recognize_image
from recommend import ally_keys, enemy_keys, load_relations, suggest_marks from recommend import ally_keys, enemy_keys, load_relations, suggest_marks
@@ -136,7 +141,7 @@ class DraftSession:
rel = self.relations or {} rel = self.relations or {}
if not rel.get("counters") and not rel.get("synergies"): if not rel.get("counters") and not rel.get("synergies"):
self._rec_warned = True self._rec_warned = True
self.log("[rec] relations empty — edit data/relations.json " self.log("[rec] relations empty — edit shared/data/relations.json "
"or run import_relations_xlsx.py") "or run import_relations_xlsx.py")
started = time.monotonic() started = time.monotonic()
pending: dict[int, tuple[str, int]] = {} pending: dict[int, tuple[str, int]] = {}
+5 -1
View File
@@ -7,8 +7,12 @@ Usage:
python evaluate.py python evaluate.py
""" """
import json
import sys import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import json
import cv2 import cv2
@@ -4,21 +4,25 @@ Usage:
python fetch_cdn_templates.py python fetch_cdn_templates.py
Writes: Writes:
data/heroes.json - hero id / key / English name / roles / aliases / base stats shared/data/heroes.json - hero id / key / English name / roles / aliases / base stats
templates/cdn/{key}.png - face-centered square crop resized to canonical size templates/cdn/{key}.png - face-centered square crop resized to canonical size
Preserves manually curated `aliases` / `abbr` from an existing heroes.json on rewrite. Preserves manually curated `aliases` / `abbr` from an existing heroes.json on rewrite.
""" """
import json import sys
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import json
import cv2 import cv2
import numpy as np import numpy as np
from common import HEROES_JSON, TEMPLATES_CDN, load_config from common import HEROES_JSON, TEMPLATES_CDN, load_config
from hero_tags import tags_for_hero from shared.hero_tags import tags_for_hero
from http_utils import fetch_hero_list, http_bytes, http_json from shared.http_utils import fetch_hero_list, http_bytes, http_json
# Steam CDN landscape hero art; cropped to a face window for top-bar matching. # Steam CDN landscape hero art; cropped to a face window for top-bar matching.
IMG_URL = "https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/heroes/{key}.png" IMG_URL = "https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/heroes/{key}.png"
+4 -1
View File
@@ -10,10 +10,13 @@ After installing, add -gamestateintegration to Dota 2's launch options
(Steam library -> right-click Dota 2 -> Properties) and restart the game. (Steam library -> right-click Dota 2 -> Properties) and restart the game.
""" """
import re
import sys import sys
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import re
from common import load_config from common import load_config
CFG_NAME = "gamestate_integration_climperor.cfg" CFG_NAME = "gamestate_integration_climperor.cfg"
+5 -2
View File
@@ -21,12 +21,15 @@ With gsi.dump_payloads (default on), every POST body is appended to
samples/raw/<matchid>/gsi.jsonl for later analysis. samples/raw/<matchid>/gsi.jsonl for later analysis.
""" """
import json
import sys import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import json
import threading import threading
import time import time
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
import mss import mss
+3
View File
@@ -1,7 +1,10 @@
"""Read the game-mode label under the top-center timer (e.g. 全英雄选择).""" """Read the game-mode label under the top-center timer (e.g. 全英雄选择)."""
import sys
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import cv2 import cv2
import numpy as np import numpy as np
+5 -1
View File
@@ -6,8 +6,12 @@ set_grid_marks(), and set_analysis(); geometry uses relative coords.
from __future__ import annotations from __future__ import annotations
import json
import sys import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import json
import threading import threading
import tkinter as tk import tkinter as tk
+5 -1
View File
@@ -12,8 +12,12 @@ misrecognized crops to failures/ (debug only, gitignored).
recognize_image() is the reusable entry point used by gsi_watch.py. recognize_image() is the reusable entry point used by gsi_watch.py.
""" """
import json
import sys import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import json
import time import time
import cv2 import cv2
+9 -4
View File
@@ -8,11 +8,16 @@ Mark available heroes with 克 / 搭 / 补:
Role-queue filters by position tags; otherwise all heroes are candidates. Role-queue filters by position tags; otherwise all heroes are candidates.
Also returns a short analysis string and per-mark reasons (no AI). Also returns a short analysis string and per-mark reasons (no AI).
Data: data/relations.json + draft_archetypes rules. Data: shared/data/relations.json + draft_archetypes rules.
""" """
from __future__ import annotations from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from draft_archetypes import ( from draft_archetypes import (
answer_for_candidate, answer_for_candidate,
collect_reasons, collect_reasons,
@@ -21,9 +26,9 @@ from draft_archetypes import (
format_analysis, format_analysis,
tag_profile, tag_profile,
) )
from grid import hero_table from shared.grid import hero_table
from hero_tags import tags_for_hero from shared.hero_tags import tags_for_hero
from relations import DEFAULT_RELATIONS, indexes, load_relations from shared.relations import DEFAULT_RELATIONS, indexes, load_relations
DEFAULT_ROLE_TAGS = { DEFAULT_ROLE_TAGS = {
"1": ["Carry"], "1": ["Carry"],
+5
View File
@@ -9,6 +9,11 @@ value+saturation isolates it cleanly. Matching is done on the binary mask
they differ in width, so mask IoU separates them by a wide margin. they differ in width, so mask IoU separates them by a wide margin.
""" """
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import cv2 import cv2
import numpy as np import numpy as np

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Some files were not shown because too many files have changed in this diff Show More