# 上分帝(Climperor)—— 方案与实施细节 `README.md` 是操作手册;本文记录背景、选型与关键决策。协作约束见 `AGENTS.md`。上分帝 Web 视觉设计规范见 `DESIGN.md`。 --- ## 1. 背景与目标 ### 要解决的问题 在 Dota 2 选将阶段(以及进入游戏后)**几秒内自动获取双方 10 个英雄**,服务天梯选将辅助或赛后分析。 ### 为什么 GSI 做不到 | 场景 | GSI 可获得的阵容数据 | |------|---------------------| | 排位 / 普通 All Pick | 仅自己的 `hero.id`;`draft` 通常为空 | | Captains Mode | 历史上有部分 pick/ban,不稳定 | | 观战 / 裁判视角 | 阵容字段较全 | | 赛后 | 需依赖 OpenDota 等外部 API | Valve 已明确关闭普通玩家视角的实时 draft ([#9562](https://github.com/ValveSoftware/Dota2-Gameplay/issues/9562)、 [#7193](https://github.com/ValveSoftware/Dota2-Gameplay/issues/7193),not planned)。 ### 候选方案 | 方案 | 结论 | |------|------| | 官方 GSI | 拿不到双方 pick | | Overwolf GEP | 备选,偏重 | | 读游戏内存 | **排除**(合规) | | 截屏 + 模板匹配 | **选定** | | 整图问多模态大模型 | 排除为主路径(实测整图几乎全错;裁顶栏后仍不如白名单匹配) | 社区同类工具共同点:**裁固定 ROI + 模板匹配 / 小模型**,输出约束在英雄白名单内。 --- ## 2. 技术方案 ### 处理流程 ``` 截图 → 相对坐标裁 10 格 →(天梯则遮罩段位条)→ CDN NCC 匹配 → score + margin 双门控 → hero_key | null ``` ### 关键决策 **① 相对坐标** 横向 `(cx - W/2) / H`,纵向与宽高 `/ H`。同宽高比下分辨率无关;21:9 等需锚点或重标定。 **② 单一 CDN 模板库** `templates/cdn/{hero}.png` 来自 Steam 官方头像,按顶栏实际窗口裁切后写入。 曾尝试「CDN 兜底 + real 实拍」双层库。实测修正 CDN 裁切后 **CDN-only 即可全对**, 继续攒 real 库性价比低,已放弃(见 §3 发现二)。皮肤靠会话策略,不堆变体模板。 **③ 宁可不认,不可乱认** - `score >= min_score`(默认 0.45) - `margin = Top1 − Top2 >= min_margin`(默认 0.04) **④ 会话层处理皮肤与淡入** - 选人阶段:多帧确认、允许更高分改判(`revise_gain`) - 决策阶段:只补空槽、禁止改判(避免皮肤顶栏覆盖默认脸结论) - best 帧优先选人阶段默认脸(`recognized` 相同时) - 本人英雄以 GSI `hero` 覆盖视觉结果(只覆盖自己的槽) **⑤ 截图按对局归档** GSI 会话写入 `samples/raw/{matchid}/`;手动 `capture.py` 仍写 `samples/raw/` 根目录。 结果 JSON 仍为 `results/draft_<时间戳>.json`(内含 `match_id`)。 **⑥ GSI 全量落盘** `gsi.dump_payloads`(默认开)把每包 POST body 追加到 `samples/raw/{matchid}/gsi.jsonl`, 行格式 `{"t": , "payload": <原文>}`。内容上限仍是 cfg 订阅字段 + 普通玩家视角; 不能靠落盘补出双方 pick。CLI:`--dump-gsi` / `--no-dump-gsi`。 --- ## 3. 关键实测(2026-07-25) 环境:2560×1440 无边框;人机 + 天梯。 ### 发现一:CDN 裁切窗口决定上限 初版按正方形取中心,比例与顶栏 111×88 不符,均分约 0.65。 网格搜索得到正确窗口 **x0=38, w=182, 全高**(源 256×144),均分约 0.94。 写入 `fetch_cdn_templates.py` 的 `CROP_X0/CROP_X1` 后,**CDN-only 30/30 / 40/40**。 > 兜底层差时先查素材处理,再考虑堆数据。 ### 发现二:不必维护 real 库 CDN-only 开箱即用后,「打几十局攒默认脸」不再是主路径。项目改为只维护 CDN。 后续审计(迁出前原型库)进一步确认: - 标注集上大量 `score=1.000` 往往是「从同帧裁进 real 再评同帧」的自证,夸大了 real 贡献。 - 剩余 real 与 CDN(天梯遮罩)多数 ≥0.85,近乎 CDN 副本;明显偏离的极少(如军团指挥官一类边缘格)。 - **近期问题局**(宙斯缺槽、斯拉达 CDN 分低、敌法末位、皮肤帧风行者 0.415)主要靠 **多帧会话 + 决策禁改判 + GSI 补自己** 凑齐,不是靠 real。 - 皮肤变体极多,入库皮肤模板不合理;段位条/徽章每人不同,匹配时已用遮罩忽略, 也不该为不同段位重复存图。 ### 发现三:自动标定靠玩家颜色条 10 条固定玩家色定位槽位;中心用中位数槽距拟合(剔除被身后头像污染的宽条); 头像下沿用「格内列 vs 格间空隙」亮度差,勿用逐行差分(易误判到名字行)。 ### 发现四:天梯段位条用遮罩 GSI 不含可靠 lobby 类型时,用右下金色勋章检测(`has_ranked_overlay`), 匹配时屏蔽底部 32% + 右侧 22%。人机无勋章走全图匹配。 ### 发现五:禁用名单不靠模板 网格按主属性分块 + 客户端本地化名行优先排布;禁用/已选卡片对比度塌陷 (std 约 8–21 vs 正常 ≥33)。`data/heroes.json` 需含 `attr` / `name_loc`; 另含手工维护的中文口语 `aliases`(重拉 CDN 表时合并保留,暂未接入推荐展示)。 格数对不上时 `ok=False`,不返回残缺名单。 ### 发现六:位置与「我」 定位局位置字用二值 IoU(非 OCR)。「我」优先 GSI `team_slot`; 否则姓名亮度相对差(最亮比次亮 ≥ 25)兜底。 ### 发现七:客户端顶栏时机(会话策略前提) 这些是实战纠正后的事实,改 `DraftSession` 时不要违背: | 现象 | 含义 | |------|------| | 顶栏默认脸 → 皮肤 | **全员选完后**才换皮肤立绘,不是一进决策就换 | | 本机已进 `STRATEGY_TIME` | 别人可能还在选;**不能**一进决策就永久停视觉 | | 空槽红旗倒计时 | 只出现在**未选**槽;揭晓后消失,不是挡脸主因 | | GSI | **没有**「皮肤已加载」字段;无法精确卡「人选完、皮肤前」单帧 | 因此正确节奏是:选人 + 决策前期持续读,直到确认 10/10 或 `strategy_tail_polls` 用尽; 凑齐后停视觉,再短等 GSI 补本人英雄。单帧皮肤弱识别(如决策 3D 模型挡顶栏)属预期, 靠会话回填,不靠降阈值或堆皮肤模板。 ### 发现八:揭晓淡入需要可改判 首帧揭晓常半透明 + 段位条,易误认(例:剑圣揭晓瞬间被认成幽鬼)。 `confirm_polls` 后若仍禁止改判会锁死错误。选人阶段允许 `score ≥ was_score + revise_gain` 的稳定新结果覆盖;决策阶段则关闭改判。 --- ## 4. 代码结构 ``` climperor/ ├── pc/ # 局内选将识别(GSI + 截屏 + OpenCV) │ ├── common.py / recognize.py / draft_session.py / gsi_watch.py │ ├── recommend.py / item_suggest.py / draft_archetypes.py / roles.py / modes.py / overlay.py │ ├── config.json / templates/ / assets/role_icons/ / samples/ │ └── requirements.txt ├── web/ # 上分帝 Web(前端、数据流水线、部署) │ ├── frontend/ # 原 web/relations/ │ ├── data/ / assets/ / dist/ │ ├── fetch_*.py / item_fears.py / mechanic_tags.py / loc_format.py │ ├── serve_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_stratz.py │ └── data/heroes.json / data/relations.json ├── .gitea/workflows/ # site-traffic-notify + web-daily/weekly/patch └── README.md / AGENTS.md / ARCHITECTURE.md / DESIGN.md / CHANGELOG.md ``` 运行时目录 `pc/preview/`、`pc/results/`、`pc/failures/`、`pc/samples/raw/`(含按对局子目录)不入库。上分帝 Web 易变数据(STRATZ meta、hero_stats 等)可由 Gitea Actions 定时拉取后直部 Pages,不必入库。 ### 主要配置 | 字段 | 含义 | 默认 | |------|------|------| | `match.min_score` / `min_margin` | 识别门控 | 0.45 / 0.04 | | `match.ranked_mask` | 天梯遮罩比例 | bottom 0.32, right 0.22 | | `gsi.port` | 监听端口 | 3223 | | `gsi.confirm_polls` | 连续同结果帧数 | 2 | | `gsi.revise_gain` | 选人阶段改判所需分数增益 | 0.15 | | `gsi.strategy_tail_polls` | 决策阶段继续视觉轮询 | 8 | | `gsi.strategy_gsi_wait` | 视觉结束后等待本人 GSI 英雄 | 3.0 | | `gsi.require_foreground` | 仅当前台为 `dota2.exe` 时截屏识别 | true | | `gsi.dump_payloads` | 全量 GSI JSONL 落盘 | true | | `recommend.enabled` | 选将全网格克/搭/补推荐 | true | | `recommend.top_n` | 标记数量上限;`≤0` 不截断 | 0 | | `recommend.min_enemies` | 开始推荐所需敌方锁人数 | 1 | | `recommend.min_heroes_for_gaps` | 缺口/补位注入所需锁人数 | 2 | | `recommend.archetypes` | 推进/全球流/缺口规则画像 | true | | `recommend.relations_path` | 定性关系文件 | `shared/data/relations.json` | | `recommend.role_tags` | 分路→角色标签过滤(1–5 号位;非定位局不过滤) | 见 config.json | | `recommend.items.*` | 锁定后核心装+应对装(`hero_items` + 定性规则) | 见 config.json | | `overlay.enabled` | 选将网格「克/搭/补」+ 分析条 + 装备图标条 | true | | `overlay.mark_size_rel` / `mark_pad_rel` / `mark_gap_rel` | 网格克/搭/补方标尺寸、内边距、间距 | 0.018 / 0.004 / 0.002 | | `overlay.counter_color` / `synergy_color` / `fill_color` / `mark_text_color` | 克 / 搭 / 补 / 文字色 | `#2ec4b6` / `#e9a825` / `#9b7ebd` / `#0b1220` | | `overlay.analysis_*` | 阵容分析横条位置/高度/字号/底色字色 | 见 config.json | | `overlay.items_*` / `item_*` | 锁定后装备图标条位置/尺寸/底色 | 见 config.json | --- ## 5. GSI 自动化 ``` Dota 2 (-gamestateintegration) → POST → pc/gsi_watch.py :3223 → pc/samples/raw/{matchid}/gsi.jsonl(可选) → DraftSession 轮询 recognize + roles/grid/modes → pc/results/draft_*.json ``` 要点:按 `matchid` 去重;截图进 `pc/samples/raw/{matchid}/`;识别在工作线程; 未标定时降级为只截图。 天梯 AP 选人按 2/2/1 成批揭晓,本轮结束前互不可见——因此必须在 **HERO_SELECTION** 跟踪,不能只在决策时间截一张终局图;决策前期仍读几帧 以吃到最后一人揭晓(见发现七)。 --- ## 6. 选将全网格「克 / 搭 / 补」推荐 - **触发**:敌方至少锁定 1 人(`min_enemies`)后开始;本人锁人后清空标记。 - **分路**:定位匹配画面字(`roles.py`)+ GSI `team_slot` 定位本人;不做截屏认自己。有分路则按 `role_tags` 过滤候选;非定位局(读不到分路字)则全英雄表。 - **关系模型**:机制克制/搭档(`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)**:[`pc/draft_archetypes.py`](pc/draft_archetypes.py) 识别敌方推进/全球流、敌我 tag 缺口(缺控制/爆发/输出/先手);输出一句 `analysis`(如 `敌:缺控制·偏推进 | 我:缺爆发`)与每英雄短 `reasons`。 - **纳入**: - **克**:关系克制边;推进/全球流应对表(如美杜莎对推进);惩戒敌方缺口 - **搭**:与已锁己有搭档边 - **补**:填我方缺口(`min_heroes_for_gaps` 默认 2 才注入缺口类标记) - **打分**:关系边为主;阵容应对 / 缺口小幅加分;敌方 tags 软加分仅排序。 - **展示**:网格左上角全量相关格标「克」(青)/「搭」(琥珀)/「补」(紫灰);叠加层横条显示阵容分析;原因进日志与 `results` JSON,不写在每个格子旁。 - **上分帝 Web**:`web/serve_relations.py` 本地开发服务;仿选将网格查看克制/被克制/搭档;数据手改 `shared/data/relations.json` 或从 xlsx 导入。 另有顶级 **排行**、**走势**(`/trends[/bracket]`,近 8 周高胜率/上场率榜)、**物品**、**版本** 页;目录见 `web/data/item_shop.json` / `web/data/patches.json` / `web/data/leaderboards.json`。 英雄详情子标签含 **走势**(见上;`/heroes//trends`)、**对位**(克制/被克/搭档三列 STRATZ Top;`/heroes//matchups`)、**近期比赛**(`/heroes//matches`)。排行页为 Immortal 四区选手榜。 - **Web History 路由 + SEO 预渲染**:状态(顶层标签 / 选中英雄 + 详情子标签 / 选中物品 / 选中版本 / 标签筛选 / 搜索)双向同步进路径 URL(`/heroes/axe/core?tags=核心&q=axe`、`/heroes/axe/trends` 等)。 旧 `#/...` 书签在装载时 `replaceState` 迁到路径。写 URL 用 `history.pushState`/`replaceState` (静默,不触发 `popstate`);前进/后退靠 `popstate`。Cloudflare `_redirects` 与 `serve_relations.py` 对未知深度路径回退 `index.html`。导出(`seo_prerender.py`)为首页、 顶层页、全英雄与机制效果写可抓取 HTML + `sitemap.xml` / `llms.txt`。 搜索 debounce 300ms + `replaceState` 防刷历史栈;坏链接丢弃该项不崩。路由逻辑集中在 `web/frontend/router.js`,`app.js` 只在 `main()` 装 `installRouter` + 各 state 变更点 调 `syncStateToUrl`。 - **不做**:避用标、Steam 登录页、爬 Dotabuff;关系**不**写入 `shared/data/heroes.json`;不接实时 AI。 --- ## 7. 与 dota2-hex 独立验证项目。可选:Rust 重写并入,或本地旁路 HTTP 回传。 合规同 `dota2-hex`:**屏幕可见信息 only**。 --- ## 8. 参考 - [Valve #9562](https://github.com/ValveSoftware/Dota2-Gameplay/issues/9562) - [Valve #14915](https://github.com/ValveSoftware/Dota2-Gameplay/issues/14915)(开发者 OCR ≈85%) - [Overwolf Dota 2 GEP](https://dev.overwolf.com/ow-native/live-game-data-gep/supported-games/dota-2/) - Steam CDN:`https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/heroes/{key}.png`