From 9c5aa5b6103cf569bbd5a8c6d6edfcfee32f443f Mon Sep 17 00:00:00 2001 From: voson Date: Wed, 29 Jul 2026 14:29:08 +0800 Subject: [PATCH] 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 --- .gitea/workflows/site-traffic-notify.yml | 4 +- .gitea/workflows/web-daily.yml | 4 +- .gitea/workflows/web-patch.yml | 4 +- .gitea/workflows/web-weekly.yml | 4 +- .gitignore | 50 +-- AGENTS.md | 305 +++++++++-------- CHANGELOG.md | 31 ++ DESIGN.md | 59 ++-- README.md | 107 +++--- {assets => pc/assets}/role_icons/Carry.png | Bin {assets => pc/assets}/role_icons/Disabler.png | Bin {assets => pc/assets}/role_icons/Durable.png | Bin {assets => pc/assets}/role_icons/Escape.png | Bin .../assets}/role_icons/Initiator.png | Bin {assets => pc/assets}/role_icons/Nuker.png | Bin {assets => pc/assets}/role_icons/Pusher.png | Bin {assets => pc/assets}/role_icons/Support.png | Bin autocalibrate.py => pc/autocalibrate.py | 5 +- calibrate.py => pc/calibrate.py | 4 + capture.py => pc/capture.py | 7 +- common.py => pc/common.py | 42 +-- config.json => pc/config.json | 2 +- draft_archetypes.py => pc/draft_archetypes.py | 7 +- draft_session.py => pc/draft_session.py | 9 +- evaluate.py => pc/evaluate.py | 6 +- .../fetch_cdn_templates.py | 12 +- gsi_setup.py => pc/gsi_setup.py | 5 +- gsi_watch.py => pc/gsi_watch.py | 7 +- modes.py => pc/modes.py | 3 + overlay.py => pc/overlay.py | 6 +- recognize.py => pc/recognize.py | 6 +- recommend.py => pc/recommend.py | 13 +- requirements.txt => pc/requirements.txt | 0 roles.py => pc/roles.py | 5 + {samples => pc/samples}/labels.json | 0 {templates => pc/templates}/cdn/abaddon.png | Bin .../templates}/cdn/abyssal_underlord.png | Bin {templates => pc/templates}/cdn/alchemist.png | Bin .../templates}/cdn/ancient_apparition.png | Bin {templates => pc/templates}/cdn/antimage.png | Bin .../templates}/cdn/arc_warden.png | Bin {templates => pc/templates}/cdn/axe.png | Bin {templates => pc/templates}/cdn/bane.png | Bin {templates => pc/templates}/cdn/batrider.png | Bin .../templates}/cdn/beastmaster.png | Bin .../templates}/cdn/bloodseeker.png | Bin .../templates}/cdn/bounty_hunter.png | Bin .../templates}/cdn/brewmaster.png | Bin .../templates}/cdn/bristleback.png | Bin .../templates}/cdn/broodmother.png | Bin {templates => pc/templates}/cdn/centaur.png | Bin .../templates}/cdn/chaos_knight.png | Bin {templates => pc/templates}/cdn/chen.png | Bin {templates => pc/templates}/cdn/clinkz.png | Bin .../templates}/cdn/crystal_maiden.png | Bin {templates => pc/templates}/cdn/dark_seer.png | Bin .../templates}/cdn/dark_willow.png | Bin .../templates}/cdn/dawnbreaker.png | Bin {templates => pc/templates}/cdn/dazzle.png | Bin .../templates}/cdn/death_prophet.png | Bin {templates => pc/templates}/cdn/disruptor.png | Bin .../templates}/cdn/doom_bringer.png | Bin .../templates}/cdn/dragon_knight.png | Bin .../templates}/cdn/drow_ranger.png | Bin .../templates}/cdn/earth_spirit.png | Bin .../templates}/cdn/earthshaker.png | Bin .../templates}/cdn/elder_titan.png | Bin .../templates}/cdn/ember_spirit.png | Bin .../templates}/cdn/enchantress.png | Bin {templates => pc/templates}/cdn/enigma.png | Bin .../templates}/cdn/faceless_void.png | Bin {templates => pc/templates}/cdn/furion.png | Bin .../templates}/cdn/grimstroke.png | Bin .../templates}/cdn/gyrocopter.png | Bin {templates => pc/templates}/cdn/hoodwink.png | Bin {templates => pc/templates}/cdn/huskar.png | Bin {templates => pc/templates}/cdn/invoker.png | Bin {templates => pc/templates}/cdn/jakiro.png | Bin .../templates}/cdn/juggernaut.png | Bin .../templates}/cdn/keeper_of_the_light.png | Bin {templates => pc/templates}/cdn/kez.png | Bin {templates => pc/templates}/cdn/kunkka.png | Bin {templates => pc/templates}/cdn/largo.png | Bin .../templates}/cdn/legion_commander.png | Bin {templates => pc/templates}/cdn/leshrac.png | Bin {templates => pc/templates}/cdn/lich.png | Bin .../templates}/cdn/life_stealer.png | Bin {templates => pc/templates}/cdn/lina.png | Bin {templates => pc/templates}/cdn/lion.png | Bin .../templates}/cdn/lone_druid.png | Bin {templates => pc/templates}/cdn/luna.png | Bin {templates => pc/templates}/cdn/lycan.png | Bin {templates => pc/templates}/cdn/magnataur.png | Bin {templates => pc/templates}/cdn/marci.png | Bin {templates => pc/templates}/cdn/mars.png | Bin {templates => pc/templates}/cdn/medusa.png | Bin {templates => pc/templates}/cdn/meepo.png | Bin {templates => pc/templates}/cdn/mirana.png | Bin .../templates}/cdn/monkey_king.png | Bin {templates => pc/templates}/cdn/morphling.png | Bin {templates => pc/templates}/cdn/muerta.png | Bin .../templates}/cdn/naga_siren.png | Bin {templates => pc/templates}/cdn/necrolyte.png | Bin {templates => pc/templates}/cdn/nevermore.png | Bin .../templates}/cdn/night_stalker.png | Bin .../templates}/cdn/nyx_assassin.png | Bin .../templates}/cdn/obsidian_destroyer.png | Bin {templates => pc/templates}/cdn/ogre_magi.png | Bin .../templates}/cdn/omniknight.png | Bin {templates => pc/templates}/cdn/oracle.png | Bin {templates => pc/templates}/cdn/pangolier.png | Bin .../templates}/cdn/phantom_assassin.png | Bin .../templates}/cdn/phantom_lancer.png | Bin {templates => pc/templates}/cdn/phoenix.png | Bin .../templates}/cdn/primal_beast.png | Bin {templates => pc/templates}/cdn/puck.png | Bin {templates => pc/templates}/cdn/pudge.png | Bin {templates => pc/templates}/cdn/pugna.png | Bin .../templates}/cdn/queenofpain.png | Bin .../templates}/cdn/rattletrap.png | Bin {templates => pc/templates}/cdn/razor.png | Bin {templates => pc/templates}/cdn/riki.png | Bin .../templates}/cdn/ringmaster.png | Bin {templates => pc/templates}/cdn/rubick.png | Bin {templates => pc/templates}/cdn/sand_king.png | Bin .../templates}/cdn/shadow_demon.png | Bin .../templates}/cdn/shadow_shaman.png | Bin {templates => pc/templates}/cdn/shredder.png | Bin {templates => pc/templates}/cdn/silencer.png | Bin .../templates}/cdn/skeleton_king.png | Bin .../templates}/cdn/skywrath_mage.png | Bin {templates => pc/templates}/cdn/slardar.png | Bin {templates => pc/templates}/cdn/slark.png | Bin {templates => pc/templates}/cdn/snapfire.png | Bin {templates => pc/templates}/cdn/sniper.png | Bin {templates => pc/templates}/cdn/spectre.png | Bin .../templates}/cdn/spirit_breaker.png | Bin .../templates}/cdn/storm_spirit.png | Bin {templates => pc/templates}/cdn/sven.png | Bin {templates => pc/templates}/cdn/techies.png | Bin .../templates}/cdn/templar_assassin.png | Bin .../templates}/cdn/terrorblade.png | Bin .../templates}/cdn/tidehunter.png | Bin {templates => pc/templates}/cdn/tinker.png | Bin {templates => pc/templates}/cdn/tiny.png | Bin {templates => pc/templates}/cdn/treant.png | Bin .../templates}/cdn/troll_warlord.png | Bin {templates => pc/templates}/cdn/tusk.png | Bin {templates => pc/templates}/cdn/undying.png | Bin {templates => pc/templates}/cdn/ursa.png | Bin .../templates}/cdn/vengefulspirit.png | Bin .../templates}/cdn/venomancer.png | Bin {templates => pc/templates}/cdn/viper.png | Bin {templates => pc/templates}/cdn/visage.png | Bin .../templates}/cdn/void_spirit.png | Bin {templates => pc/templates}/cdn/warlock.png | Bin {templates => pc/templates}/cdn/weaver.png | Bin .../templates}/cdn/windrunner.png | Bin .../templates}/cdn/winter_wyvern.png | Bin {templates => pc/templates}/cdn/wisp.png | Bin .../templates}/cdn/witch_doctor.png | Bin {templates => pc/templates}/cdn/zuus.png | Bin .../templates}/modes/all_pick.png | Bin .../templates}/roles/hard_support.png | Bin {templates => pc/templates}/roles/mid.png | Bin {templates => pc/templates}/roles/off.png | Bin {templates => pc/templates}/roles/safe.png | Bin .../templates}/roles/soft_support.png | Bin shared/__init__.py | 6 + .../audit_relations.py | 20 +- {data => shared/data}/heroes.json | 0 {data => shared/data}/relations.json | 0 fetch_matchups.py => shared/fetch_matchups.py | 14 +- fetch_stratz.py => shared/fetch_stratz.py | 14 +- grid.py => shared/grid.py | 44 ++- hero_tags.py => shared/hero_tags.py | 0 http_utils.py => shared/http_utils.py | 0 .../import_relations_xlsx.py | 12 +- shared/paths.py | 52 +++ relations.py => shared/relations.py | 12 +- _cf_add_cname.py => web/_cf_add_cname.py | 0 _cf_domain.py => web/_cf_domain.py | 0 _cf_human_home.py => web/_cf_human_home.py | 0 .../_cf_human_traffic.py | 0 _cf_status.py => web/_cf_status.py | 0 _cf_traffic.py => web/_cf_traffic.py | 0 .../_gitea_actions_secrets.py | 0 .../_gitea_dispatch_notify.py | 0 .../_notify_pass_feishu.py | 0 .../_oss_ability_videos.py | 4 +- _oss_fix_public.py => web/_oss_fix_public.py | 0 .../_oss_launch_upload.py | 5 +- .../_oss_static_assets.py | 5 +- {assets => web/assets}/attr_icons/agi.png | Bin {assets => web/assets}/attr_icons/all.png | Bin {assets => web/assets}/attr_icons/int.png | Bin {assets => web/assets}/attr_icons/str.png | Bin .../assets}/item_cat_icons/itemcat_arcane.png | Bin .../item_cat_icons/itemcat_armaments.png | Bin .../assets}/item_cat_icons/itemcat_armor.png | Bin .../item_cat_icons/itemcat_artifacts.png | Bin .../item_cat_icons/itemcat_attributes.png | Bin .../assets}/item_cat_icons/itemcat_caster.png | Bin .../assets}/item_cat_icons/itemcat_common.png | Bin .../item_cat_icons/itemcat_consumables.png | Bin .../assets}/item_cat_icons/itemcat_secret.png | Bin .../item_cat_icons/itemcat_support.png | Bin .../item_cat_icons/itemcat_weapons.png | Bin .../assets}/rank_icons/rank_icon_1.png | Bin .../assets}/rank_icons/rank_icon_2.png | Bin .../assets}/rank_icons/rank_icon_3.png | Bin .../assets}/rank_icons/rank_icon_4.png | Bin .../assets}/rank_icons/rank_icon_5.png | Bin .../assets}/rank_icons/rank_icon_6.png | Bin .../assets}/rank_icons/rank_icon_7.png | Bin .../assets}/rank_icons/rank_icon_8.png | Bin .../assets}/streamer_avatars/.gitkeep | 0 .../assets}/streamer_avatars/dadigua.jpg | Bin .../assets}/streamer_avatars/dota25830.jpg | Bin .../assets}/streamer_avatars/gudu.jpg | Bin .../assets}/streamer_avatars/gugong.jpg | Bin .../assets}/streamer_avatars/huomao.jpg | Bin .../assets}/streamer_avatars/jianxin.jpg | Bin .../assets}/streamer_avatars/mayi.jpg | Bin .../assets}/streamer_avatars/shawang.jpg | Bin .../assets}/streamer_avatars/xiaowang.jpg | Bin .../assets}/streamer_avatars/yangfanqiguo.jpg | Bin .../assets}/streamer_avatars/yaseguilai.jpg | Bin {assets => web/assets}/ui_icons/cooldown.png | Bin .../assets}/ui_icons/dota2_logo.png | Bin .../assets}/ui_icons/dota2_logo_wordmark.png | Bin .../assets}/ui_icons/platform_bilibili.png | Bin .../assets}/ui_icons/platform_douyin.png | Bin {data => web/data}/ability_tag_overrides.json | 0 {data => web/data}/hero_abilities.json | 0 {data => web/data}/hero_fear_overrides.json | 0 {data => web/data}/hero_grid_order.json | 0 {data => web/data}/hero_item_fears.json | 0 {data => web/data}/hero_items.json | 0 {data => web/data}/item_shop.json | 0 {data => web/data}/item_tag_overrides.json | 0 {data => web/data}/items_meta.json | 0 {data => web/data}/leaderboards.json | 0 {data => web/data}/patches.json | 0 {data => web/data}/streamers.json | 28 +- {data => web/data}/talent_overrides.json | 0 .../deploy_relations.py | 7 +- .../export_relations_site.py | 22 +- .../fetch_ability_videos.py | 8 +- .../fetch_dota2_logo.py | 10 +- .../fetch_hero_abilities.py | 15 +- .../fetch_hero_items.py | 12 +- .../fetch_hero_matches.py | 12 +- .../fetch_hero_portraits.py | 9 +- .../fetch_hero_stats.py | 12 +- .../fetch_item_counter_stats.py | 10 +- fetch_item_shop.py => web/fetch_item_shop.py | 10 +- .../fetch_items_meta.py | 11 +- .../fetch_leaderboards.py | 10 +- fetch_patches.py => web/fetch_patches.py | 10 +- .../fetch_pro_builds.py | 13 +- .../fetch_pro_matches.py | 13 +- .../fetch_stratz_meta.py | 10 +- web/fetch_streamer_live.py | 257 ++++++++++++++ fetch_streamers.py => web/fetch_streamers.py | 10 +- web/{relations => frontend}/_headers | 0 web/{relations => frontend}/app.js | 99 +++++- web/{relations => frontend}/config.js | 2 +- web/frontend/functions/api/live-status.js | 323 ++++++++++++++++++ web/{relations => frontend}/index.html | 8 +- web/{relations => frontend}/router.js | 0 web/{relations => frontend}/style.css | 5 +- item_fears.py => web/item_fears.py | 14 +- loc_format.py => web/loc_format.py | 0 mechanic_tags.py => web/mechanic_tags.py | 0 .../notify_site_traffic.py | 2 +- refresh_web.py => web/refresh_web.py | 8 +- requirements-web.txt => web/requirements.txt | 0 .../run_notify_site_traffic.ps1 | 0 serve_relations.py => web/serve_relations.py | 56 +-- 280 files changed, 1451 insertions(+), 450 deletions(-) rename {assets => pc/assets}/role_icons/Carry.png (100%) rename {assets => pc/assets}/role_icons/Disabler.png (100%) rename {assets => pc/assets}/role_icons/Durable.png (100%) rename {assets => pc/assets}/role_icons/Escape.png (100%) rename {assets => pc/assets}/role_icons/Initiator.png (100%) rename {assets => pc/assets}/role_icons/Nuker.png (100%) rename {assets => pc/assets}/role_icons/Pusher.png (100%) rename {assets => pc/assets}/role_icons/Support.png (100%) rename autocalibrate.py => pc/autocalibrate.py (99%) rename calibrate.py => pc/calibrate.py (96%) rename capture.py => pc/capture.py (98%) rename common.py => pc/common.py (74%) rename config.json => pc/config.json (98%) rename draft_archetypes.py => pc/draft_archetypes.py (98%) rename draft_session.py => pc/draft_session.py (99%) rename evaluate.py => pc/evaluate.py (96%) rename fetch_cdn_templates.py => pc/fetch_cdn_templates.py (96%) rename gsi_setup.py => pc/gsi_setup.py (98%) rename gsi_watch.py => pc/gsi_watch.py (99%) rename modes.py => pc/modes.py (98%) rename overlay.py => pc/overlay.py (99%) rename recognize.py => pc/recognize.py (98%) rename recommend.py => pc/recommend.py (96%) rename requirements.txt => pc/requirements.txt (100%) rename roles.py => pc/roles.py (98%) rename {samples => pc/samples}/labels.json (100%) rename {templates => pc/templates}/cdn/abaddon.png (100%) rename {templates => pc/templates}/cdn/abyssal_underlord.png (100%) rename {templates => pc/templates}/cdn/alchemist.png (100%) rename {templates => pc/templates}/cdn/ancient_apparition.png (100%) rename {templates => pc/templates}/cdn/antimage.png (100%) rename {templates => pc/templates}/cdn/arc_warden.png (100%) rename {templates => pc/templates}/cdn/axe.png (100%) rename {templates => pc/templates}/cdn/bane.png (100%) rename {templates => pc/templates}/cdn/batrider.png (100%) rename {templates => pc/templates}/cdn/beastmaster.png (100%) rename {templates => pc/templates}/cdn/bloodseeker.png (100%) rename {templates => pc/templates}/cdn/bounty_hunter.png (100%) rename {templates => pc/templates}/cdn/brewmaster.png (100%) rename {templates => pc/templates}/cdn/bristleback.png (100%) rename {templates => pc/templates}/cdn/broodmother.png (100%) rename {templates => pc/templates}/cdn/centaur.png (100%) rename {templates => pc/templates}/cdn/chaos_knight.png (100%) rename {templates => pc/templates}/cdn/chen.png (100%) rename {templates => pc/templates}/cdn/clinkz.png (100%) rename {templates => pc/templates}/cdn/crystal_maiden.png (100%) rename {templates => pc/templates}/cdn/dark_seer.png (100%) rename {templates => pc/templates}/cdn/dark_willow.png (100%) rename {templates => pc/templates}/cdn/dawnbreaker.png (100%) rename {templates => pc/templates}/cdn/dazzle.png (100%) rename {templates => pc/templates}/cdn/death_prophet.png (100%) rename {templates => pc/templates}/cdn/disruptor.png (100%) rename {templates => pc/templates}/cdn/doom_bringer.png (100%) rename {templates => pc/templates}/cdn/dragon_knight.png (100%) rename {templates => pc/templates}/cdn/drow_ranger.png (100%) rename {templates => pc/templates}/cdn/earth_spirit.png (100%) rename {templates => pc/templates}/cdn/earthshaker.png (100%) rename {templates => pc/templates}/cdn/elder_titan.png (100%) rename {templates => pc/templates}/cdn/ember_spirit.png (100%) rename {templates => pc/templates}/cdn/enchantress.png (100%) rename {templates => pc/templates}/cdn/enigma.png (100%) rename {templates => pc/templates}/cdn/faceless_void.png (100%) rename {templates => pc/templates}/cdn/furion.png (100%) rename {templates => pc/templates}/cdn/grimstroke.png (100%) rename {templates => pc/templates}/cdn/gyrocopter.png (100%) rename {templates => pc/templates}/cdn/hoodwink.png (100%) rename {templates => pc/templates}/cdn/huskar.png (100%) rename {templates => pc/templates}/cdn/invoker.png (100%) rename {templates => pc/templates}/cdn/jakiro.png (100%) rename {templates => pc/templates}/cdn/juggernaut.png (100%) rename {templates => pc/templates}/cdn/keeper_of_the_light.png (100%) rename {templates => pc/templates}/cdn/kez.png (100%) rename {templates => pc/templates}/cdn/kunkka.png (100%) rename {templates => pc/templates}/cdn/largo.png (100%) rename {templates => pc/templates}/cdn/legion_commander.png (100%) rename {templates => pc/templates}/cdn/leshrac.png (100%) rename {templates => pc/templates}/cdn/lich.png (100%) rename {templates => pc/templates}/cdn/life_stealer.png (100%) rename {templates => pc/templates}/cdn/lina.png (100%) rename {templates => pc/templates}/cdn/lion.png (100%) rename {templates => pc/templates}/cdn/lone_druid.png (100%) rename {templates => pc/templates}/cdn/luna.png (100%) rename {templates => pc/templates}/cdn/lycan.png (100%) rename {templates => pc/templates}/cdn/magnataur.png (100%) rename {templates => pc/templates}/cdn/marci.png (100%) rename {templates => pc/templates}/cdn/mars.png (100%) rename {templates => pc/templates}/cdn/medusa.png (100%) rename {templates => pc/templates}/cdn/meepo.png (100%) rename {templates => pc/templates}/cdn/mirana.png (100%) rename {templates => pc/templates}/cdn/monkey_king.png (100%) rename {templates => pc/templates}/cdn/morphling.png (100%) rename {templates => pc/templates}/cdn/muerta.png (100%) rename {templates => pc/templates}/cdn/naga_siren.png (100%) rename {templates => pc/templates}/cdn/necrolyte.png (100%) rename {templates => pc/templates}/cdn/nevermore.png (100%) rename {templates => pc/templates}/cdn/night_stalker.png (100%) rename {templates => pc/templates}/cdn/nyx_assassin.png (100%) rename {templates => pc/templates}/cdn/obsidian_destroyer.png (100%) rename {templates => pc/templates}/cdn/ogre_magi.png (100%) rename {templates => pc/templates}/cdn/omniknight.png (100%) rename {templates => pc/templates}/cdn/oracle.png (100%) rename {templates => pc/templates}/cdn/pangolier.png (100%) rename {templates => pc/templates}/cdn/phantom_assassin.png (100%) rename {templates => pc/templates}/cdn/phantom_lancer.png (100%) rename {templates => pc/templates}/cdn/phoenix.png (100%) rename {templates => pc/templates}/cdn/primal_beast.png (100%) rename {templates => pc/templates}/cdn/puck.png (100%) rename {templates => pc/templates}/cdn/pudge.png (100%) rename {templates => pc/templates}/cdn/pugna.png (100%) rename {templates => pc/templates}/cdn/queenofpain.png (100%) rename {templates => pc/templates}/cdn/rattletrap.png (100%) rename {templates => pc/templates}/cdn/razor.png (100%) rename {templates => pc/templates}/cdn/riki.png (100%) rename {templates => pc/templates}/cdn/ringmaster.png (100%) rename {templates => pc/templates}/cdn/rubick.png (100%) rename {templates => pc/templates}/cdn/sand_king.png (100%) rename {templates => pc/templates}/cdn/shadow_demon.png (100%) rename {templates => pc/templates}/cdn/shadow_shaman.png (100%) rename {templates => pc/templates}/cdn/shredder.png (100%) rename {templates => pc/templates}/cdn/silencer.png (100%) rename {templates => pc/templates}/cdn/skeleton_king.png (100%) rename {templates => pc/templates}/cdn/skywrath_mage.png (100%) rename {templates => pc/templates}/cdn/slardar.png (100%) rename {templates => pc/templates}/cdn/slark.png (100%) rename {templates => pc/templates}/cdn/snapfire.png (100%) rename {templates => pc/templates}/cdn/sniper.png (100%) rename {templates => pc/templates}/cdn/spectre.png (100%) rename {templates => pc/templates}/cdn/spirit_breaker.png (100%) rename {templates => pc/templates}/cdn/storm_spirit.png (100%) rename {templates => pc/templates}/cdn/sven.png (100%) rename {templates => pc/templates}/cdn/techies.png (100%) rename {templates => pc/templates}/cdn/templar_assassin.png (100%) rename {templates => pc/templates}/cdn/terrorblade.png (100%) rename {templates => pc/templates}/cdn/tidehunter.png (100%) rename {templates => pc/templates}/cdn/tinker.png (100%) rename {templates => pc/templates}/cdn/tiny.png (100%) rename {templates => pc/templates}/cdn/treant.png (100%) rename {templates => pc/templates}/cdn/troll_warlord.png (100%) rename {templates => pc/templates}/cdn/tusk.png (100%) rename {templates => pc/templates}/cdn/undying.png (100%) rename {templates => pc/templates}/cdn/ursa.png (100%) rename {templates => pc/templates}/cdn/vengefulspirit.png (100%) rename {templates => pc/templates}/cdn/venomancer.png (100%) rename {templates => pc/templates}/cdn/viper.png (100%) rename {templates => pc/templates}/cdn/visage.png (100%) rename {templates => pc/templates}/cdn/void_spirit.png (100%) rename {templates => pc/templates}/cdn/warlock.png (100%) rename {templates => pc/templates}/cdn/weaver.png (100%) rename {templates => pc/templates}/cdn/windrunner.png (100%) rename {templates => pc/templates}/cdn/winter_wyvern.png (100%) rename {templates => pc/templates}/cdn/wisp.png (100%) rename {templates => pc/templates}/cdn/witch_doctor.png (100%) rename {templates => pc/templates}/cdn/zuus.png (100%) rename {templates => pc/templates}/modes/all_pick.png (100%) rename {templates => pc/templates}/roles/hard_support.png (100%) rename {templates => pc/templates}/roles/mid.png (100%) rename {templates => pc/templates}/roles/off.png (100%) rename {templates => pc/templates}/roles/safe.png (100%) rename {templates => pc/templates}/roles/soft_support.png (100%) create mode 100644 shared/__init__.py rename audit_relations.py => shared/audit_relations.py (97%) rename {data => shared/data}/heroes.json (100%) rename {data => shared/data}/relations.json (100%) rename fetch_matchups.py => shared/fetch_matchups.py (92%) rename fetch_stratz.py => shared/fetch_stratz.py (95%) rename grid.py => shared/grid.py (88%) rename hero_tags.py => shared/hero_tags.py (100%) rename http_utils.py => shared/http_utils.py (100%) rename import_relations_xlsx.py => shared/import_relations_xlsx.py (95%) create mode 100644 shared/paths.py rename relations.py => shared/relations.py (97%) rename _cf_add_cname.py => web/_cf_add_cname.py (100%) rename _cf_domain.py => web/_cf_domain.py (100%) rename _cf_human_home.py => web/_cf_human_home.py (100%) rename _cf_human_traffic.py => web/_cf_human_traffic.py (100%) rename _cf_status.py => web/_cf_status.py (100%) rename _cf_traffic.py => web/_cf_traffic.py (100%) rename _gitea_actions_secrets.py => web/_gitea_actions_secrets.py (100%) rename _gitea_dispatch_notify.py => web/_gitea_dispatch_notify.py (100%) rename _notify_pass_feishu.py => web/_notify_pass_feishu.py (100%) rename _oss_ability_videos.py => web/_oss_ability_videos.py (98%) rename _oss_fix_public.py => web/_oss_fix_public.py (100%) rename _oss_launch_upload.py => web/_oss_launch_upload.py (87%) rename _oss_static_assets.py => web/_oss_static_assets.py (98%) rename {assets => web/assets}/attr_icons/agi.png (100%) rename {assets => web/assets}/attr_icons/all.png (100%) rename {assets => web/assets}/attr_icons/int.png (100%) rename {assets => web/assets}/attr_icons/str.png (100%) rename {assets => web/assets}/item_cat_icons/itemcat_arcane.png (100%) rename {assets => web/assets}/item_cat_icons/itemcat_armaments.png (100%) rename {assets => web/assets}/item_cat_icons/itemcat_armor.png (100%) rename {assets => web/assets}/item_cat_icons/itemcat_artifacts.png (100%) rename {assets => web/assets}/item_cat_icons/itemcat_attributes.png (100%) rename {assets => web/assets}/item_cat_icons/itemcat_caster.png (100%) rename {assets => web/assets}/item_cat_icons/itemcat_common.png (100%) rename {assets => web/assets}/item_cat_icons/itemcat_consumables.png (100%) rename {assets => web/assets}/item_cat_icons/itemcat_secret.png (100%) rename {assets => web/assets}/item_cat_icons/itemcat_support.png (100%) rename {assets => web/assets}/item_cat_icons/itemcat_weapons.png (100%) rename {assets => web/assets}/rank_icons/rank_icon_1.png (100%) rename {assets => web/assets}/rank_icons/rank_icon_2.png (100%) rename {assets => web/assets}/rank_icons/rank_icon_3.png (100%) rename {assets => web/assets}/rank_icons/rank_icon_4.png (100%) rename {assets => web/assets}/rank_icons/rank_icon_5.png (100%) rename {assets => web/assets}/rank_icons/rank_icon_6.png (100%) rename {assets => web/assets}/rank_icons/rank_icon_7.png (100%) rename {assets => web/assets}/rank_icons/rank_icon_8.png (100%) rename {assets => web/assets}/streamer_avatars/.gitkeep (100%) rename {assets => web/assets}/streamer_avatars/dadigua.jpg (100%) rename {assets => web/assets}/streamer_avatars/dota25830.jpg (100%) rename {assets => web/assets}/streamer_avatars/gudu.jpg (100%) rename {assets => web/assets}/streamer_avatars/gugong.jpg (100%) rename {assets => web/assets}/streamer_avatars/huomao.jpg (100%) rename {assets => web/assets}/streamer_avatars/jianxin.jpg (100%) rename {assets => web/assets}/streamer_avatars/mayi.jpg (100%) rename {assets => web/assets}/streamer_avatars/shawang.jpg (100%) rename {assets => web/assets}/streamer_avatars/xiaowang.jpg (100%) rename {assets => web/assets}/streamer_avatars/yangfanqiguo.jpg (100%) rename {assets => web/assets}/streamer_avatars/yaseguilai.jpg (100%) rename {assets => web/assets}/ui_icons/cooldown.png (100%) rename {assets => web/assets}/ui_icons/dota2_logo.png (100%) rename {assets => web/assets}/ui_icons/dota2_logo_wordmark.png (100%) rename {assets => web/assets}/ui_icons/platform_bilibili.png (100%) rename {assets => web/assets}/ui_icons/platform_douyin.png (100%) rename {data => web/data}/ability_tag_overrides.json (100%) rename {data => web/data}/hero_abilities.json (100%) rename {data => web/data}/hero_fear_overrides.json (100%) rename {data => web/data}/hero_grid_order.json (100%) rename {data => web/data}/hero_item_fears.json (100%) rename {data => web/data}/hero_items.json (100%) rename {data => web/data}/item_shop.json (100%) rename {data => web/data}/item_tag_overrides.json (100%) rename {data => web/data}/items_meta.json (100%) rename {data => web/data}/leaderboards.json (100%) rename {data => web/data}/patches.json (100%) rename {data => web/data}/streamers.json (88%) rename {data => web/data}/talent_overrides.json (100%) rename deploy_relations.py => web/deploy_relations.py (98%) rename export_relations_site.py => web/export_relations_site.py (92%) rename fetch_ability_videos.py => web/fetch_ability_videos.py (99%) rename fetch_dota2_logo.py => web/fetch_dota2_logo.py (94%) rename fetch_hero_abilities.py => web/fetch_hero_abilities.py (99%) rename fetch_hero_items.py => web/fetch_hero_items.py (97%) rename fetch_hero_matches.py => web/fetch_hero_matches.py (99%) rename fetch_hero_portraits.py => web/fetch_hero_portraits.py (93%) rename fetch_hero_stats.py => web/fetch_hero_stats.py (97%) rename fetch_item_counter_stats.py => web/fetch_item_counter_stats.py (98%) rename fetch_item_shop.py => web/fetch_item_shop.py (98%) rename fetch_items_meta.py => web/fetch_items_meta.py (98%) rename fetch_leaderboards.py => web/fetch_leaderboards.py (95%) rename fetch_patches.py => web/fetch_patches.py (99%) rename fetch_pro_builds.py => web/fetch_pro_builds.py (98%) rename fetch_pro_matches.py => web/fetch_pro_matches.py (98%) rename fetch_stratz_meta.py => web/fetch_stratz_meta.py (99%) create mode 100644 web/fetch_streamer_live.py rename fetch_streamers.py => web/fetch_streamers.py (98%) rename web/{relations => frontend}/_headers (100%) rename web/{relations => frontend}/app.js (97%) rename web/{relations => frontend}/config.js (82%) create mode 100644 web/frontend/functions/api/live-status.js rename web/{relations => frontend}/index.html (96%) rename web/{relations => frontend}/router.js (100%) rename web/{relations => frontend}/style.css (99%) rename item_fears.py => web/item_fears.py (98%) rename loc_format.py => web/loc_format.py (100%) rename mechanic_tags.py => web/mechanic_tags.py (100%) rename notify_site_traffic.py => web/notify_site_traffic.py (99%) rename refresh_web.py => web/refresh_web.py (96%) rename requirements-web.txt => web/requirements.txt (100%) rename run_notify_site_traffic.ps1 => web/run_notify_site_traffic.ps1 (100%) rename serve_relations.py => web/serve_relations.py (96%) diff --git a/.gitea/workflows/site-traffic-notify.yml b/.gitea/workflows/site-traffic-notify.yml index 9d89a5a..f23a92f 100644 --- a/.gitea/workflows/site-traffic-notify.yml +++ b/.gitea/workflows/site-traffic-notify.yml @@ -36,7 +36,7 @@ jobs: PYTHON=python3 command -v python3 >/dev/null || PYTHON=python 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 - "$PYTHON" notify_site_traffic.py + "$PYTHON" web/notify_site_traffic.py fi diff --git a/.gitea/workflows/web-daily.yml b/.gitea/workflows/web-daily.yml index 82759bf..f9623f8 100644 --- a/.gitea/workflows/web-daily.yml +++ b/.gitea/workflows/web-daily.yml @@ -25,7 +25,7 @@ jobs: PYTHON=python3 command -v python3 >/dev/null || PYTHON=python 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 env: @@ -38,4 +38,4 @@ jobs: set -e PYTHON=python3 command -v python3 >/dev/null || PYTHON=python - "$PYTHON" refresh_web.py --tier daily + "$PYTHON" web/refresh_web.py --tier daily diff --git a/.gitea/workflows/web-patch.yml b/.gitea/workflows/web-patch.yml index 2f8ee47..610f74f 100644 --- a/.gitea/workflows/web-patch.yml +++ b/.gitea/workflows/web-patch.yml @@ -24,7 +24,7 @@ jobs: PYTHON=python3 command -v python3 >/dev/null || PYTHON=python 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 env: @@ -36,4 +36,4 @@ jobs: set -e PYTHON=python3 command -v python3 >/dev/null || PYTHON=python - "$PYTHON" refresh_web.py --tier patch + "$PYTHON" web/refresh_web.py --tier patch diff --git a/.gitea/workflows/web-weekly.yml b/.gitea/workflows/web-weekly.yml index b9bfc2a..38e056e 100644 --- a/.gitea/workflows/web-weekly.yml +++ b/.gitea/workflows/web-weekly.yml @@ -23,7 +23,7 @@ jobs: PYTHON=python3 command -v python3 >/dev/null || PYTHON=python 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 env: @@ -36,4 +36,4 @@ jobs: set -e PYTHON=python3 command -v python3 >/dev/null || PYTHON=python - "$PYTHON" refresh_web.py --tier weekly + "$PYTHON" web/refresh_web.py --tier weekly diff --git a/.gitignore b/.gitignore index 28212e2..39b1e0f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,10 +13,10 @@ Thumbs.db oss_upload.pid # Runtime / debug artifacts (regenerable) -preview/ -results/ -failures/ -samples/raw/ +pc/preview/ +pc/results/ +pc/failures/ +pc/samples/raw/ _tools/ _tmp_* _cmp_* @@ -24,27 +24,31 @@ plans/ *_log.txt # Regenerable data caches / reports (re-fetch or re-run scripts) -data/matchups.json -data/synergies.json -data/stratz_matchups.json -data/stratz_hero_meta.json -data/stratz_matchup_tops.json -data/item_counter_stats.json -data/hero_stats.json -data/hero_matches.json -data/pro_matches.json -data/pro_builds.json -data/relations_audit.json -data/relations_import_report.json -data/relations_patch_* +shared/data/matchups.json +shared/data/synergies.json +shared/data/stratz_matchups.json +shared/data/relations_audit.json +shared/data/relations_import_report.json +shared/data/relations_patch_* +web/data/stratz_hero_meta.json +web/data/stratz_matchup_tops.json +web/data/item_counter_stats.json +web/data/hero_stats.json +web/data/hero_matches.json +web/data/pro_matches.json +web/data/pro_builds.json +web/data/_jugg_times_sample.json # Large regenerable CDN media (fetch scripts) -assets/ability_videos/ -assets/ability_icons/ -assets/hero_portraits/ -assets/item_icons/ +web/assets/ability_videos/ +web/assets/ability_icons/ +web/assets/hero_portraits/ +web/assets/item_icons/ # Curated streamer highlight clips (large; sync via OSS, not git) -assets/streamer_videos/ +web/assets/streamer_videos/ # Static site export (export_relations_site.py) -dist/ +web/dist/ + +# Local probes / one-off diagnostics +_probe_item_fears_api.py diff --git a/AGENTS.md b/AGENTS.md index 66c949f..5091031 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,183 +8,214 @@ Windows 上的 Dota 2 **天梯选将识别**工具:用 GSI 感知选将阶段 - 中文名:上分帝 - 英文名 / 仓库目录:Climperor / `climperor` -- 模板来源:**仅** Steam CDN 官方头像(`templates/cdn/`) +- 模板来源:**仅** Steam CDN 官方头像(`pc/templates/cdn/`) - **已放弃** 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 模板加载 | -| `recognize.py` | 单帧识别;`recognize_image()` 供会话复用 | -| `draft_session.py` | 整局选将跟踪、改判、皮肤规避策略 | -| `gsi_watch.py` / `gsi_setup.py` | GSI 监听与 cfg 安装 | -| `http_utils.py` | 所有 fetch 脚本共用的 HTTP 请求、图标下载、Valve datafeed 加载(统一 UA / 超时;429/5xx 指数退避重试) | -| `fetch_patches.py` | 拉取近一年(默认 365 天,`--days`/`--since`)版本列表 + 逐版本 `patchnotes` 详情 → `data/patches.json`;构建 id→名称/图标的 `lookup` 并下载引用到的物品/技能图标(`--no-icons` 跳过;`--force` 重抓全量;`--check` 只比对列表与本地 details,stdout JSON) | -| `requirements-web.txt` | Web 刷新额外依赖(`oss2`);Gitea Actions 与 `requirements.txt` 一并 `pip install` | -| `fetch_cdn_templates.py` | 拉取 CDN 头像 + 生成 `data/heroes.json`(含基础属性/血蓝;保留已有 `aliases`) | -| `relations.py` | 定性克制/搭档边读写与名称解析 | -| `import_relations_xlsx.py` | 从选将笔记 xlsx 种子导入 `data/relations.json` | -| `fetch_stratz.py` | 拉取 STRATZ 对位优势 / 搭档协同 → `data/stratz_matchups.json` / `data/synergies.json`(需 token;`--mode matchups\|synergies`;供 `audit_relations.py`) | -| `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) | -| `fetch_matchups.py` | 拉取 OpenDota 对位优势 → `data/matchups.json`(供 `audit_relations.py` 交叉审计) | -| `audit_relations.py` | OpenDota+STRATZ 交叉审计 `relations.json`,列出缺失英雄 | -| `recommend.py` | 定位局分路过滤;克/搭/补全网格标记;调用 `draft_archetypes` 做推进/全球流/缺口画像与短文案 | -| `draft_archetypes.py` | 规则阵容画像(推进/全球流/敌我缺口)+ `analysis` / `reasons` 文案(不接 AI) | -| `serve_relations.py` | 上分帝 Web 本地开发服务(`web/relations/`;改 `data/*.json` 后刷新;`/streamer-video/` 提供主播高光 mp4,支持 HTTP Range) | -| `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) | -| `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 Secrets;STRATZ 可选) | -| `.gitea/workflows/web-weekly.yml` | 周二 07:00 CST:`refresh_web.py --tier weekly`(需 `STRATZ_API_TOKEN` + CF + OSS) | -| `.gitea/workflows/web-patch.yml` | 每 6 小时:`refresh_web.py --tier patch`(`fetch_patches.py --check`,有新版本才拉详情与连带;需 CF + OSS) | -| `_cf_status.py` | 只读查询 Cloudflare Pages 项目 / 部署 / 自定义域名状态(凭据经 keyzoo 注入) | -| `_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` 为配套辅助 | -| `fetch_hero_portraits.py` | 拉取官网横版头像 → `assets/hero_portraits/`(上分帝 Web) | -| `fetch_hero_items.py` | 拉取 OpenDota 热门装备 → `data/hero_items.json` + `assets/item_icons/` | -| `fetch_hero_stats.py` | 拉取 OpenDota 各段位场次/胜场 → `data/hero_stats.json`(**仅上分帝 Web**;勿写入 relations/heroes,勿进 recommend) | -| `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) | -| `fetch_leaderboards.py` | 拉取 Valve Immortal 四区榜 Top100 → `data/leaderboards.json`(**仅上分帝 Web「排行」**;无 MMR/account_id;勿进 recommend) | -| `fetch_streamers.py` | 从抖音主页补全 `data/streamers.json` 的昵称/签名/关注粉丝获赞/头像(手工名单 + 直播间/主页 URL;支持 `v.douyin.com` 短链;失败保留旧值;**不**探测开播/`is_live`;**仅上分帝 Web「主播」**;勿进 recommend;进 `refresh_web` daily) | -| `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`) | -| `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 显式 flag,Valve 移除升级后残留的 `scepter_loc`/`shard_loc` 文案会被清空;逐级相同的 `cast_points`/`channel_times` 合并为单值(施法前摇/吟唱时间);可选 `--icons` / `--icons-only` 缓存 Steam CDN 技能图标 → `assets/ability_icons/`(先天用共用 `innate.png`,不拉 CDN) | -| `fetch_ability_videos.py` | 官网技能演示 webm/mp4(Steam CDN;限速)→ `assets/ability_videos/`;A 杖/魔晶赋予技能(`granted_by_scepter`/`granted_by_shard`)CDN 名用 `_aghanims_scepter`/`_aghanims_shard`,本地文件名仍用 ability key | -| `fetch_item_counter_stats.py` | OpenDota Explorer 近场次聚合:对阵英雄时敌方终局装备购买率/胜率,与同窗口该装备全局队伍基线作差 → `data/item_counter_stats.json`(可再生成缓存;观测证据,不作因果结论;weekly 软失败) | -| `item_fears.py` | 规则推导「英雄怕的装备」→ `data/hero_item_fears.json`;可选读取 `item_counter_stats.json`,对全部英雄的已有机制候选小幅调序;统计发现的新组合须经机制复核后写入 overrides | -| `autocalibrate.py` / `calibrate.py` | ROI 自动 / 手动标定 | -| `capture.py` | 屏幕捕获 | -| `overlay.py` | 顶栏角色标签 + 网格「克/搭/补」方标 + 阵容分析横条(点击穿透) | -| `evaluate.py` | 按 `samples/labels.json` 批量评测 | -| `roles.py` / `grid.py` / `modes.py` | 位置字、禁用网格、模式字 | -| `config.json` | 相对坐标、阈值、GSI、recommend 参数 | -| `data/heroes.json` | 英雄表(id/key/attr/roles/aliases/abbr/tags + 基础属性/初始血蓝等;由 `fetch_cdn_templates.py` 生成) | -| `data/patches.json` | 近一年版本列表 + 逐版本详情(`patches`/`lookup`/`details`;由 `fetch_patches.py` 生成,Web 版本页只读) | -| `data/relations.json` | 定性克制边 + 搭档边(可提交;手改 JSON,Web 站点只读) | -| `data/hero_grid_order.json` | Web 站点四列网格顺序 | -| `data/hero_items.json` | 核心成品装备缓存(Web 站点只读;由 `fetch_hero_items.py` 生成) | -| `data/hero_stats.json` | 英雄各段位 pick/win + `totals` + `window_*`(OpenDota **近约 7 天**;Web「走势」Tab:胜率/上场率/场次;冠绝与超凡样本合并展示;由 `fetch_hero_stats.py` 生成;**不**参与局内推荐) | -| `data/hero_matches.json` | 同英雄近期比赛列表 + 终局出装/加点 + 可选购买时间(OpenDota;`--source league\|public\|both`;Web「近期比赛」Tab;由 `fetch_hero_matches.py` 生成;**不**参与局内推荐) | -| `data/leaderboards.json` | Valve Immortal 四区 Top100(`china`/`europe`/`americas`/`se_asia`;仅排名+昵称等;由 `fetch_leaderboards.py` 生成;Web「排行」选手榜只读;**不**参与局内推荐) | -| `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「主播」只读;**不**参与局内推荐) | -| `data/stratz_hero_meta.json` | STRATZ 各勋章段位近 N 周 pick/win(`weeks`/`latest`)+ **同段位最近 1 周分路**(`winWeek`+`positionIds`)+ `meta_board`(由 `fetch_stratz_meta.py` 生成;Web 英雄详情「走势」优先 + 顶层「走势」`#/trends` 近 N 周榜;**不**参与局内推荐) | -| `data/stratz_matchup_tops.json` | STRATZ 对位/协同 Top(counters/countered/synergies;由 `fetch_stratz_meta.py` 生成;Web「对位」Tab;与定性 `relations.json` 分开展示;**不**参与局内推荐) | -| `data/item_shop.json` | 商店 11 列目录(官网 basic/upgrade;由 `fetch_item_shop.py` 生成;物品页只读) | -| `data/items_meta.json` | 成品装备描述与机制标签(由 `fetch_items_meta.py` 生成) | -| `data/item_tag_overrides.json` | 装备标签手工加减(合并进 items_meta) | -| `data/ability_tag_overrides.json` | 技能「施加」类 tags 手工加减(合并进 hero_abilities;仅 Web 机制页) | -| `data/hero_fear_overrides.json` | 英雄→害怕装备手工加减(合并进 item_fears) | -| `data/hero_abilities.json` | 英雄技能与机制汇总(由 `fetch_hero_abilities.py` 生成;含 ability `tags`) | -| `data/item_counter_stats.json` | OpenDota 对阵装备观测证据缓存(对阵购买率/条件胜率减同装备全局基线;可再生成;不进 recommend / relations) | -| `data/hero_item_fears.json` | 英雄怕的装备(规则推导;Web「怕」行) | -| `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/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` 别名兼容) | -| `templates/cdn/` | 全英雄 CDN 模板(需提交或由脚本生成) | -| `templates/roles/` / `templates/modes/` | 位置 / 模式辅助模板 | -| `assets/role_icons/` | Valve 选人筛选角色图标(`filter_*`,品红 chroma 去黑边) | -| `assets/hero_portraits/` | 官网横版头像(上分帝 Web;默认 wide 面部构图,非匹配模板) | -| `assets/attr_icons/` | 官网主属性图标(力量/敏捷/智力/全才,上分帝 Web 用) | -| `assets/rank_icons/` | 天梯勋章图标(OpenDota `rank_icon_1..8` 先锋→冠绝;走势段位选择器) | -| `assets/item_icons/` | Steam CDN 装备图标(上分帝 Web常用装备) | -| `assets/item_cat_icons/` | 官网商店分类图标(`itemcat_*.png`,物品页列头) | -| `assets/ability_icons/` | Steam CDN 技能图标(上分帝 Web;按需缓存);`innate.png` 先天共用图标;`talent_tree.png` 天赋树触发图标 | -| `assets/ui_icons/` | dota2.com.cn 通用 UI 图标(`cooldown.png` 等,技能详情底栏冷却图标)+ 平台 logo(`platform_douyin.png`,主播页) | -| `assets/streamer_avatars/` | 主播头像缓存(由 `fetch_streamers.py` 写入;上分帝 Web「主播」) | -| `assets/streamer_videos/` | 主播精选高光 mp4(手工放入;上分帝 Web「主播」卡片;勿提交,线上走 OSS `streamer-video/`) | -| `assets/ability_videos/` | 官网技能演示视频(webm/mp4;勿提交,由脚本拉取;线上由阿里云 OSS `climperor` 桶 `ability-video/` 前缀托管) | +| `pc/common.py` | 配置 IO、槽位几何、裁切、NCC 匹配、天梯遮罩、CDN 模板加载;re-export `shared.paths` 常量 | +| `pc/recognize.py` | 单帧识别;`recognize_image()` 供会话复用 | +| `pc/draft_session.py` | 整局选将跟踪、改判、皮肤规避策略 | +| `pc/gsi_watch.py` / `pc/gsi_setup.py` | GSI 监听与 cfg 安装 | +| `pc/fetch_cdn_templates.py` | 拉取 CDN 头像 + 生成 `shared/data/heroes.json`(含基础属性/血蓝;保留已有 `aliases`) | +| `pc/recommend.py` | 定位局分路过滤;克/搭/补全网格标记;调用 `draft_archetypes` 做推进/全球流/缺口画像与短文案 | +| `pc/draft_archetypes.py` | 规则阵容画像(推进/全球流/敌我缺口)+ `analysis` / `reasons` 文案(不接 AI) | +| `pc/autocalibrate.py` / `pc/calibrate.py` | ROI 自动 / 手动标定 | +| `pc/capture.py` | 屏幕捕获 | +| `pc/overlay.py` | 顶栏角色标签 + 网格「克/搭/补」方标 + 阵容分析横条(点击穿透) | +| `pc/evaluate.py` | 按 `pc/samples/labels.json` 批量评测 | +| `pc/roles.py` / `pc/modes.py` | 位置字、模式字(禁用网格在 `shared/grid.py`) | +| `pc/config.json` | 相对坐标、阈值、GSI、recommend 参数(`relations_path` 指向 `shared/data/relations.json`) | +| `pc/templates/cdn/` | 全英雄 CDN 模板(需提交或由脚本生成) | +| `pc/templates/roles/` / `pc/templates/modes/` | 位置 / 模式辅助模板 | +| `pc/assets/role_icons/` | Valve 选人筛选角色图标(`filter_*`,品红 chroma 去黑边) | +| `pc/requirements.txt` | PC 侧依赖(opencv/numpy/mss/openpyxl) | + +### shared/(共享包) + +| 路径 | 职责 | +|------|------| +| `shared/paths.py` | 全部路径常量单一来源(`ROOT`/`SHARED_DATA`/`HEROES_JSON`/`TEMPLATES_CDN`/`DATA`/`WEB_FRONTEND`/各 Web 资产目录);纯常量、零第三方依赖 | +| `shared/grid.py` | 英雄表 `hero_table()`、选人网格布局与禁用读取(cv2/numpy 懒加载,Web/CI 侧只取表不触发) | +| `shared/relations.py` | 定性克制/搭档边读写与名称解析;默认路径 `shared/data/relations.json` | +| `shared/hero_tags.py` | 中文定位 tags(核心/辅助/…/幻象) | +| `shared/http_utils.py` | 所有 fetch 脚本共用的 HTTP 请求、图标下载、Valve datafeed 加载(统一 UA / 超时;429/5xx 指数退避重试) | +| `shared/import_relations_xlsx.py` | 从选将笔记 xlsx 种子导入 `shared/data/relations.json` | +| `shared/fetch_stratz.py` | 拉取 STRATZ 对位优势 / 搭档协同 → `shared/data/stratz_matchups.json` / `shared/data/synergies.json`(需 token;`--mode matchups\|synergies`;供 `audit_relations.py`) | +| `shared/fetch_matchups.py` | 拉取 OpenDota 对位优势 → `shared/data/matchups.json`(供 `audit_relations.py` 交叉审计) | +| `shared/audit_relations.py` | OpenDota+STRATZ 交叉审计 `relations.json`,列出缺失英雄 | +| `shared/data/heroes.json` | 英雄表(id/key/attr/roles/aliases/abbr/tags + 基础属性/初始血蓝等;由 `pc/fetch_cdn_templates.py` 生成) | +| `shared/data/relations.json` | 定性克制边 + 搭档边(可提交;手改 JSON,Web 站点只读) | + +### web/(上分帝 Web) + +| 路径 | 职责 | +|------|------| +| `web/fetch_patches.py` | 拉取近一年(默认 365 天,`--days`/`--since`)版本列表 + 逐版本 `patchnotes` 详情 → `web/data/patches.json`;构建 id→名称/图标的 `lookup` 并下载引用到的物品/技能图标(`--no-icons` 跳过;`--force` 重抓全量;`--check` 只比对列表与本地 details,stdout JSON) | +| `web/requirements.txt` | Web 刷新依赖(`oss2`);Gitea Actions 仅装它 | +| `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) | +| `web/serve_relations.py` | 上分帝 Web 本地开发服务(`web/frontend/`;改 `web/data/*.json` 后刷新;`/streamer-video/` 提供主播高光 mp4,支持 HTTP Range) | +| `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` 可选本地拷贝技能/主播视频,生产部署勿用) | +| `web/deploy_relations.py` | 一键部署上分帝 Web 静态站点到 Cloudflare Pages(导出 + 资产预检 + `wrangler` 直传 + 绑域名;默认 OSS base 指向 `climperor` 桶的视频与静态图;凭据经 keyzoo 注入或 env) | +| `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 | +| `web/notify_site_traffic.py` | 上分帝 Web 日报 → 飞书 webhook(访问估数 + Gitea `web-daily`/`weekly`/`patch` 运行结论 + Pages 当日生产部署;`--dry-run` 只打印卡片) | +| `.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 | +| `.gitea/workflows/web-daily.yml` | 每日 06:00 CST:`web/refresh_web.py --tier daily`(self-hosted;需 CF + OSS Secrets;STRATZ 可选) | +| `.gitea/workflows/web-weekly.yml` | 周二 07:00 CST:`web/refresh_web.py --tier weekly`(需 `STRATZ_API_TOKEN` + CF + OSS) | +| `.gitea/workflows/web-patch.yml` | 每 6 小时:`web/refresh_web.py --tier patch`(`fetch_patches.py --check`,有新版本才拉详情与连带;需 CF + OSS) | +| `web/_cf_status.py` | 只读查询 Cloudflare Pages 项目 / 部署 / 自定义域名状态(凭据经 keyzoo 注入) | +| `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` 为配套辅助 | +| `web/fetch_hero_portraits.py` | 拉取官网横版头像 → `web/assets/hero_portraits/`(上分帝 Web) | +| `web/fetch_hero_items.py` | 拉取 OpenDota 热门装备 → `web/data/hero_items.json` + `web/assets/item_icons/` | +| `web/fetch_hero_stats.py` | 拉取 OpenDota 各段位场次/胜场 → `web/data/hero_stats.json`(**仅上分帝 Web**;勿写入 relations/heroes,勿进 recommend) | +| `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) | +| `web/fetch_leaderboards.py` | 拉取 Valve Immortal 四区榜 Top100 → `web/data/leaderboards.json`(**仅上分帝 Web「排行」**;无 MMR/account_id;勿进 recommend) | +| `web/fetch_streamers.py` | 从抖音主页补全 `web/data/streamers.json` 的昵称/签名/关注粉丝获赞/头像(手工名单 + 直播间/主页 URL;支持 `v.douyin.com` 短链;失败保留旧值;**不**探测开播(由 `fetch_streamer_live.py` 负责);**仅上分帝 Web「主播」**;勿进 recommend;进 `refresh_web` daily) | +| `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 | +| `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/` | +| `web/fetch_item_shop.py` | 官网商店 11 列目录(dota2.com.cn/itemscategory)+ 合成图 → `web/data/item_shop.json` + 图标 | +| `web/fetch_items_meta.py` | Valve/OpenDota 装备描述 → 机制标签 → `web/data/items_meta.json`(`%token%` 用 special_values 填数;查询类 tags 共用 `mechanic_tags.py`) | +| `web/mechanic_tags.py` | 上分帝 Web「机制」页共用标签(弱/强驱散、缠绕/缴械/沉默/锁闭/眩晕/妖术/破坏、睡眠/恐惧/嘲讽/致盲/束缚、隐身/虚无/吹风)+ 中文 labels;语义为「施加该效果」 | +| `web/loc_format.py` | Valve 文案共用:去 HTML、填充 `%token%` / `{s:token}` | +| `web/fetch_hero_abilities.py` | Valve herodata 技能/魔晶/神杖/天赋 + 驱散汇总 → `web/data/hero_abilities.json`;每条 ability 另有「施加」类 `tags`(与 `dispellable` 区分;`--tags-only` 可只重算);`has_scepter`/`has_shard` 只信 Valve 显式 flag,Valve 移除升级后残留的 `scepter_loc`/`shard_loc` 文案会被清空;逐级相同的 `cast_points`/`channel_times` 合并为单值(施法前摇/吟唱时间);可选 `--icons` / `--icons-only` 缓存 Steam CDN 技能图标 → `web/assets/ability_icons/`(先天用共用 `innate.png`,不拉 CDN) | +| `web/fetch_ability_videos.py` | 官网技能演示 webm/mp4(Steam CDN;限速)→ `web/assets/ability_videos/`;A 杖/魔晶赋予技能(`granted_by_scepter`/`granted_by_shard`)CDN 名用 `_aghanims_scepter`/`_aghanims_shard`,本地文件名仍用 ability key | +| `web/fetch_item_counter_stats.py` | OpenDota Explorer 近场次聚合:对阵英雄时敌方终局装备购买率/胜率,与同窗口该装备全局队伍基线作差 → `web/data/item_counter_stats.json`(可再生成缓存;观测证据,不作因果结论;weekly 软失败) | +| `web/item_fears.py` | 规则推导「英雄怕的装备」→ `web/data/hero_item_fears.json`;可选读取 `item_counter_stats.json`,对全部英雄的已有机制候选小幅调序;统计发现的新组合须经机制复核后写入 overrides | +| `web/data/patches.json` | 近一年版本列表 + 逐版本详情(`patches`/`lookup`/`details`;由 `fetch_patches.py` 生成,Web 版本页只读) | +| `web/data/hero_grid_order.json` | Web 站点四列网格顺序 | +| `web/data/hero_items.json` | 核心成品装备缓存(Web 站点只读;由 `fetch_hero_items.py` 生成) | +| `web/data/hero_stats.json` | 英雄各段位 pick/win + `totals` + `window_*`(OpenDota **近约 7 天**;Web「走势」Tab:胜率/上场率/场次;冠绝与超凡样本合并展示;由 `fetch_hero_stats.py` 生成;**不**参与局内推荐) | +| `web/data/hero_matches.json` | 同英雄近期比赛列表 + 终局出装/加点 + 可选购买时间(OpenDota;`--source league\|public\|both`;Web「近期比赛」Tab;由 `fetch_hero_matches.py` 生成;**不**参与局内推荐) | +| `web/data/leaderboards.json` | Valve Immortal 四区 Top100(`china`/`europe`/`americas`/`se_asia`;仅排名+昵称等;由 `fetch_leaderboards.py` 生成;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「主播」只读;**不**参与局内推荐) | +| `web/data/stratz_hero_meta.json` | STRATZ 各勋章段位近 N 周 pick/win(`weeks`/`latest`)+ **同段位最近 1 周分路**(`winWeek`+`positionIds`)+ `meta_board`(由 `fetch_stratz_meta.py` 生成;Web 英雄详情「走势」优先 + 顶层「走势」`#/trends` 近 N 周榜;**不**参与局内推荐) | +| `web/data/stratz_matchup_tops.json` | STRATZ 对位/协同 Top(counters/countered/synergies;由 `fetch_stratz_meta.py` 生成;Web「对位」Tab;与定性 `relations.json` 分开展示;**不**参与局内推荐) | +| `web/data/item_shop.json` | 商店 11 列目录(官网 basic/upgrade;由 `fetch_item_shop.py` 生成;物品页只读) | +| `web/data/items_meta.json` | 成品装备描述与机制标签(由 `fetch_items_meta.py` 生成) | +| `web/data/item_tag_overrides.json` | 装备标签手工加减(合并进 items_meta) | +| `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`): -- `samples/raw//` — GSI 会话截图与 `gsi.jsonl`;手动 `capture.py` 可写在 `raw/` 根下 -- `preview/` — 标定 / sheet 预览 -- `dist/` — 静态站点导出(`export_relations_site.py`) -- `results/` — 每局 JSON -- `failures/` — `--truth` 调试用错识裁切 -- `data/matchups.json` / `synergies.json` / `stratz_matchups.json` / `relations_audit.json` 等 — 可再生成缓存与报告 -- `assets/ability_videos/` — 官网技能演示(体积大,可再拉取) -- `assets/streamer_videos/` — 主播精选高光(体积大,手工放入后 OSS 同步) +- `pc/samples/raw//` — GSI 会话截图与 `gsi.jsonl`;手动 `capture.py` 可写在 `raw/` 根下 +- `pc/preview/` — 标定 / sheet 预览 +- `web/dist/` — 静态站点导出(`export_relations_site.py`) +- `pc/results/` — 每局 JSON +- `pc/failures/` — `--truth` 调试用错识裁切 +- `shared/data/matchups.json` / `synergies.json` / `stratz_matchups.json` / `relations_audit.json` 等 — 可再生成缓存与报告 +- `web/data/hero_stats.json` / `hero_matches.json` / `stratz_*.json` 等 — 可再生成缓存 +- `web/assets/ability_videos/` — 官网技能演示(体积大,可再拉取) +- `web/assets/streamer_videos/` — 主播精选高光(体积大,手工放入后 OSS 同步) ## 数据流 ``` -Dota 2 GSI → gsi_watch.py (:3223) - → 全量 payload → samples/raw//gsi.jsonl(可关) - → DraftSession 轮询截屏 → samples/raw// +Dota 2 GSI → pc/gsi_watch.py (:3223) + → 全量 payload → pc/samples/raw//gsi.jsonl(可关) + → DraftSession 轮询截屏 → pc/samples/raw// → recognize_image (CDN 模板 + 可选天梯遮罩) - → roles(分路字)/ grid(禁用 + 单元格)/ modes + → roles(分路字)/ shared.grid(禁用 + 单元格)/ modes → recommend 克/搭/补(relations + draft_archetypes 规则画像,本人槽位只信 GSI) → overlay 角色标签 + 网格克/搭/补 + 阵容分析条(可选) - → results/draft_*.json + 终端时间线 + → pc/results/draft_*.json + 终端时间线 ``` ## 技术约束(修改前必读) - **合规**:仅 GSI + 屏幕截图;**禁止**读进程内存、注入、绕过 VAC。 - **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 软失败刷新粉丝等字段(不阻断整档)。 -- **装备机制 / 怕的装备**:标签与技能汇总来自 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。 +- **克制 / 搭档数据**:机制克制/搭档只用定性边存 `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,禁止直接把高相关当因果克制。大段技能文案只进 `web/data/hero_abilities.json` / `items_meta.json`,**不要**塞进 `heroes.json`。本阶段仅上分帝 Web 展示,**不对局内出装推荐**。核心装「使用率」为 `hero_items` 列表内相对热度归一化,非绝对出场率、无段位维度。技能/物品「施加」类 tags(机制查询页)与技能 `dispellable`(效果能否被驱散)严格区分;**禁止**写入 recommend / relations。 - **机制查询页**:顶层 `#/mechanics[/{effect}]`;侧栏弱/强驱散 + 核心控制;结果为施加该效果的技能与物品。数据边界:仅 Web。 - **模板策略**:只维护 CDN 层。皮肤问题用会话策略(选人可改判、决策 `allow_revise=False`、best 帧偏 HERO_SELECTION),不要为皮肤加模板库,也不要复活 real 双层库。 - **顶栏时机**:皮肤在**全员选完后**才上顶栏;本机进决策时别人可能还在选——须保持 `strategy_tail` 视觉,禁止「一进 STRATEGY 就永久停读」。 - **坐标**:一律相对坐标(相对屏幕高 / 相对中心),勿写死像素分辨率。 - **宁可不认,不可乱认**:`min_score` + `min_margin` 双门控;不确定就 `null`。 - **平台**:面向 Windows;截屏依赖无边框/窗口模式。 -- **上分帝 Web 定时刷新**:Gitea Actions(self-hosted)跑 `refresh_web.py`;易变 STRATZ/stats/主播粉丝等 **不回写 git**(job 内现拉现部署)。数据-only 刷新不 bump `SITE_VERSION`。技能视频与手工 `relations.json` 不进定时。禁止把 STRATZ / hero_stats / matches / leaderboards / streamers 写入 recommend。 +- **上分帝 Web 定时刷新**:Gitea Actions(self-hosted)跑 `web/refresh_web.py`;易变 STRATZ/stats/主播粉丝等 **不回写 git**(job 内现拉现部署)。数据-only 刷新不 bump `SITE_VERSION`。技能视频与手工 `relations.json` 不进定时。禁止把 STRATZ / hero_stats / matches / leaderboards / streamers 写入 recommend。 ## 开发命令 ```powershell -pip install -r requirements.txt -pip install -r requirements-web.txt # 可选:本机 refresh_web / OSS(CI 会装) -python fetch_cdn_templates.py -python import_relations_xlsx.py # 可选:从选将笔记 xlsx 导入关系 -python fetch_hero_portraits.py # 官网横版头像(上分帝 Web) -python fetch_hero_items.py # OpenDota 热门装备缓存(上分帝 Web) -python fetch_hero_stats.py # OpenDota 各段位胜率/场次(上分帝 Web 走势兜底) -python fetch_stratz_meta.py # STRATZ 周胜率/分路/对位 Top(走势/对位/Meta;需 token) -python fetch_hero_matches.py # 同英雄近期比赛出装/加点(上分帝 Web;可 --heroes antimage;--workers 6;--enrich-item-times) -python fetch_leaderboards.py # Valve Immortal 四区 Top100(排行页) -python fetch_streamers.py # 抖音主播主页补全(主播页;手工名单;亦由 daily 定时软失败刷新) -python fetch_item_shop.py # 商店分类目录(物品页) -python fetch_items_meta.py # 装备描述 + 机制标签 -python fetch_hero_abilities.py # 英雄技能 / 驱散汇总 -python fetch_hero_abilities.py --tags-only # 只重算技能「施加」类 tags -python fetch_hero_abilities.py --icons-only # 缓存技能图标到 assets/ability_icons/ -python fetch_ability_videos.py # 官网技能演示(限速;默认 webm) -python fetch_item_counter_stats.py # OpenDota 对阵装备相对全局基线(可再生成缓存) -python item_fears.py # 英雄怕的装备 -python fetch_patches.py # 近一年版本日志 + 详情(版本页) -python fetch_patches.py --check # 只比对列表与本地 details,stdout JSON(has_new) -python refresh_web.py --tier patch --skip-deploy --skip-oss # 本机试跑某档(daily/weekly/patch/all) -python serve_relations.py # 上分帝 Web 本地开发服务(英雄 / 排行 / 主播 / 走势 / 机制 / 物品 / 版本) -python export_relations_site.py # 导出静态站点到 dist/relations/(可部署 Pages) -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 deploy_relations.py # 部署 dist/relations 到 Cloudflare Pages(凭据经 keyzoo 注入或 env;默认 dota2.refining.dev;视频与图标走 OSS) -python notify_site_traffic.py --dry-run # 上分帝 Web 日活摘要(飞书;需 CF + webhook env / keyzoo) -python _oss_static_assets.py upload # 图标/portrait 变更后同步 OSS(keyzoo voson-RAM) -python _cf_status.py # 只读查 CF Pages 部署 / 自定义域名状态 +pip install -r pc/requirements.txt +pip install -r web/requirements.txt # 可选:本机 refresh_web / OSS(CI 会装) +python pc/fetch_cdn_templates.py +python shared/import_relations_xlsx.py # 可选:从选将笔记 xlsx 导入关系 +python web/fetch_hero_portraits.py # 官网横版头像(上分帝 Web) +python web/fetch_hero_items.py # OpenDota 热门装备缓存(上分帝 Web) +python web/fetch_hero_stats.py # OpenDota 各段位胜率/场次(上分帝 Web 走势兜底) +python web/fetch_stratz_meta.py # STRATZ 周胜率/分路/对位 Top(走势/对位/Meta;需 token) +python web/fetch_hero_matches.py # 同英雄近期比赛出装/加点(上分帝 Web;可 --heroes antimage;--workers 6;--enrich-item-times) +python web/fetch_leaderboards.py # Valve Immortal 四区 Top100(排行页) +python web/fetch_streamers.py # 抖音主播主页补全(主播页;手工名单;亦由 daily 定时软失败刷新) +python web/fetch_streamer_live.py # 探测真实在播状态回写 is_live(主播页角标;daily;--dry-run 只打印) +python web/fetch_item_shop.py # 商店分类目录(物品页) +python web/fetch_items_meta.py # 装备描述 + 机制标签 +python web/fetch_hero_abilities.py # 英雄技能 / 驱散汇总 +python web/fetch_hero_abilities.py --tags-only # 只重算技能「施加」类 tags +python web/fetch_hero_abilities.py --icons-only # 缓存技能图标到 web/assets/ability_icons/ +python web/fetch_ability_videos.py # 官网技能演示(限速;默认 webm) +python web/fetch_item_counter_stats.py # OpenDota 对阵装备相对全局基线(可再生成缓存) +python web/item_fears.py # 英雄怕的装备 +python web/fetch_patches.py # 近一年版本日志 + 详情(版本页) +python web/fetch_patches.py --check # 只比对列表与本地 details,stdout JSON(has_new) +python web/refresh_web.py --tier patch --skip-deploy --skip-oss # 本机试跑某档(daily/weekly/patch/all) +python web/serve_relations.py # 上分帝 Web 本地开发服务(英雄 / 排行 / 主播 / 走势 / 机制 / 物品 / 版本) +python web/export_relations_site.py # 导出静态站点到 web/dist/relations/(可部署 Pages) +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 web/deploy_relations.py # 部署 web/dist/relations 到 Cloudflare Pages(凭据经 keyzoo 注入或 env;默认 dota2.refining.dev;视频与图标走 OSS) +python web/notify_site_traffic.py --dry-run # 上分帝 Web 日活摘要(飞书;需 CF + webhook env / keyzoo) +python web/_oss_static_assets.py upload # 图标/portrait 变更后同步 OSS(keyzoo voson-RAM) +python web/_cf_status.py # 只读查 CF Pages 部署 / 自定义域名状态 # Gitea Actions(self-hosted):web-daily / web-weekly / web-patch;Secrets 见 README「定时刷新」与各 workflow 头注释 -python gsi_setup.py --check -python gsi_watch.py --once -python recognize.py samples/raw/<帧>.png --sheet -python evaluate.py +python pc/gsi_setup.py --check +python pc/gsi_watch.py --once +python pc/recognize.py pc/samples/raw/<帧>.png --sheet +python pc/evaluate.py ``` ## 修改原则 - 只改任务所需逻辑,避免无关重构与大范围格式化。 - 代码标识符、日志、错误信息、注释用**英文**;用户可见摘要可用中文。 -- 改匹配阈值或裁切时,用 `evaluate.py` / 标注帧验证,并更新 `CHANGELOG.md` 与必要时的 `DESIGN.md`。 -- 改 GSI cfg 时同步核对 `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`。 -- 改 `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 详情条,勿塞机制文案。 -- 发版上分帝 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`。 +- 改匹配阈值或裁切时,用 `pc/evaluate.py` / 标注帧验证,并更新 `CHANGELOG.md` 与必要时的 `DESIGN.md`。 +- 改 GSI cfg 时同步核对 `pc/gsi_setup.py` 与 Dota `gamestate_integration` 目录。 +- 改 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`。 +- 改 `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 `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`。 ## 文档分工 diff --git a/CHANGELOG.md b/CHANGELOG.md index c826d3a..232b6e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,37 @@ ## [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 ### Added diff --git a/DESIGN.md b/DESIGN.md index 7b48627..dd99503 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -158,26 +158,27 @@ GSI 不含可靠 lobby 类型时,用右下金色勋章检测(`has_ranked_ove ``` climperor/ -├── config.json -├── data/ # heroes / relations / items / abilities / fears … -├── common.py / recognize.py / draft_session.py -├── gsi_watch.py / gsi_setup.py / overlay.py -├── recommend.py / relations.py / roles.py / grid.py / modes.py -├── http_utils.py / loc_format.py / hero_tags.py / item_fears.py -├── fetch_cdn_templates.py / fetch_hero_portraits.py / fetch_hero_items.py -├── fetch_item_shop.py / fetch_items_meta.py / fetch_hero_abilities.py -├── fetch_ability_videos.py / fetch_stratz.py / audit_relations.py -├── fetch_patches.py / refresh_web.py / import_relations_xlsx.py / serve_relations.py -├── export_relations_site.py / deploy_relations.py +├── pc/ # 局内选将识别(GSI + 截屏 + OpenCV) +│ ├── common.py / recognize.py / draft_session.py / gsi_watch.py +│ ├── recommend.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_matchups.py / fetch_stratz.py +│ └── data/heroes.json / data/relations.json ├── .gitea/workflows/ # site-traffic-notify + web-daily/weekly/patch -├── autocalibrate.py / calibrate.py / capture.py / evaluate.py -├── web/relations/ # 上分帝 Web(英雄/排行/物品/版本) -├── templates/cdn|roles|modes/ -├── assets/ # portraits / icons / videos(部分 gitignore) -└── samples/labels.json # 评测真值;raw// 见 .gitignore +└── README.md / AGENTS.md / DESIGN.md / CHANGELOG.md ``` -运行时目录 `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_heroes_for_gaps` | 缺口/补位注入所需锁人数 | 2 | | `recommend.archetypes` | 推进/全球流/缺口规则画像 | true | -| `recommend.relations_path` | 定性关系文件 | `data/relations.json` | +| `recommend.relations_path` | 定性关系文件 | `shared/data/relations.json` | | `recommend.role_tags` | 分路→角色标签过滤(1–5 号位;非定位局不过滤) | 见 config.json | | `overlay.enabled` | 选将顶栏角色标签悬浮层 | true | | `overlay.y_gap_rel` / `icon_h_rel` / `icon_gap_rel` | 标签相对屏幕高的间距与尺寸 | 0.008 / 0.016 / 0.002 | @@ -211,13 +212,13 @@ climperor/ ``` Dota 2 (-gamestateintegration) - → POST → gsi_watch.py :3223 - → samples/raw/{matchid}/gsi.jsonl(可选) + → POST → pc/gsi_watch.py :3223 + → pc/samples/raw/{matchid}/gsi.jsonl(可选) → 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 成批揭晓,本轮结束前互不可见——因此必须在 @@ -230,26 +231,26 @@ Dota 2 (-gamestateintegration) - **触发**:敌方至少锁定 1 人(`min_enemies`)后开始;本人锁人后清空标记。 - **分路**:定位匹配画面字(`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`),与网格定性「克/怕/搭」分开展示;均不进局内推荐。 -- **阵容画像(规则,不接 AI)**:[`draft_archetypes.py`](draft_archetypes.py) 识别敌方推进/全球流、敌我 tag 缺口(缺控制/爆发/输出/先手);输出一句 `analysis`(如 `敌:缺控制·偏推进 | 我:缺爆发`)与每英雄短 `reasons`。 +- **关系模型**:机制克制/搭档(`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**:`serve_relations.py` 本地开发服务;仿选将网格查看克制/被克制/搭档;数据手改 `data/relations.json` 或从 xlsx 导入。 - 另有顶级 **排行**、**走势**(`#/trends[/bracket]`,近 8 周高胜率/上场率榜)、**物品**、**版本** 页;目录见 `data/item_shop.json` / `data/patches.json` / `data/leaderboards.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 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`(静默,不触发 `hashchange`,无回环);前进/后退靠 `hashchange` 监听重 parse + 校验 + render。 搜索 debounce 300ms + `replaceState` 防刷历史栈;坏链接丢弃该项不崩。路由逻辑集中在 - `web/relations/router.js`,`app.js` 只在 `main()` 装 `installRouter` + 各 state 变更点 + `web/frontend/router.js`,`app.js` 只在 `main()` 装 `installRouter` + 各 state 变更点 调 `syncStateToUrl`。 -- **不做**:避用标、Steam 登录页、爬 Dotabuff;关系**不**写入 `data/heroes.json`;不接实时 AI。 +- **不做**:避用标、Steam 登录页、爬 Dotabuff;关系**不**写入 `shared/data/heroes.json`;不接实时 AI。 --- diff --git a/README.md b/README.md index a0606f5..a6761ba 100644 --- a/README.md +++ b/README.md @@ -21,53 +21,56 @@ Dota 2 **天梯选将识别**:从选人 / 决策时间截图中识别双方 10 ## 安装 ```powershell -pip install -r requirements.txt -python fetch_cdn_templates.py # 一次性:data/heroes.json(含基础属性/血蓝)+ templates/cdn/ +pip install -r pc/requirements.txt +python pc/fetch_cdn_templates.py # 一次性:shared/data/heroes.json(含基础属性/血蓝)+ pc/templates/cdn/ # 可选:从选将笔记 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/relations/`):英雄克制/搭档、排行、主播、物品、版本等内容站点,与局内选将识别解耦。 +上分帝 Web 是独立子项目(`web/`):英雄克制/搭档、排行、主播、物品、版本等内容站点,与局内选将识别解耦。 ```powershell -python fetch_hero_portraits.py # 官网竖版头像 → assets/hero_portraits/ -python fetch_hero_items.py # OpenDota 热度 + Valve 中文名 + Steam 图标 → data/hero_items.json -python fetch_hero_stats.py # OpenDota 各段位胜率/场次 → data/hero_stats.json(Web 走势兜底) -python fetch_stratz_meta.py # STRATZ 周胜率/分路/对位 Top → stratz_hero_meta.json(需 token;见 .env.example) -python fetch_hero_matches.py # 同英雄近期比赛出装/加点(含购买时间)→ data/hero_matches.json -# 既有行补时间:python fetch_hero_matches.py --enrich-item-times [--heroes juggernaut] -python fetch_item_shop.py # 官网商店 11 列目录 → data/item_shop.json -python fetch_items_meta.py # 装备描述 + 机制标签 → data/items_meta.json -python fetch_hero_abilities.py # 英雄技能 / 驱散汇总 → data/hero_abilities.json -python fetch_ability_videos.py # 官网技能演示 webm(限速)→ assets/ability_videos/ -python fetch_item_counter_stats.py # OpenDota 对阵购买率/胜率相对全局基线 → 可再生成缓存 -python item_fears.py # 规则推导「怕的装备」→ data/hero_item_fears.json -python fetch_patches.py # 近一年版本日志 + 详情 → data/patches.json(版本页) -python fetch_leaderboards.py # Valve Immortal 四区 Top100 → data/leaderboards.json(排行页) -python fetch_streamers.py # 抖音主播主页补全 → data/streamers.json(主播页;手工名单) -python serve_relations.py # 本地开发服务 http://127.0.0.1:8765(改 JSON 后刷新) +python web/fetch_hero_portraits.py # 官网竖版头像 → web/assets/hero_portraits/ +python web/fetch_hero_items.py # OpenDota 热度 + Valve 中文名 + Steam 图标 → web/data/hero_items.json +python web/fetch_hero_stats.py # OpenDota 各段位胜率/场次 → web/data/hero_stats.json(Web 走势兜底) +python web/fetch_stratz_meta.py # STRATZ 周胜率/分路/对位 Top → stratz_hero_meta.json(需 token;见 .env.example) +python web/fetch_hero_matches.py # 同英雄近期比赛出装/加点(含购买时间)→ web/data/hero_matches.json +# 既有行补时间:python web/fetch_hero_matches.py --enrich-item-times [--heroes juggernaut] +python web/fetch_item_shop.py # 官网商店 11 列目录 → web/data/item_shop.json +python web/fetch_items_meta.py # 装备描述 + 机制标签 → web/data/items_meta.json +python web/fetch_hero_abilities.py # 英雄技能 / 驱散汇总 → web/data/hero_abilities.json +python web/fetch_ability_videos.py # 官网技能演示 webm(限速)→ web/assets/ability_videos/ +python web/fetch_item_counter_stats.py # OpenDota 对阵购买率/胜率相对全局基线 → 可再生成缓存 +python web/item_fears.py # 规则推导「怕的装备」→ web/data/hero_item_fears.json +python web/fetch_patches.py # 近一年版本日志 + 详情 → web/data/patches.json(版本页) +python web/fetch_leaderboards.py # Valve Immortal 四区 Top100 → web/data/leaderboards.json(排行页) +python web/fetch_streamers.py # 抖音主播主页补全 → web/data/streamers.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 ```powershell -python deploy_relations.py +python web/deploy_relations.py # 串联:导出 → 资产完整性预检 → wrangler 直传 → 绑定自定义域名 # 凭据经 keyzoo 注入(refining/cloudflare 的 Global API Key); # 或手动设 $env:CLOUDFLARE_EMAIL / $env:CLOUDFLARE_API_KEY 后运行 # 默认项目 climperor-relations、域名 dota2.refining.dev(可用 --project-name / --domain 覆盖) # 技能演示视频与静态图标默认走阿里云 OSS 桶 climperor(Pages 只部署 ~2MB HTML/JS/data) -python _oss_static_assets.py upload # patch/新英雄后同步图标到 OSS -python _cf_status.py # 部署后只读核对(项目 / 部署 / 域名状态) +python web/_oss_static_assets.py upload # patch/新英雄后同步图标到 OSS +python web/_cf_status.py # 部署后只读核对(项目 / 部署 / 域名状态) ``` ### 定时刷新(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) | 档位 | 拉取内容 | |----------|-------------|------|----------| @@ -75,17 +78,17 @@ python _cf_status.py # 部署后只读核对(项目 / 部署 / 域名状态 | `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 | -**版本检测:** `--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 -pip install -r requirements.txt -r requirements-web.txt -python fetch_patches.py --check -python refresh_web.py --tier patch --dry-run -python refresh_web.py --tier patch --skip-deploy --skip-oss +pip install -r web/requirements.txt +python web/fetch_patches.py --check +python web/refresh_web.py --tier patch --dry-run +python web/refresh_web.py --tier patch --skip-deploy --skip-oss # 真实拉数+部署(需 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。 @@ -94,32 +97,32 @@ python refresh_web.py --tier patch --skip-deploy --skip-oss | 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 内若触发连带时不需要) | | `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)、**近期比赛**(终局出装/加点)、**主播**(常玩该英雄的收录主播)、**版本变更**(该英雄近一年各版本改动)。 英雄页上方网格固定高度;点选技能 / 核心装备 / 被克装备后,详情显示在下方(装备含合成,非弹窗)。 -顶部另有 **排行** 页:Valve Immortal 四区榜各 Top 100(中国 / 欧洲 / 美洲 / 东南亚);数据来自 `data/leaderboards.json`(`python 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/`。 +顶部另有 **排行** 页:Valve Immortal 四区榜各 Top 100(中国 / 欧洲 / 美洲 / 东南亚);数据来自 `web/data/leaderboards.json`(`python web/fetch_leaderboards.py`)。 +顶部另有 **主播** 页:手工收录直播间/主页(首版仅抖音),抖音式卡片展示昵称、签名、关注/粉丝/获赞、常用英雄与可选精选高光视频(滚入视口中部自动播放;有 `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 周胜率数据)。 -顶部另有 **物品** 页:对齐 [官网商店物品](https://www.dota2.com.cn/items/index.htm) 的 **11 列竖排**(基础分类 5 列 + 合成分类 6 列),点选后详情显示在右侧(含合成组件)。目录来自 `data/item_shop.json`(`python fetch_item_shop.py`)。 -顶部另有 **版本** 页:默认展示最新版本完整改动(综合 / 物品 / 中立物品 / 英雄),下拉切换近一年其它版本;数据来自 `data/patches.json`(`python 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`。**尚无对局内出装推荐**。 +顶部另有 **物品** 页:对齐 [官网商店物品](https://www.dota2.com.cn/items/index.htm) 的 **11 列竖排**(基础分类 5 列 + 合成分类 6 列),点选后详情显示在右侧(含合成组件)。目录来自 `web/data/item_shop.json`(`python web/fetch_item_shop.py`)。 +顶部另有 **版本** 页:默认展示最新版本完整改动(综合 / 物品 / 中立物品 / 英雄),下拉切换近一年其它版本;数据来自 `web/data/patches.json`(`python web/fetch_patches.py`,近一年窗口可用 `--days` / `--since` 调)。 +数据在 `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 自动跟踪) ```powershell -python gsi_setup.py # 写入 Dota GSI 配置 +python pc/gsi_setup.py # 写入 Dota GSI 配置 # Steam → Dota 2 → 属性 → 启动项加上 -gamestateintegration,重启游戏 -python gsi_watch.py +python pc/gsi_watch.py ``` -进入英雄选择后自动跟踪;敌方锁 ≥1 人后按关系与阵容画像在英雄网格标出 **克 / 搭 / 补**,并显示一句阵容分析(需 `data/relations.json`;定位局按分路过滤)。凑齐 10 人或超时后写出 `results/draft_<时间戳>.json`(该目录已 gitignore)。 -会话截图按对局落在 `samples/raw//`;默认同时把每包 GSI JSON 追加到同目录 `gsi.jsonl`(`--no-dump-gsi` 可关)。 +进入英雄选择后自动跟踪;敌方锁 ≥1 人后按关系与阵容画像在英雄网格标出 **克 / 搭 / 补**,并显示一句阵容分析(需 `shared/data/relations.json`;定位局按分路过滤)。凑齐 10 人或超时后写出 `pc/results/draft_<时间戳>.json`(该目录已 gitignore)。 +会话截图按对局落在 `pc/samples/raw//`;默认同时把每包 GSI JSON 追加到同目录 `gsi.jsonl`(`--no-dump-gsi` 可关)。 > 主菜单收不到 GSI 是正常的:客户端**第一次载入对局后**才开始推送。看到 `[gsi] connected` 才算链路通。 @@ -128,30 +131,30 @@ python gsi_watch.py ### 截图 ```powershell -python capture.py # 单张 → samples/raw/ -python capture.py --loop 300 3 # 每 3 秒一张,持续 300 秒 +python pc/capture.py # 单张 → pc/samples/raw/ +python pc/capture.py --loop 300 3 # 每 3 秒一张,持续 300 秒 ``` -GSI 跟踪时自动写入 `samples/raw//draft_*.png`(含 milestone / best)。 +GSI 跟踪时自动写入 `pc/samples/raw//draft_*.png`(含 milestone / best)。 需无边框窗口或窗口模式(独占全屏可能黑帧)。 ### 标定 ROI(一次性) ```powershell -python autocalibrate.py samples/raw//<某帧>.png -# 失败时退路:python calibrate.py samples/raw//<某帧>.png +python pc/autocalibrate.py pc/samples/raw//<某帧>.png +# 失败时退路:python pc/calibrate.py pc/samples/raw//<某帧>.png ``` ### 识别 / 评测 ```powershell -python recognize.py samples/raw//<帧>.png -python recognize.py samples/raw//<帧>.png --sheet -python recognize.py samples/raw//<帧>.png --truth hero1,...,hero10 -python evaluate.py # 按 samples/labels.json 批量评测 +python pc/recognize.py pc/samples/raw//<帧>.png +python pc/recognize.py pc/samples/raw//<帧>.png --sheet +python pc/recognize.py pc/samples/raw//<帧>.png --truth hero1,...,hero10 +python pc/evaluate.py # 按 pc/samples/labels.json 批量评测 ``` -辅助脚本:`roles.py`(位置字)、`grid.py`(禁用名单)、`modes.py`(模式字)。 +辅助脚本:`pc/roles.py`(位置字)、`shared/grid.py`(禁用名单)、`pc/modes.py`(模式字)。 ## 截图要求 diff --git a/assets/role_icons/Carry.png b/pc/assets/role_icons/Carry.png similarity index 100% rename from assets/role_icons/Carry.png rename to pc/assets/role_icons/Carry.png diff --git a/assets/role_icons/Disabler.png b/pc/assets/role_icons/Disabler.png similarity index 100% rename from assets/role_icons/Disabler.png rename to pc/assets/role_icons/Disabler.png diff --git a/assets/role_icons/Durable.png b/pc/assets/role_icons/Durable.png similarity index 100% rename from assets/role_icons/Durable.png rename to pc/assets/role_icons/Durable.png diff --git a/assets/role_icons/Escape.png b/pc/assets/role_icons/Escape.png similarity index 100% rename from assets/role_icons/Escape.png rename to pc/assets/role_icons/Escape.png diff --git a/assets/role_icons/Initiator.png b/pc/assets/role_icons/Initiator.png similarity index 100% rename from assets/role_icons/Initiator.png rename to pc/assets/role_icons/Initiator.png diff --git a/assets/role_icons/Nuker.png b/pc/assets/role_icons/Nuker.png similarity index 100% rename from assets/role_icons/Nuker.png rename to pc/assets/role_icons/Nuker.png diff --git a/assets/role_icons/Pusher.png b/pc/assets/role_icons/Pusher.png similarity index 100% rename from assets/role_icons/Pusher.png rename to pc/assets/role_icons/Pusher.png diff --git a/assets/role_icons/Support.png b/pc/assets/role_icons/Support.png similarity index 100% rename from assets/role_icons/Support.png rename to pc/assets/role_icons/Support.png diff --git a/autocalibrate.py b/pc/autocalibrate.py similarity index 99% rename from autocalibrate.py rename to pc/autocalibrate.py index 2cf13cd..6cfa21d 100644 --- a/autocalibrate.py +++ b/pc/autocalibrate.py @@ -12,9 +12,12 @@ Writes slot geometry into config.json and preview/autocalibrate_check.png. """ import sys -import time from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import time + import cv2 import numpy as np diff --git a/calibrate.py b/pc/calibrate.py similarity index 96% rename from calibrate.py rename to pc/calibrate.py index 032faef..7b9e7a3 100644 --- a/calibrate.py +++ b/pc/calibrate.py @@ -11,6 +11,10 @@ and stored as resolution-independent relative coordinates. """ import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + import time import cv2 diff --git a/capture.py b/pc/capture.py similarity index 98% rename from capture.py rename to pc/capture.py index a5fbc1a..45cc138 100644 --- a/capture.py +++ b/pc/capture.py @@ -10,11 +10,14 @@ Notes: - Frames are saved as PNG at native resolution, named cap_HHMMSS.png. """ -import json import sys -import time from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import json +import time + import cv2 import mss import numpy as np diff --git a/common.py b/pc/common.py similarity index 74% rename from common.py rename to pc/common.py index 5feebdd..c21c0ee 100644 --- a/common.py +++ b/pc/common.py @@ -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 -from pathlib import Path import cv2 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" -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: diff --git a/config.json b/pc/config.json similarity index 98% rename from config.json rename to pc/config.json index f0cd245..eeb2ae9 100644 --- a/config.json +++ b/pc/config.json @@ -108,7 +108,7 @@ "min_enemies": 1, "min_heroes_for_gaps": 2, "archetypes": true, - "relations_path": "data/relations.json", + "relations_path": "shared/data/relations.json", "role_tags": { "1": ["Carry"], "2": ["Carry", "Nuker", "Escape"], diff --git a/draft_archetypes.py b/pc/draft_archetypes.py similarity index 98% rename from draft_archetypes.py rename to pc/draft_archetypes.py index d9da143..0c3f701 100644 --- a/draft_archetypes.py +++ b/pc/draft_archetypes.py @@ -6,10 +6,15 @@ and short Chinese analysis / reason strings for recommend marks. 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 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. PUSH_CORE = frozenset({ diff --git a/draft_session.py b/pc/draft_session.py similarity index 99% rename from draft_session.py rename to pc/draft_session.py index 4f0eb46..f67a7d1 100644 --- a/draft_session.py +++ b/pc/draft_session.py @@ -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. """ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + import time import mss 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 recognize import recognize_image from recommend import ally_keys, enemy_keys, load_relations, suggest_marks @@ -136,7 +141,7 @@ class DraftSession: rel = self.relations or {} if not rel.get("counters") and not rel.get("synergies"): 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") started = time.monotonic() pending: dict[int, tuple[str, int]] = {} diff --git a/evaluate.py b/pc/evaluate.py similarity index 96% rename from evaluate.py rename to pc/evaluate.py index 6c29968..4de705e 100644 --- a/evaluate.py +++ b/pc/evaluate.py @@ -7,8 +7,12 @@ Usage: python evaluate.py """ -import json import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import json import cv2 diff --git a/fetch_cdn_templates.py b/pc/fetch_cdn_templates.py similarity index 96% rename from fetch_cdn_templates.py rename to pc/fetch_cdn_templates.py index 3fde606..94a5426 100644 --- a/fetch_cdn_templates.py +++ b/pc/fetch_cdn_templates.py @@ -4,21 +4,25 @@ Usage: python fetch_cdn_templates.py 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 Preserves manually curated `aliases` / `abbr` from an existing heroes.json on rewrite. """ -import json +import sys from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import json + import cv2 import numpy as np from common import HEROES_JSON, TEMPLATES_CDN, load_config -from hero_tags import tags_for_hero -from http_utils import fetch_hero_list, http_bytes, http_json +from shared.hero_tags import tags_for_hero +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. IMG_URL = "https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/heroes/{key}.png" diff --git a/gsi_setup.py b/pc/gsi_setup.py similarity index 98% rename from gsi_setup.py rename to pc/gsi_setup.py index 2556084..487fc8f 100644 --- a/gsi_setup.py +++ b/pc/gsi_setup.py @@ -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. """ -import re import sys from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import re + from common import load_config CFG_NAME = "gamestate_integration_climperor.cfg" diff --git a/gsi_watch.py b/pc/gsi_watch.py similarity index 99% rename from gsi_watch.py rename to pc/gsi_watch.py index 7086d13..fa55240 100644 --- a/gsi_watch.py +++ b/pc/gsi_watch.py @@ -21,12 +21,15 @@ With gsi.dump_payloads (default on), every POST body is appended to samples/raw//gsi.jsonl for later analysis. """ -import json import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import json import threading import time from http.server import BaseHTTPRequestHandler, HTTPServer -from pathlib import Path import mss diff --git a/modes.py b/pc/modes.py similarity index 98% rename from modes.py rename to pc/modes.py index 503660d..b9366dd 100644 --- a/modes.py +++ b/pc/modes.py @@ -1,7 +1,10 @@ """Read the game-mode label under the top-center timer (e.g. 全英雄选择).""" +import sys from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + import cv2 import numpy as np diff --git a/overlay.py b/pc/overlay.py similarity index 99% rename from overlay.py rename to pc/overlay.py index fc78580..ceed0d0 100644 --- a/overlay.py +++ b/pc/overlay.py @@ -6,8 +6,12 @@ set_grid_marks(), and set_analysis(); geometry uses relative coords. from __future__ import annotations -import json import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import json import threading import tkinter as tk diff --git a/recognize.py b/pc/recognize.py similarity index 98% rename from recognize.py rename to pc/recognize.py index 8f01166..d9e5681 100644 --- a/recognize.py +++ b/pc/recognize.py @@ -12,8 +12,12 @@ misrecognized crops to failures/ (debug only, gitignored). recognize_image() is the reusable entry point used by gsi_watch.py. """ -import json import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import json import time import cv2 diff --git a/recommend.py b/pc/recommend.py similarity index 96% rename from recommend.py rename to pc/recommend.py index d2b3f5b..a2b8265 100644 --- a/recommend.py +++ b/pc/recommend.py @@ -8,11 +8,16 @@ Mark available heroes with 克 / 搭 / 补: Role-queue filters by position tags; otherwise all heroes are candidates. 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 +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + from draft_archetypes import ( answer_for_candidate, collect_reasons, @@ -21,9 +26,9 @@ from draft_archetypes import ( format_analysis, tag_profile, ) -from grid import hero_table -from hero_tags import tags_for_hero -from relations import DEFAULT_RELATIONS, indexes, load_relations +from shared.grid import hero_table +from shared.hero_tags import tags_for_hero +from shared.relations import DEFAULT_RELATIONS, indexes, load_relations DEFAULT_ROLE_TAGS = { "1": ["Carry"], diff --git a/requirements.txt b/pc/requirements.txt similarity index 100% rename from requirements.txt rename to pc/requirements.txt diff --git a/roles.py b/pc/roles.py similarity index 98% rename from roles.py rename to pc/roles.py index 21e1184..616396b 100644 --- a/roles.py +++ b/pc/roles.py @@ -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. """ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + import cv2 import numpy as np diff --git a/samples/labels.json b/pc/samples/labels.json similarity index 100% rename from samples/labels.json rename to pc/samples/labels.json diff --git a/templates/cdn/abaddon.png b/pc/templates/cdn/abaddon.png similarity index 100% rename from templates/cdn/abaddon.png rename to pc/templates/cdn/abaddon.png diff --git a/templates/cdn/abyssal_underlord.png b/pc/templates/cdn/abyssal_underlord.png similarity index 100% rename from templates/cdn/abyssal_underlord.png rename to pc/templates/cdn/abyssal_underlord.png diff --git a/templates/cdn/alchemist.png b/pc/templates/cdn/alchemist.png similarity index 100% rename from templates/cdn/alchemist.png rename to pc/templates/cdn/alchemist.png diff --git a/templates/cdn/ancient_apparition.png b/pc/templates/cdn/ancient_apparition.png similarity index 100% rename from templates/cdn/ancient_apparition.png rename to pc/templates/cdn/ancient_apparition.png diff --git a/templates/cdn/antimage.png b/pc/templates/cdn/antimage.png similarity index 100% rename from templates/cdn/antimage.png rename to pc/templates/cdn/antimage.png diff --git a/templates/cdn/arc_warden.png b/pc/templates/cdn/arc_warden.png similarity index 100% rename from templates/cdn/arc_warden.png rename to pc/templates/cdn/arc_warden.png diff --git a/templates/cdn/axe.png b/pc/templates/cdn/axe.png similarity index 100% rename from templates/cdn/axe.png rename to pc/templates/cdn/axe.png diff --git a/templates/cdn/bane.png b/pc/templates/cdn/bane.png similarity index 100% rename from templates/cdn/bane.png rename to pc/templates/cdn/bane.png diff --git a/templates/cdn/batrider.png b/pc/templates/cdn/batrider.png similarity index 100% rename from templates/cdn/batrider.png rename to pc/templates/cdn/batrider.png diff --git a/templates/cdn/beastmaster.png b/pc/templates/cdn/beastmaster.png similarity index 100% rename from templates/cdn/beastmaster.png rename to pc/templates/cdn/beastmaster.png diff --git a/templates/cdn/bloodseeker.png b/pc/templates/cdn/bloodseeker.png similarity index 100% rename from templates/cdn/bloodseeker.png rename to pc/templates/cdn/bloodseeker.png diff --git a/templates/cdn/bounty_hunter.png b/pc/templates/cdn/bounty_hunter.png similarity index 100% rename from templates/cdn/bounty_hunter.png rename to pc/templates/cdn/bounty_hunter.png diff --git a/templates/cdn/brewmaster.png b/pc/templates/cdn/brewmaster.png similarity index 100% rename from templates/cdn/brewmaster.png rename to pc/templates/cdn/brewmaster.png diff --git a/templates/cdn/bristleback.png b/pc/templates/cdn/bristleback.png similarity index 100% rename from templates/cdn/bristleback.png rename to pc/templates/cdn/bristleback.png diff --git a/templates/cdn/broodmother.png b/pc/templates/cdn/broodmother.png similarity index 100% rename from templates/cdn/broodmother.png rename to pc/templates/cdn/broodmother.png diff --git a/templates/cdn/centaur.png b/pc/templates/cdn/centaur.png similarity index 100% rename from templates/cdn/centaur.png rename to pc/templates/cdn/centaur.png diff --git a/templates/cdn/chaos_knight.png b/pc/templates/cdn/chaos_knight.png similarity index 100% rename from templates/cdn/chaos_knight.png rename to pc/templates/cdn/chaos_knight.png diff --git a/templates/cdn/chen.png b/pc/templates/cdn/chen.png similarity index 100% rename from templates/cdn/chen.png rename to pc/templates/cdn/chen.png diff --git a/templates/cdn/clinkz.png b/pc/templates/cdn/clinkz.png similarity index 100% rename from templates/cdn/clinkz.png rename to pc/templates/cdn/clinkz.png diff --git a/templates/cdn/crystal_maiden.png b/pc/templates/cdn/crystal_maiden.png similarity index 100% rename from templates/cdn/crystal_maiden.png rename to pc/templates/cdn/crystal_maiden.png diff --git a/templates/cdn/dark_seer.png b/pc/templates/cdn/dark_seer.png similarity index 100% rename from templates/cdn/dark_seer.png rename to pc/templates/cdn/dark_seer.png diff --git a/templates/cdn/dark_willow.png b/pc/templates/cdn/dark_willow.png similarity index 100% rename from templates/cdn/dark_willow.png rename to pc/templates/cdn/dark_willow.png diff --git a/templates/cdn/dawnbreaker.png b/pc/templates/cdn/dawnbreaker.png similarity index 100% rename from templates/cdn/dawnbreaker.png rename to pc/templates/cdn/dawnbreaker.png diff --git a/templates/cdn/dazzle.png b/pc/templates/cdn/dazzle.png similarity index 100% rename from templates/cdn/dazzle.png rename to pc/templates/cdn/dazzle.png diff --git a/templates/cdn/death_prophet.png b/pc/templates/cdn/death_prophet.png similarity index 100% rename from templates/cdn/death_prophet.png rename to pc/templates/cdn/death_prophet.png diff --git a/templates/cdn/disruptor.png b/pc/templates/cdn/disruptor.png similarity index 100% rename from templates/cdn/disruptor.png rename to pc/templates/cdn/disruptor.png diff --git a/templates/cdn/doom_bringer.png b/pc/templates/cdn/doom_bringer.png similarity index 100% rename from templates/cdn/doom_bringer.png rename to pc/templates/cdn/doom_bringer.png diff --git a/templates/cdn/dragon_knight.png b/pc/templates/cdn/dragon_knight.png similarity index 100% rename from templates/cdn/dragon_knight.png rename to pc/templates/cdn/dragon_knight.png diff --git a/templates/cdn/drow_ranger.png b/pc/templates/cdn/drow_ranger.png similarity index 100% rename from templates/cdn/drow_ranger.png rename to pc/templates/cdn/drow_ranger.png diff --git a/templates/cdn/earth_spirit.png b/pc/templates/cdn/earth_spirit.png similarity index 100% rename from templates/cdn/earth_spirit.png rename to pc/templates/cdn/earth_spirit.png diff --git a/templates/cdn/earthshaker.png b/pc/templates/cdn/earthshaker.png similarity index 100% rename from templates/cdn/earthshaker.png rename to pc/templates/cdn/earthshaker.png diff --git a/templates/cdn/elder_titan.png b/pc/templates/cdn/elder_titan.png similarity index 100% rename from templates/cdn/elder_titan.png rename to pc/templates/cdn/elder_titan.png diff --git a/templates/cdn/ember_spirit.png b/pc/templates/cdn/ember_spirit.png similarity index 100% rename from templates/cdn/ember_spirit.png rename to pc/templates/cdn/ember_spirit.png diff --git a/templates/cdn/enchantress.png b/pc/templates/cdn/enchantress.png similarity index 100% rename from templates/cdn/enchantress.png rename to pc/templates/cdn/enchantress.png diff --git a/templates/cdn/enigma.png b/pc/templates/cdn/enigma.png similarity index 100% rename from templates/cdn/enigma.png rename to pc/templates/cdn/enigma.png diff --git a/templates/cdn/faceless_void.png b/pc/templates/cdn/faceless_void.png similarity index 100% rename from templates/cdn/faceless_void.png rename to pc/templates/cdn/faceless_void.png diff --git a/templates/cdn/furion.png b/pc/templates/cdn/furion.png similarity index 100% rename from templates/cdn/furion.png rename to pc/templates/cdn/furion.png diff --git a/templates/cdn/grimstroke.png b/pc/templates/cdn/grimstroke.png similarity index 100% rename from templates/cdn/grimstroke.png rename to pc/templates/cdn/grimstroke.png diff --git a/templates/cdn/gyrocopter.png b/pc/templates/cdn/gyrocopter.png similarity index 100% rename from templates/cdn/gyrocopter.png rename to pc/templates/cdn/gyrocopter.png diff --git a/templates/cdn/hoodwink.png b/pc/templates/cdn/hoodwink.png similarity index 100% rename from templates/cdn/hoodwink.png rename to pc/templates/cdn/hoodwink.png diff --git a/templates/cdn/huskar.png b/pc/templates/cdn/huskar.png similarity index 100% rename from templates/cdn/huskar.png rename to pc/templates/cdn/huskar.png diff --git a/templates/cdn/invoker.png b/pc/templates/cdn/invoker.png similarity index 100% rename from templates/cdn/invoker.png rename to pc/templates/cdn/invoker.png diff --git a/templates/cdn/jakiro.png b/pc/templates/cdn/jakiro.png similarity index 100% rename from templates/cdn/jakiro.png rename to pc/templates/cdn/jakiro.png diff --git a/templates/cdn/juggernaut.png b/pc/templates/cdn/juggernaut.png similarity index 100% rename from templates/cdn/juggernaut.png rename to pc/templates/cdn/juggernaut.png diff --git a/templates/cdn/keeper_of_the_light.png b/pc/templates/cdn/keeper_of_the_light.png similarity index 100% rename from templates/cdn/keeper_of_the_light.png rename to pc/templates/cdn/keeper_of_the_light.png diff --git a/templates/cdn/kez.png b/pc/templates/cdn/kez.png similarity index 100% rename from templates/cdn/kez.png rename to pc/templates/cdn/kez.png diff --git a/templates/cdn/kunkka.png b/pc/templates/cdn/kunkka.png similarity index 100% rename from templates/cdn/kunkka.png rename to pc/templates/cdn/kunkka.png diff --git a/templates/cdn/largo.png b/pc/templates/cdn/largo.png similarity index 100% rename from templates/cdn/largo.png rename to pc/templates/cdn/largo.png diff --git a/templates/cdn/legion_commander.png b/pc/templates/cdn/legion_commander.png similarity index 100% rename from templates/cdn/legion_commander.png rename to pc/templates/cdn/legion_commander.png diff --git a/templates/cdn/leshrac.png b/pc/templates/cdn/leshrac.png similarity index 100% rename from templates/cdn/leshrac.png rename to pc/templates/cdn/leshrac.png diff --git a/templates/cdn/lich.png b/pc/templates/cdn/lich.png similarity index 100% rename from templates/cdn/lich.png rename to pc/templates/cdn/lich.png diff --git a/templates/cdn/life_stealer.png b/pc/templates/cdn/life_stealer.png similarity index 100% rename from templates/cdn/life_stealer.png rename to pc/templates/cdn/life_stealer.png diff --git a/templates/cdn/lina.png b/pc/templates/cdn/lina.png similarity index 100% rename from templates/cdn/lina.png rename to pc/templates/cdn/lina.png diff --git a/templates/cdn/lion.png b/pc/templates/cdn/lion.png similarity index 100% rename from templates/cdn/lion.png rename to pc/templates/cdn/lion.png diff --git a/templates/cdn/lone_druid.png b/pc/templates/cdn/lone_druid.png similarity index 100% rename from templates/cdn/lone_druid.png rename to pc/templates/cdn/lone_druid.png diff --git a/templates/cdn/luna.png b/pc/templates/cdn/luna.png similarity index 100% rename from templates/cdn/luna.png rename to pc/templates/cdn/luna.png diff --git a/templates/cdn/lycan.png b/pc/templates/cdn/lycan.png similarity index 100% rename from templates/cdn/lycan.png rename to pc/templates/cdn/lycan.png diff --git a/templates/cdn/magnataur.png b/pc/templates/cdn/magnataur.png similarity index 100% rename from templates/cdn/magnataur.png rename to pc/templates/cdn/magnataur.png diff --git a/templates/cdn/marci.png b/pc/templates/cdn/marci.png similarity index 100% rename from templates/cdn/marci.png rename to pc/templates/cdn/marci.png diff --git a/templates/cdn/mars.png b/pc/templates/cdn/mars.png similarity index 100% rename from templates/cdn/mars.png rename to pc/templates/cdn/mars.png diff --git a/templates/cdn/medusa.png b/pc/templates/cdn/medusa.png similarity index 100% rename from templates/cdn/medusa.png rename to pc/templates/cdn/medusa.png diff --git a/templates/cdn/meepo.png b/pc/templates/cdn/meepo.png similarity index 100% rename from templates/cdn/meepo.png rename to pc/templates/cdn/meepo.png diff --git a/templates/cdn/mirana.png b/pc/templates/cdn/mirana.png similarity index 100% rename from templates/cdn/mirana.png rename to pc/templates/cdn/mirana.png diff --git a/templates/cdn/monkey_king.png b/pc/templates/cdn/monkey_king.png similarity index 100% rename from templates/cdn/monkey_king.png rename to pc/templates/cdn/monkey_king.png diff --git a/templates/cdn/morphling.png b/pc/templates/cdn/morphling.png similarity index 100% rename from templates/cdn/morphling.png rename to pc/templates/cdn/morphling.png diff --git a/templates/cdn/muerta.png b/pc/templates/cdn/muerta.png similarity index 100% rename from templates/cdn/muerta.png rename to pc/templates/cdn/muerta.png diff --git a/templates/cdn/naga_siren.png b/pc/templates/cdn/naga_siren.png similarity index 100% rename from templates/cdn/naga_siren.png rename to pc/templates/cdn/naga_siren.png diff --git a/templates/cdn/necrolyte.png b/pc/templates/cdn/necrolyte.png similarity index 100% rename from templates/cdn/necrolyte.png rename to pc/templates/cdn/necrolyte.png diff --git a/templates/cdn/nevermore.png b/pc/templates/cdn/nevermore.png similarity index 100% rename from templates/cdn/nevermore.png rename to pc/templates/cdn/nevermore.png diff --git a/templates/cdn/night_stalker.png b/pc/templates/cdn/night_stalker.png similarity index 100% rename from templates/cdn/night_stalker.png rename to pc/templates/cdn/night_stalker.png diff --git a/templates/cdn/nyx_assassin.png b/pc/templates/cdn/nyx_assassin.png similarity index 100% rename from templates/cdn/nyx_assassin.png rename to pc/templates/cdn/nyx_assassin.png diff --git a/templates/cdn/obsidian_destroyer.png b/pc/templates/cdn/obsidian_destroyer.png similarity index 100% rename from templates/cdn/obsidian_destroyer.png rename to pc/templates/cdn/obsidian_destroyer.png diff --git a/templates/cdn/ogre_magi.png b/pc/templates/cdn/ogre_magi.png similarity index 100% rename from templates/cdn/ogre_magi.png rename to pc/templates/cdn/ogre_magi.png diff --git a/templates/cdn/omniknight.png b/pc/templates/cdn/omniknight.png similarity index 100% rename from templates/cdn/omniknight.png rename to pc/templates/cdn/omniknight.png diff --git a/templates/cdn/oracle.png b/pc/templates/cdn/oracle.png similarity index 100% rename from templates/cdn/oracle.png rename to pc/templates/cdn/oracle.png diff --git a/templates/cdn/pangolier.png b/pc/templates/cdn/pangolier.png similarity index 100% rename from templates/cdn/pangolier.png rename to pc/templates/cdn/pangolier.png diff --git a/templates/cdn/phantom_assassin.png b/pc/templates/cdn/phantom_assassin.png similarity index 100% rename from templates/cdn/phantom_assassin.png rename to pc/templates/cdn/phantom_assassin.png diff --git a/templates/cdn/phantom_lancer.png b/pc/templates/cdn/phantom_lancer.png similarity index 100% rename from templates/cdn/phantom_lancer.png rename to pc/templates/cdn/phantom_lancer.png diff --git a/templates/cdn/phoenix.png b/pc/templates/cdn/phoenix.png similarity index 100% rename from templates/cdn/phoenix.png rename to pc/templates/cdn/phoenix.png diff --git a/templates/cdn/primal_beast.png b/pc/templates/cdn/primal_beast.png similarity index 100% rename from templates/cdn/primal_beast.png rename to pc/templates/cdn/primal_beast.png diff --git a/templates/cdn/puck.png b/pc/templates/cdn/puck.png similarity index 100% rename from templates/cdn/puck.png rename to pc/templates/cdn/puck.png diff --git a/templates/cdn/pudge.png b/pc/templates/cdn/pudge.png similarity index 100% rename from templates/cdn/pudge.png rename to pc/templates/cdn/pudge.png diff --git a/templates/cdn/pugna.png b/pc/templates/cdn/pugna.png similarity index 100% rename from templates/cdn/pugna.png rename to pc/templates/cdn/pugna.png diff --git a/templates/cdn/queenofpain.png b/pc/templates/cdn/queenofpain.png similarity index 100% rename from templates/cdn/queenofpain.png rename to pc/templates/cdn/queenofpain.png diff --git a/templates/cdn/rattletrap.png b/pc/templates/cdn/rattletrap.png similarity index 100% rename from templates/cdn/rattletrap.png rename to pc/templates/cdn/rattletrap.png diff --git a/templates/cdn/razor.png b/pc/templates/cdn/razor.png similarity index 100% rename from templates/cdn/razor.png rename to pc/templates/cdn/razor.png diff --git a/templates/cdn/riki.png b/pc/templates/cdn/riki.png similarity index 100% rename from templates/cdn/riki.png rename to pc/templates/cdn/riki.png diff --git a/templates/cdn/ringmaster.png b/pc/templates/cdn/ringmaster.png similarity index 100% rename from templates/cdn/ringmaster.png rename to pc/templates/cdn/ringmaster.png diff --git a/templates/cdn/rubick.png b/pc/templates/cdn/rubick.png similarity index 100% rename from templates/cdn/rubick.png rename to pc/templates/cdn/rubick.png diff --git a/templates/cdn/sand_king.png b/pc/templates/cdn/sand_king.png similarity index 100% rename from templates/cdn/sand_king.png rename to pc/templates/cdn/sand_king.png diff --git a/templates/cdn/shadow_demon.png b/pc/templates/cdn/shadow_demon.png similarity index 100% rename from templates/cdn/shadow_demon.png rename to pc/templates/cdn/shadow_demon.png diff --git a/templates/cdn/shadow_shaman.png b/pc/templates/cdn/shadow_shaman.png similarity index 100% rename from templates/cdn/shadow_shaman.png rename to pc/templates/cdn/shadow_shaman.png diff --git a/templates/cdn/shredder.png b/pc/templates/cdn/shredder.png similarity index 100% rename from templates/cdn/shredder.png rename to pc/templates/cdn/shredder.png diff --git a/templates/cdn/silencer.png b/pc/templates/cdn/silencer.png similarity index 100% rename from templates/cdn/silencer.png rename to pc/templates/cdn/silencer.png diff --git a/templates/cdn/skeleton_king.png b/pc/templates/cdn/skeleton_king.png similarity index 100% rename from templates/cdn/skeleton_king.png rename to pc/templates/cdn/skeleton_king.png diff --git a/templates/cdn/skywrath_mage.png b/pc/templates/cdn/skywrath_mage.png similarity index 100% rename from templates/cdn/skywrath_mage.png rename to pc/templates/cdn/skywrath_mage.png diff --git a/templates/cdn/slardar.png b/pc/templates/cdn/slardar.png similarity index 100% rename from templates/cdn/slardar.png rename to pc/templates/cdn/slardar.png diff --git a/templates/cdn/slark.png b/pc/templates/cdn/slark.png similarity index 100% rename from templates/cdn/slark.png rename to pc/templates/cdn/slark.png diff --git a/templates/cdn/snapfire.png b/pc/templates/cdn/snapfire.png similarity index 100% rename from templates/cdn/snapfire.png rename to pc/templates/cdn/snapfire.png diff --git a/templates/cdn/sniper.png b/pc/templates/cdn/sniper.png similarity index 100% rename from templates/cdn/sniper.png rename to pc/templates/cdn/sniper.png diff --git a/templates/cdn/spectre.png b/pc/templates/cdn/spectre.png similarity index 100% rename from templates/cdn/spectre.png rename to pc/templates/cdn/spectre.png diff --git a/templates/cdn/spirit_breaker.png b/pc/templates/cdn/spirit_breaker.png similarity index 100% rename from templates/cdn/spirit_breaker.png rename to pc/templates/cdn/spirit_breaker.png diff --git a/templates/cdn/storm_spirit.png b/pc/templates/cdn/storm_spirit.png similarity index 100% rename from templates/cdn/storm_spirit.png rename to pc/templates/cdn/storm_spirit.png diff --git a/templates/cdn/sven.png b/pc/templates/cdn/sven.png similarity index 100% rename from templates/cdn/sven.png rename to pc/templates/cdn/sven.png diff --git a/templates/cdn/techies.png b/pc/templates/cdn/techies.png similarity index 100% rename from templates/cdn/techies.png rename to pc/templates/cdn/techies.png diff --git a/templates/cdn/templar_assassin.png b/pc/templates/cdn/templar_assassin.png similarity index 100% rename from templates/cdn/templar_assassin.png rename to pc/templates/cdn/templar_assassin.png diff --git a/templates/cdn/terrorblade.png b/pc/templates/cdn/terrorblade.png similarity index 100% rename from templates/cdn/terrorblade.png rename to pc/templates/cdn/terrorblade.png diff --git a/templates/cdn/tidehunter.png b/pc/templates/cdn/tidehunter.png similarity index 100% rename from templates/cdn/tidehunter.png rename to pc/templates/cdn/tidehunter.png diff --git a/templates/cdn/tinker.png b/pc/templates/cdn/tinker.png similarity index 100% rename from templates/cdn/tinker.png rename to pc/templates/cdn/tinker.png diff --git a/templates/cdn/tiny.png b/pc/templates/cdn/tiny.png similarity index 100% rename from templates/cdn/tiny.png rename to pc/templates/cdn/tiny.png diff --git a/templates/cdn/treant.png b/pc/templates/cdn/treant.png similarity index 100% rename from templates/cdn/treant.png rename to pc/templates/cdn/treant.png diff --git a/templates/cdn/troll_warlord.png b/pc/templates/cdn/troll_warlord.png similarity index 100% rename from templates/cdn/troll_warlord.png rename to pc/templates/cdn/troll_warlord.png diff --git a/templates/cdn/tusk.png b/pc/templates/cdn/tusk.png similarity index 100% rename from templates/cdn/tusk.png rename to pc/templates/cdn/tusk.png diff --git a/templates/cdn/undying.png b/pc/templates/cdn/undying.png similarity index 100% rename from templates/cdn/undying.png rename to pc/templates/cdn/undying.png diff --git a/templates/cdn/ursa.png b/pc/templates/cdn/ursa.png similarity index 100% rename from templates/cdn/ursa.png rename to pc/templates/cdn/ursa.png diff --git a/templates/cdn/vengefulspirit.png b/pc/templates/cdn/vengefulspirit.png similarity index 100% rename from templates/cdn/vengefulspirit.png rename to pc/templates/cdn/vengefulspirit.png diff --git a/templates/cdn/venomancer.png b/pc/templates/cdn/venomancer.png similarity index 100% rename from templates/cdn/venomancer.png rename to pc/templates/cdn/venomancer.png diff --git a/templates/cdn/viper.png b/pc/templates/cdn/viper.png similarity index 100% rename from templates/cdn/viper.png rename to pc/templates/cdn/viper.png diff --git a/templates/cdn/visage.png b/pc/templates/cdn/visage.png similarity index 100% rename from templates/cdn/visage.png rename to pc/templates/cdn/visage.png diff --git a/templates/cdn/void_spirit.png b/pc/templates/cdn/void_spirit.png similarity index 100% rename from templates/cdn/void_spirit.png rename to pc/templates/cdn/void_spirit.png diff --git a/templates/cdn/warlock.png b/pc/templates/cdn/warlock.png similarity index 100% rename from templates/cdn/warlock.png rename to pc/templates/cdn/warlock.png diff --git a/templates/cdn/weaver.png b/pc/templates/cdn/weaver.png similarity index 100% rename from templates/cdn/weaver.png rename to pc/templates/cdn/weaver.png diff --git a/templates/cdn/windrunner.png b/pc/templates/cdn/windrunner.png similarity index 100% rename from templates/cdn/windrunner.png rename to pc/templates/cdn/windrunner.png diff --git a/templates/cdn/winter_wyvern.png b/pc/templates/cdn/winter_wyvern.png similarity index 100% rename from templates/cdn/winter_wyvern.png rename to pc/templates/cdn/winter_wyvern.png diff --git a/templates/cdn/wisp.png b/pc/templates/cdn/wisp.png similarity index 100% rename from templates/cdn/wisp.png rename to pc/templates/cdn/wisp.png diff --git a/templates/cdn/witch_doctor.png b/pc/templates/cdn/witch_doctor.png similarity index 100% rename from templates/cdn/witch_doctor.png rename to pc/templates/cdn/witch_doctor.png diff --git a/templates/cdn/zuus.png b/pc/templates/cdn/zuus.png similarity index 100% rename from templates/cdn/zuus.png rename to pc/templates/cdn/zuus.png diff --git a/templates/modes/all_pick.png b/pc/templates/modes/all_pick.png similarity index 100% rename from templates/modes/all_pick.png rename to pc/templates/modes/all_pick.png diff --git a/templates/roles/hard_support.png b/pc/templates/roles/hard_support.png similarity index 100% rename from templates/roles/hard_support.png rename to pc/templates/roles/hard_support.png diff --git a/templates/roles/mid.png b/pc/templates/roles/mid.png similarity index 100% rename from templates/roles/mid.png rename to pc/templates/roles/mid.png diff --git a/templates/roles/off.png b/pc/templates/roles/off.png similarity index 100% rename from templates/roles/off.png rename to pc/templates/roles/off.png diff --git a/templates/roles/safe.png b/pc/templates/roles/safe.png similarity index 100% rename from templates/roles/safe.png rename to pc/templates/roles/safe.png diff --git a/templates/roles/soft_support.png b/pc/templates/roles/soft_support.png similarity index 100% rename from templates/roles/soft_support.png rename to pc/templates/roles/soft_support.png diff --git a/shared/__init__.py b/shared/__init__.py new file mode 100644 index 0000000..68bc693 --- /dev/null +++ b/shared/__init__.py @@ -0,0 +1,6 @@ +"""Shared package: hero table, relations, tags, HTTP helpers, path constants. + +Used by both subprojects (pc/ and web/). Must never import from pc/ or web/. +Scripts in this package can be run directly: python shared/ - - + + + diff --git a/web/relations/router.js b/web/frontend/router.js similarity index 100% rename from web/relations/router.js rename to web/frontend/router.js diff --git a/web/relations/style.css b/web/frontend/style.css similarity index 99% rename from web/relations/style.css rename to web/frontend/style.css index 31efb4f..d514467 100644 --- a/web/relations/style.css +++ b/web/frontend/style.css @@ -3152,10 +3152,11 @@ body:has(#items-view:not(.hidden)) { text-decoration: none; color: inherit; } -a.streamer-avatar-wrap.is-live { +/* Linked wraps: live rooms and offline rooms alike (offline keeps click-through). */ +a.streamer-avatar-wrap { cursor: pointer; } -a.streamer-avatar-wrap.is-live:hover .streamer-avatar { +a.streamer-avatar-wrap:hover .streamer-avatar { filter: brightness(1.06); } .streamer-avatar-inner { diff --git a/item_fears.py b/web/item_fears.py similarity index 98% rename from item_fears.py rename to web/item_fears.py index 10dee44..58f4628 100644 --- a/item_fears.py +++ b/web/item_fears.py @@ -7,15 +7,19 @@ Usage: from __future__ import annotations +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + import argparse import json from datetime import datetime, timezone -from pathlib import Path -from common import DATA -from grid import hero_table -from hero_tags import ILLUSION_KEYS -from http_utils import http_json +from shared.grid import hero_table +from shared.hero_tags import ILLUSION_KEYS +from shared.http_utils import http_json +from shared.paths import DATA ITEMS_META = DATA / "items_meta.json" HERO_ABILITIES = DATA / "hero_abilities.json" diff --git a/loc_format.py b/web/loc_format.py similarity index 100% rename from loc_format.py rename to web/loc_format.py diff --git a/mechanic_tags.py b/web/mechanic_tags.py similarity index 100% rename from mechanic_tags.py rename to web/mechanic_tags.py diff --git a/notify_site_traffic.py b/web/notify_site_traffic.py similarity index 99% rename from notify_site_traffic.py rename to web/notify_site_traffic.py index 7e97496..85c8f50 100644 --- a/notify_site_traffic.py +++ b/web/notify_site_traffic.py @@ -171,7 +171,7 @@ def gql(query: str, variables: dict) -> dict: def read_site_version() -> str: - path = Path(__file__).resolve().parent / "web" / "relations" / "config.js" + path = Path(__file__).resolve().parent / "frontend" / "config.js" try: text = path.read_text(encoding="utf-8") except OSError: diff --git a/refresh_web.py b/web/refresh_web.py similarity index 96% rename from refresh_web.py rename to web/refresh_web.py index 36837d3..112163c 100644 --- a/refresh_web.py +++ b/web/refresh_web.py @@ -1,8 +1,10 @@ """Orchestrate Climperor web data refresh tiers, then optional OSS + deploy. Tiers (see AGENTS.md / Gitea Actions workflows): - daily — OpenDota stats, leaderboards, matches, pro matches, streamers; - patch check + daily — OpenDota stats, leaderboards, matches, pro matches, streamers, + streamer live probe; patch check + (live badge still trails reality by up to a day — truly real-time + would need a dedicated higher-frequency workflow, e.g. 15-min) weekly — STRATZ meta, hero items, items_meta, item_fears patch — patch list check; on has_new fetch details + version-linked scripts all — weekly then daily (patch check included in daily/patch) @@ -145,6 +147,8 @@ def run_daily(*, dry_run: bool = False) -> bool: run_script("fetch_pro_matches.py", dry_run=dry_run) # Soft-fail Douyin enrichment (script itself exits 0; keep previous values on miss). run_script("fetch_streamers.py", dry_run=dry_run) + # Soft-fail live probe (exits 0; probe failures keep previous is_live). + run_script("fetch_streamer_live.py", dry_run=dry_run) check = patch_check() if check.get("has_new"): print( diff --git a/requirements-web.txt b/web/requirements.txt similarity index 100% rename from requirements-web.txt rename to web/requirements.txt diff --git a/run_notify_site_traffic.ps1 b/web/run_notify_site_traffic.ps1 similarity index 100% rename from run_notify_site_traffic.ps1 rename to web/run_notify_site_traffic.ps1 diff --git a/serve_relations.py b/web/serve_relations.py similarity index 96% rename from serve_relations.py rename to web/serve_relations.py index 73e2f75..a61559e 100644 --- a/serve_relations.py +++ b/web/serve_relations.py @@ -10,20 +10,28 @@ Edit data/*.json directly, then refresh the page. from __future__ import annotations +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + import argparse import json import mimetypes import webbrowser from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from pathlib import Path from urllib.parse import urlparse import urllib.error -from common import ( +from shared.grid import ATTR_ORDER, hero_table +from shared.hero_tags import TAG_ORDER, tags_for_hero +from shared.http_utils import http_bytes +from shared.paths import ( ABILITY_ICONS, ABILITY_VIDEOS, ATTR_ICONS, + DATA, HERO_PORTRAITS, ITEM_CAT_ICONS, ITEM_ICONS, @@ -33,28 +41,27 @@ from common import ( STREAMER_VIDEOS, TEMPLATES_CDN, UI_ICONS, + WEB_FRONTEND, ) -from grid import ATTR_ORDER, hero_table -from hero_tags import TAG_ORDER, tags_for_hero -from http_utils import http_bytes -from mechanic_tags import QUERY_MECHANIC_ORDER, mechanic_query_payload -from relations import DEFAULT_RELATIONS, load_relations +from shared.relations import DEFAULT_RELATIONS, load_relations -WEB_DIR = ROOT / "web" / "relations" -GRID_ORDER_PATH = ROOT / "data" / "hero_grid_order.json" -HERO_ITEMS_PATH = ROOT / "data" / "hero_items.json" -HERO_STATS_PATH = ROOT / "data" / "hero_stats.json" -HERO_MATCHES_PATH = ROOT / "data" / "hero_matches.json" -HERO_ITEM_FEARS_PATH = ROOT / "data" / "hero_item_fears.json" -HERO_ABILITIES_PATH = ROOT / "data" / "hero_abilities.json" -ITEM_SHOP_PATH = ROOT / "data" / "item_shop.json" -ITEMS_META_PATH = ROOT / "data" / "items_meta.json" -PATCHES_PATH = ROOT / "data" / "patches.json" -LEADERBOARDS_PATH = ROOT / "data" / "leaderboards.json" -PRO_MATCHES_PATH = ROOT / "data" / "pro_matches.json" -STREAMERS_PATH = ROOT / "data" / "streamers.json" -STRATZ_HERO_META_PATH = ROOT / "data" / "stratz_hero_meta.json" -STRATZ_MATCHUP_TOPS_PATH = ROOT / "data" / "stratz_matchup_tops.json" +from mechanic_tags import QUERY_MECHANIC_ORDER, mechanic_query_payload + +WEB_DIR = WEB_FRONTEND +GRID_ORDER_PATH = DATA / "hero_grid_order.json" +HERO_ITEMS_PATH = DATA / "hero_items.json" +HERO_STATS_PATH = DATA / "hero_stats.json" +HERO_MATCHES_PATH = DATA / "hero_matches.json" +HERO_ITEM_FEARS_PATH = DATA / "hero_item_fears.json" +HERO_ABILITIES_PATH = DATA / "hero_abilities.json" +ITEM_SHOP_PATH = DATA / "item_shop.json" +ITEMS_META_PATH = DATA / "items_meta.json" +PATCHES_PATH = DATA / "patches.json" +LEADERBOARDS_PATH = DATA / "leaderboards.json" +PRO_MATCHES_PATH = DATA / "pro_matches.json" +STREAMERS_PATH = DATA / "streamers.json" +STRATZ_HERO_META_PATH = DATA / "stratz_hero_meta.json" +STRATZ_MATCHUP_TOPS_PATH = DATA / "stratz_matchup_tops.json" ATTR_COLS = {"str": 6, "agi": 6, "int": 6, "all": 4} ATTR_LABELS = {"str": "力量", "agi": "敏捷", "int": "智力", "all": "全才"} ABILITY_ICON_URL = ( @@ -779,6 +786,11 @@ class Handler(BaseHTTPRequestHandler): if path in ("/api/data", "/data.json"): self._json(200, build_payload()) return + if path == "/api/live-status": + # Local-dev stub for the Pages Function (edge probing runs in + # production only); the empty map keeps the UI on data.json is_live. + self._json(200, {"probed_at": None, "ttl": 300, "streamers": {}}) + return if path.startswith("/attr/"): key = path[len("/attr/") :] if key not in ("str.png", "agi.png", "int.png", "all.png"):