Add daily Feishu digest for dota2.refining.dev traffic.
Gitea Actions runs notify_site_traffic.py at 09:00 CST using Cloudflare GraphQL and a Feishu webhook (secrets from keyzoo). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# Daily Feishu digest for dota2.refining.dev traffic.
|
||||
# Repo secrets: CLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY, FEISHU_WEBHOOK_URL
|
||||
# (copy from keyzoo refining/cloudflare + refining/feishu_webhook)
|
||||
|
||||
name: site-traffic-notify
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 09:00 China = 01:00 UTC
|
||||
- cron: "0 1 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
day:
|
||||
description: "YYYY-MM-DD (empty = yesterday CST)"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
# Mac mini Gitea runner (labels: self-hosted, macOS, ARM64, darwin-arm64)
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Post Feishu digest
|
||||
env:
|
||||
CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}
|
||||
CLOUDFLARE_API_KEY: ${{ secrets.CLOUDFLARE_API_KEY }}
|
||||
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
|
||||
run: |
|
||||
set -e
|
||||
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 }}"
|
||||
else
|
||||
"$PYTHON" notify_site_traffic.py
|
||||
fi
|
||||
@@ -11,6 +11,8 @@ Windows 上的 Dota 2 **天梯选将识别**工具:用 GSI 感知选将阶段
|
||||
- 模板来源:**仅** Steam CDN 官方头像(`templates/cdn/`)
|
||||
- **已放弃** real 实拍模板库补充;不要再引入 `templates/real/` 或入库流水线
|
||||
|
||||
**上分帝 Web**(`web/relations/`)是独立子项目:英雄克制/搭档、排行、物品、版本等内容站点,与局内选将识别解耦。文档与注释统称「上分帝 Web」,勿再称「关系预览」;历史路径/脚本名(如 `serve_relations.py`、`dist/relations/`)暂保留。
|
||||
|
||||
## 目录与模块
|
||||
|
||||
| 路径 | 职责 |
|
||||
@@ -24,17 +26,24 @@ Windows 上的 Dota 2 **天梯选将识别**工具:用 GSI 感知选将阶段
|
||||
| `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`) |
|
||||
| `fetch_stratz.py` | 拉取 STRATZ 对位优势 / 搭档协同 → `data/stratz_matchups.json` / `data/synergies.json`(需 token;`--mode matchups\|synergies`;供 `audit_relations.py`) |
|
||||
| `fetch_stratz_meta.py` | 拉取 STRATZ 各段位周胜率/上场率 + 分路统计 + 对位 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` | 分路过滤;按克制/被克制/搭档边打分,输出 Top-3 |
|
||||
| `serve_relations.py` | 游戏外关系/物品只读预览(`web/relations/`;改 `data/relations.json`) |
|
||||
| `export_relations_site.py` | 导出关系预览为纯静态站点 → `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` | 一键部署关系预览静态站点到 Cloudflare Pages(导出 + 资产预检 + `wrangler` 直传 + 绑域名;默认 OSS base 指向 `climperor` 桶的视频与静态图;凭据经 keyzoo 注入或 env) |
|
||||
| `recommend.py` | 定位局分路过滤;克/搭/补全网格标记;调用 `draft_archetypes` 做推进/全球流/缺口画像与短文案 |
|
||||
| `draft_archetypes.py` | 规则阵容画像(推进/全球流/敌我缺口)+ `analysis` / `reasons` 文案(不接 AI) |
|
||||
| `serve_relations.py` | 上分帝 Web 本地开发服务(`web/relations/`;改 `data/*.json` 后刷新) |
|
||||
| `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) |
|
||||
| `notify_site_traffic.py` | 上分帝 Web 日活摘要 → 飞书 webhook(Cloudflare GraphQL;按已知浏览器 UA + `/` 估真实访问;`--dry-run` 只打印卡片) |
|
||||
| `.gitea/workflows/site-traffic-notify.yml` | 每日 09:00 CST 在 self-hosted runner 跑 `notify_site_traffic.py`;Secrets:`CLOUDFLARE_EMAIL` / `CLOUDFLARE_API_KEY` / `FEISHU_WEBHOOK_URL`(自 keyzoo `refining/cloudflare` + `refining/feishu_webhook`) |
|
||||
| `_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/`(关系预览) |
|
||||
| `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` 优先国服;`--enrich-item-times` 补购买时间;**仅上分帝 Web**;勿进 recommend) |
|
||||
| `fetch_leaderboards.py` | 拉取 Valve Immortal 四区榜 Top100 → `data/leaderboards.json`(**仅上分帝 Web「排行」**;无 MMR/account_id;勿进 recommend) |
|
||||
| `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 填数) |
|
||||
| `loc_format.py` | Valve 文案共用:去 HTML、填充 `%token%` / `{s:token}` |
|
||||
@@ -43,31 +52,37 @@ Windows 上的 Dota 2 **天梯选将识别**工具:用 GSI 感知选将阶段
|
||||
| `item_fears.py` | 规则推导「英雄怕的装备」→ `data/hero_item_fears.json` |
|
||||
| `autocalibrate.py` / `calibrate.py` | ROI 自动 / 手动标定 |
|
||||
| `capture.py` | 屏幕捕获 |
|
||||
| `overlay.py` | 顶栏角色标签 + 选将网格左上角青标 Top-3(点击穿透) |
|
||||
| `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` 生成,预览版本页只读) |
|
||||
| `data/relations.json` | 定性克制边 + 搭档边(可提交;手改 JSON,预览页只读) |
|
||||
| `data/hero_grid_order.json` | 预览页四列网格顺序 |
|
||||
| `data/hero_items.json` | 核心成品装备缓存(预览页只读;由 `fetch_hero_items.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/stratz_hero_meta.json` | STRATZ 各勋章段位近 N 周 pick/win + **同段位最近 1 周分路** + `meta_board`(由 `fetch_stratz_meta.py` 生成;Web 英雄详情「走势」优先 + 顶层「走势」近 N 周高胜率榜 +「排行 → 英雄 Meta」;**不**参与局内推荐) |
|
||||
| `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/hero_fear_overrides.json` | 英雄→害怕装备手工加减(合并进 item_fears) |
|
||||
| `data/hero_abilities.json` | 英雄技能与机制汇总(由 `fetch_hero_abilities.py` 生成) |
|
||||
| `data/hero_item_fears.json` | 英雄怕的装备(规则推导;预览「怕」行) |
|
||||
| `web/relations/` | 关系只读预览页静态资源(`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 双向同步(顶层标签 / 英雄 + 子标签 / 物品 / 版本 / 标签筛选 / 搜索) |
|
||||
| `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\|trends\|items\|patches` / 英雄 + 子标签 `skills\|core\|fears\|trends\|matchups\|matches\|patches` / Immortal 地区或 Meta 段位 `#/rankings/meta[/bracket]` / 近 8 周走势榜 `#/trends[/bracket]` / 物品 / 版本 / 标签筛选 / 搜索;旧 `stats` 别名兼容) |
|
||||
| `templates/cdn/` | 全英雄 CDN 模板(需提交或由脚本生成) |
|
||||
| `templates/roles/` / `templates/modes/` | 位置 / 模式辅助模板 |
|
||||
| `assets/role_icons/` | Valve 选人筛选角色图标(`filter_*`,品红 chroma 去黑边) |
|
||||
| `assets/hero_portraits/` | 官网横版头像(关系预览;默认 wide 面部构图,非匹配模板) |
|
||||
| `assets/attr_icons/` | 官网主属性图标(力量/敏捷/智力/全才,关系预览用) |
|
||||
| `assets/item_icons/` | Steam CDN 装备图标(关系预览常用装备) |
|
||||
| `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 技能图标(关系预览;按需缓存);`innate.png` 先天共用图标;`talent_tree.png` 天赋树触发图标 |
|
||||
| `assets/ability_icons/` | Steam CDN 技能图标(上分帝 Web;按需缓存);`innate.png` 先天共用图标;`talent_tree.png` 天赋树触发图标 |
|
||||
| `assets/ui_icons/` | dota2.com.cn 通用 UI 图标(`cooldown.png` 等,技能详情底栏冷却图标) |
|
||||
| `assets/ability_videos/` | 官网技能演示视频(webm/mp4;勿提交,由脚本拉取;线上由阿里云 OSS `climperor` 桶 `ability-video/` 前缀托管) |
|
||||
|
||||
@@ -89,8 +104,8 @@ Dota 2 GSI → gsi_watch.py (:3223)
|
||||
→ DraftSession 轮询截屏 → samples/raw/<matchid>/
|
||||
→ recognize_image (CDN 模板 + 可选天梯遮罩)
|
||||
→ roles(分路字)/ grid(禁用 + 单元格)/ modes
|
||||
→ recommend Top-3(data/relations.json 定性边,本人槽位只信 GSI)
|
||||
→ overlay 角色标签 + 网格青标(可选)
|
||||
→ recommend 克/搭/补(relations + draft_archetypes 规则画像,本人槽位只信 GSI)
|
||||
→ overlay 角色标签 + 网格克/搭/补 + 阵容分析条(可选)
|
||||
→ results/draft_*.json + 终端时间线
|
||||
```
|
||||
|
||||
@@ -98,8 +113,8 @@ Dota 2 GSI → gsi_watch.py (:3223)
|
||||
|
||||
- **合规**:仅 GSI + 屏幕截图;**禁止**读进程内存、注入、绕过 VAC。
|
||||
- **GSI 范围**:普通玩家视角拿不到双方 pick;GSI 只作阶段触发、`team_slot`、本人 `hero`、本机 `accountid`/`steamid`。本人顶栏槽位**只信** GSI,不用截屏名字亮度猜测。
|
||||
- **克制 / 搭档数据**:定性边存 `data/relations.json`(克制有向 + 搭档无向 + 理由),**不要**用胜率/场次表达机制克制,也**不要**写入 `data/heroes.json`。预览三视图:克制 / 被克制 / 搭档。
|
||||
- **装备机制 / 怕的装备**:标签与技能汇总来自 Valve/OpenDota 自动抽取 + `item_tag_overrides.json`;`item_fears.py` 规则映射到英雄弱点。大段技能文案只进 `data/hero_abilities.json` / `items_meta.json`,**不要**塞进 `heroes.json`。本阶段仅关系预览展示,**不对局内出装推荐**。
|
||||
- **克制 / 搭档数据**:机制克制/搭档只用定性边存 `data/relations.json`(克制有向 + 搭档无向 + 理由),**不要**用胜率/场次表达机制克制,也**不要**写入 `data/heroes.json`。Web 英雄页三视图:克制 / 被克制 / 搭档。OpenDota 段位胜率/场次单独存 `data/hero_stats.json`(近约 7 天窗;上场率 = 出场/(Σ出场/10);冠绝样本过小时与超凡合并),**仅**上分帝 Web 英雄详情「走势」兜底展示。STRATZ 周胜率/分路/`meta_board` 存 `data/stratz_hero_meta.json`,对位 Top 存 `data/stratz_matchup_tops.json`(英雄详情「走势」与顶层「走势」榜 `#/trends` 优先用 STRATZ;「对位」Tab 与「排行 → 英雄 Meta」只读),**禁止**合并进 relations/heroes,禁止 `recommend.py` 读取。同英雄近期比赛出装/加点单独存 `data/hero_matches.json`,**仅** Web「近期比赛」Tab,禁止进 recommend。Valve Immortal 四区榜单独存 `data/leaderboards.json`,**仅** Web「排行」页选手榜,禁止进 recommend。
|
||||
- **装备机制 / 怕的装备**:标签与技能汇总来自 Valve/OpenDota 自动抽取 + `item_tag_overrides.json`;`item_fears.py` 规则映射到英雄弱点。大段技能文案只进 `data/hero_abilities.json` / `items_meta.json`,**不要**塞进 `heroes.json`。本阶段仅上分帝 Web 展示,**不对局内出装推荐**。核心装「使用率」为 `hero_items` 列表内相对热度归一化,非绝对出场率、无段位维度。
|
||||
- **模板策略**:只维护 CDN 层。皮肤问题用会话策略(选人可改判、决策 `allow_revise=False`、best 帧偏 HERO_SELECTION),不要为皮肤加模板库,也不要复活 real 双层库。
|
||||
- **顶栏时机**:皮肤在**全员选完后**才上顶栏;本机进决策时别人可能还在选——须保持 `strategy_tail` 视觉,禁止「一进 STRATEGY 就永久停读」。
|
||||
- **坐标**:一律相对坐标(相对屏幕高 / 相对中心),勿写死像素分辨率。
|
||||
@@ -112,8 +127,12 @@ Dota 2 GSI → gsi_watch.py (:3223)
|
||||
pip install -r requirements.txt
|
||||
python fetch_cdn_templates.py
|
||||
python import_relations_xlsx.py # 可选:从选将笔记 xlsx 导入关系
|
||||
python fetch_hero_portraits.py # 官网横版头像(关系预览)
|
||||
python fetch_hero_items.py # OpenDota 热门装备缓存(关系预览)
|
||||
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;--enrich-item-times)
|
||||
python fetch_leaderboards.py # Valve Immortal 四区 Top100(排行页)
|
||||
python fetch_item_shop.py # 商店分类目录(物品页)
|
||||
python fetch_items_meta.py # 装备描述 + 机制标签
|
||||
python fetch_hero_abilities.py # 英雄技能 / 驱散汇总
|
||||
@@ -121,10 +140,11 @@ python fetch_hero_abilities.py --icons-only # 缓存技能图标到 assets/abil
|
||||
python fetch_ability_videos.py # 官网技能演示(限速;默认 webm)
|
||||
python item_fears.py # 英雄怕的装备
|
||||
python fetch_patches.py # 近一年版本日志 + 详情(版本页)
|
||||
python serve_relations.py # 浏览器只读预览 英雄关系 / 物品商店
|
||||
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 部署 / 自定义域名状态
|
||||
python gsi_setup.py --check
|
||||
@@ -139,9 +159,9 @@ python evaluate.py
|
||||
- 代码标识符、日志、错误信息、注释用**英文**;用户可见摘要可用中文。
|
||||
- 改匹配阈值或裁切时,用 `evaluate.py` / 标注帧验证,并更新 `CHANGELOG.md` 与必要时的 `DESIGN.md`。
|
||||
- 改 GSI cfg 时同步核对 `gsi_setup.py` 与 Dota `gamestate_integration` 目录。
|
||||
- 改预览路由形态(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`+幻想系推导),仅关系预览筛选用;基础属性/血蓝等数值字段供关系预览详情条,勿塞机制文案。
|
||||
- 发版关系预览时同步 bump `export_relations_site.py` 的 `SITE_VERSION`、`web/relations/config.js` 的同名变量,以及 `index.html` 里 `config.js`/`router.js`/`app.js` 的 `?v=` 缓存戳;并写 `CHANGELOG.md`。
|
||||
- 改 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`。
|
||||
- 不要重新引入 real 模板双层库、`cdn_penalty`、`build_library.py`。
|
||||
|
||||
## 文档分工
|
||||
|
||||
+192
-37
@@ -4,11 +4,166 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- 上分帝 Web 日活飞书推送:`notify_site_traffic.py` + Gitea Actions
|
||||
`site-traffic-notify`(每日 09:00 CST;真实访问按已知浏览器 UA 打开 `/` 估算)。
|
||||
- 上分帝 Web 顶层「走势」`#/trends[/bracket]`:近 8 周高胜率/上场率榜(初期 / 迷你走势 / 末期 / 变化;STRATZ;无禁用率;`0.5.41`)。
|
||||
- 上分帝 Web 接入 STRATZ(`fetch_stratz_meta.py`,token 经 `.env` / keyzoo):
|
||||
- 「走势」优先用 STRATZ 近几周段位胜率/上场率/场次,并展示周折线柱与分路胜率(OpenDota 作兜底)
|
||||
- 新子 Tab「对位」`#/heroes/<key>/matchups`:天梯对位优势 / 被克 / 协同 Top(与网格机制「克/怕/搭」分开展示)
|
||||
- 「排行」新增「英雄 Meta」模式 `#/rankings/meta[/bracket]`(按段位上场排序)
|
||||
- 缓存:`data/stratz_hero_meta.json` / `data/stratz_matchup_tops.json`(gitignore;仅 Web,不进 recommend / relations)
|
||||
- 上分帝 Web「近期比赛」Tab(`#/heroes/<key>/matches`):同英雄近期局终局出装 +
|
||||
加点顺序;展示选手昵称/段位勋章与 OpenDota 选手·比赛链接;
|
||||
`fetch_hero_matches.py` → `data/hero_matches.json`
|
||||
(`--source league|public|both`;`--enrich-players` 可补昵称段位;
|
||||
`--enrich-item-times` 可补购买时间;仅 Web 站点,不进局内推荐)。
|
||||
- 选将规则阵容画像(不接 AI):识别敌方推进/全球流、敌我缺口(缺控制/爆发/输出/先手);
|
||||
全网格标 **克 / 搭 / 补**;叠加层显示一句阵容分析;每英雄短推荐原因写入日志与
|
||||
`results` JSON(`draft_archetypes.py` + `recommend.suggest_marks`)。
|
||||
|
||||
### Changed
|
||||
|
||||
- 上分帝 Web「走势」:三卡 / 分路胜率统一为 **当前选中段位 · 最近 1 周**(STRATZ `winWeek`);分路不再用合并档 `stats` 接口;段位栏下显示时间窗提示(`0.5.30`)。
|
||||
- 上分帝 Web「走势」:近 8 周改为 **逐周列表**(日期 / 胜率 / 横向条 / 场次),Tab 内容区可滚动(`0.5.31`)。
|
||||
- 上分帝 Web「走势」近 8 周:左对齐条按本英雄 8 周区间缩放、周环比、去掉 50% 中线(`0.5.37`)。
|
||||
- 文档与注释统一称呼:`web/relations/` 子项目改称 **上分帝 Web**(不再称「关系预览」);路径与脚本名(如 `serve_relations.py`)暂保持不变。
|
||||
- 「近期比赛」公开对局默认优先国服(Perfect World cluster;`--public-region china`),联赛+天梯可用 `--source both`;卡片来源显示「国服 / 天梯 / 联赛」。
|
||||
- 「近期比赛」改为展示最近 10 场胜局(传奇及以上天梯 + 联赛/职业,按时间从新到旧);公开列表过滤机器人/Turbo,仅保留 ranked lobby(`0.5.40`)。
|
||||
- 选将推荐不再限 Top-3 名次青标;非定位局也可推克/搭/补(无分路字则不过滤)。
|
||||
|
||||
## [0.5.29] - 2026-07-28
|
||||
|
||||
### Added
|
||||
|
||||
- 上分帝 Web:STRATZ 走势 / 对位 Tab / 英雄 Meta 榜(见 Unreleased 明细;站点版本 `0.5.29`)。
|
||||
|
||||
## [0.5.28] - 2026-07-28
|
||||
|
||||
### Added
|
||||
|
||||
- 「近期比赛」出装图标叠加购买时间(MM:SS,支持负值);数据来自 OpenDota
|
||||
`purchase_log`(未解析对局无时间)。中立装用 `neutral_item_history`。
|
||||
- `fetch_hero_matches.py --enrich-item-times`:只补时间字段,无需全量重拉列表。
|
||||
|
||||
### Changed
|
||||
|
||||
- 「近期比赛」出装布局对齐 OpenDota:主栏一行、背包第二行(背包轮廓图标),去掉竖分隔线。
|
||||
|
||||
## [0.5.27] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- 「近期比赛」卡片头:昵称旁不再显示段位文字(如「冠绝」);保留勋章图标,排行榜 # 放进勋章 tooltip。
|
||||
|
||||
## [0.5.19] - 2026-07-28
|
||||
|
||||
### Fixed
|
||||
|
||||
- 排行页标题「Immortal 排行榜」与 info 图标保持同一行不换行。
|
||||
|
||||
## [0.5.18] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- 排行页「Immortal 排行榜」标题旁增加与走势页一致的 info 气泡提示(官方 Immortal 地区榜 · 各区分数不可比)。
|
||||
|
||||
## [0.5.13] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- 排行榜按视口居中(不含右侧说明);说明绝对定位贴在榜单右侧。
|
||||
|
||||
## [0.5.12] - 2026-07-28
|
||||
|
||||
### Fixed
|
||||
|
||||
- 排行表分割线错位(去掉选手列 flex)。
|
||||
|
||||
## [0.5.11] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- 排行页右侧说明贴紧榜单(与榜单成组居中),不再贴屏幕右缘。
|
||||
|
||||
## [0.5.10] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- 排行页国籍列显示中文国名(未知代码仍回退为原代码)。
|
||||
|
||||
## [0.5.9] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- 排行页战队 / 国籍无数据时留空,不再显示「—」。
|
||||
|
||||
## [0.5.8] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- 排行页:榜单单独居中,右侧说明贴屏幕右缘(不再与榜单成组居中)。
|
||||
|
||||
## [0.5.7] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- 排行页新增「战队」列(原选手行内战队标签拆出)。
|
||||
|
||||
## [0.5.6] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- 排行页新增「国籍」列(原选手行内国旗代码拆出)。
|
||||
|
||||
## [0.5.5] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- 排行页去掉官方榜说明文案。
|
||||
|
||||
## [0.5.4] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- 排行页副标题去掉地区名与「Top 100」,仅保留更新时间。
|
||||
|
||||
## [0.5.3] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- 排行页:榜单居中,标题 / 地区切换 / 说明贴在右侧。
|
||||
|
||||
## [0.5.1] - 2026-07-28
|
||||
|
||||
### Added
|
||||
|
||||
- 上分帝 Web「排行」页(`#/rankings`,插在英雄之后):Valve Immortal 四区榜
|
||||
Top 100(中国 / 欧洲 / 美洲 / 东南亚);`fetch_leaderboards.py` →
|
||||
`data/leaderboards.json`(仅排名+昵称等,无 MMR/账号 ID)。
|
||||
|
||||
## [0.5.0] - 2026-07-28
|
||||
|
||||
### Added
|
||||
|
||||
- 上分帝 Web「走势」Tab(`#/heroes/<key>/trends`):OpenDota 近约 7 天各段位
|
||||
胜率 / 上场率 / 场次;顶部勋章条(先锋→冠绝)切换段位。
|
||||
- `fetch_hero_stats.py` → `data/hero_stats.json`(含 `totals` / `window_*`);
|
||||
仅 Web 站点,不进 `relations.json` / 局内推荐。
|
||||
- 核心装备图标显示列表内相对使用率 %(OpenDota popularity 归一化)。
|
||||
- 冠绝勋章旁说明:样本过小则与超凡合并展示。
|
||||
|
||||
### Changed
|
||||
|
||||
- 走势布局居中:勋章条 + 三卡,无段位列表 / 被禁率;勋章约 90px。
|
||||
- 静态资源增加 `/rank/` 与 `assets/rank_icons/rank_icon_1..8.png`。
|
||||
|
||||
## [0.4.2] - 2026-07-27
|
||||
|
||||
### Changed
|
||||
|
||||
- 关系预览静态图标(头像 / 装备 / 技能 / 属性等)改由阿里云 OSS 同桶托管;
|
||||
- 上分帝 Web静态图标(头像 / 装备 / 技能 / 属性等)改由阿里云 OSS 同桶托管;
|
||||
`config.js` 新增 `STATIC_ASSET_BASE`,生产 Pages 包体约 **2 MB**(仅
|
||||
HTML/JS/data),换机部署无需本地 fetch 图标。同步脚本
|
||||
`_oss_static_assets.py upload`。
|
||||
@@ -21,7 +176,7 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- 线上关系预览技能演示视频改由阿里云 OSS 托管(桶 `climperor`,
|
||||
- 线上上分帝 Web技能演示视频改由阿里云 OSS 托管(桶 `climperor`,
|
||||
`https://climperor.oss-cn-shanghai.aliyuncs.com/ability-video/...`),不再打进
|
||||
Cloudflare Pages;本地 `serve_relations.py` 仍读 `assets/ability_videos/`。
|
||||
导出写入 `config.js` 的 `ABILITY_VIDEO_BASE`(`export_relations_site.py
|
||||
@@ -39,7 +194,7 @@
|
||||
|
||||
### Added
|
||||
|
||||
- 新增 `deploy_relations.py`:一键把关系预览静态站点部署到 Cloudflare Pages
|
||||
- 新增 `deploy_relations.py`:一键把上分帝 Web静态站点部署到 Cloudflare Pages
|
||||
—— 串联导出(`export_relations_site.py`)→ 资产完整性预检(缺图则中止)
|
||||
→ `wrangler pages deploy` 直传 `dist/relations/` → 绑定自定义域名。凭据
|
||||
经 keyzoo 注入(`refining/cloudflare` 的 Global API Key)或手动
|
||||
@@ -60,24 +215,24 @@
|
||||
- 修复 `serializeHash` 生成 `#heroes`(缺 `/`)导致刷新后路由解析异常的
|
||||
问题,现统一为 `#/heroes` 形态。
|
||||
- 版本日志中未知单位 id 不再显示 `# 1961` 原始数字,回退为「其它单位」。
|
||||
- 本地预览 `serve_relations.py` 增加 CSP 响应头(`default-src 'self'` 等),
|
||||
- 本地开发服务 `serve_relations.py` 增加 CSP 响应头(`default-src 'self'` 等),
|
||||
限制资源只从本站加载。
|
||||
- `data.json` 加载失败时显式抛出 HTTP 状态错误,不再静默进入异常渲染。
|
||||
|
||||
### Performance
|
||||
|
||||
- 关系预览缓存英雄技能条目(`skillEntriesFor`)与血蓝条量程
|
||||
- 上分帝 Web缓存英雄技能条目(`skillEntriesFor`)与血蓝条量程
|
||||
(`resourceBarScale`)计算结果,数据重载时失效,减少重复遍历。
|
||||
|
||||
## [0.3.0] - 2026-07-27
|
||||
|
||||
### Added
|
||||
|
||||
- 关系预览「版本」页改为默认展示**最新版本**的完整改动详情(综合 / 物品 / 中立物品 /
|
||||
- 上分帝 Web「版本」页改为默认展示**最新版本**的完整改动详情(综合 / 物品 / 中立物品 /
|
||||
英雄),顶部下拉可切换近一年内的其它版本;`fetch_patches.py` 重写为抓取近一年
|
||||
(默认 365 天,`--days` / `--since` 可调)版本列表 + 逐版本 `patchnotes` 详情,
|
||||
构建 `lookup`(ability_id / hero_id → 名称 + 图标 key)并下载引用到的物品 / 技能
|
||||
图标到 `assets/`,静态导出与本地预览均自包含、不联网。
|
||||
图标到 `assets/`,静态导出与本地开发服务均自包含、不联网。
|
||||
- 版本页排版对齐官网:左大标题「游戏性更新 {版本}」+ 右选择框,内容居中窄列;
|
||||
物品图标按 Steam CDN 88×64 比例(66×48)显示(修复正方形框上下黑边);英雄头像
|
||||
复用英雄页 16:9 宽卡(`?v=wide`、`object-fit:contain`)不再裁切面部;图标放大
|
||||
@@ -88,19 +243,19 @@
|
||||
技能页 bundled `innate.png`(从 `hero_abilities.is_innate` 构建先天 key 集合判断),
|
||||
覆盖 117 个被引用的先天技能。
|
||||
- `hero_notes` 属性标签补全 `movement` → 「移速」(修复原始英文 `movement` 漏显示)。
|
||||
- 新增 `export_relations_site.py`:一键导出关系预览为纯静态站点(`dist/relations/`),
|
||||
- 新增 `export_relations_site.py`:一键导出上分帝 Web为纯静态站点(`dist/relations/`),
|
||||
内含 `data.json` 快照 + 前端 + 图片资源,可直接部署到任意静态托管;
|
||||
`--with-videos` 可选拷贝技能演示视频(体积数 GB)。
|
||||
- 关系预览前端资源路径改为相对路径(`data.json` / `attr/` / `portrait/` / `item/` 等),
|
||||
- 上分帝 Web前端资源路径改为相对路径(`data.json` / `attr/` / `portrait/` / `item/` 等),
|
||||
静态托管在子路径下也能正常工作;`serve_relations.py` 同时提供 `/data.json` 路由,
|
||||
本地预览行为不变。
|
||||
- 关系预览技能详情:左侧播放官网技能演示视频(`assets/ability_videos/` 本地缓存,
|
||||
本地开发服务行为不变。
|
||||
- 上分帝 Web技能详情:左侧播放官网技能演示视频(`assets/ability_videos/` 本地缓存,
|
||||
`/ability-video/<英雄>/<技能>.webm` 路由),右侧为技能名与描述(对齐官网左右分栏);
|
||||
未缓存的技能自动隐藏视频,描述列占满整行。
|
||||
- 英雄详情的装备详情区不再重复显示大图(上方装备行已有图标)。
|
||||
- 关系预览技能详情新增「施法前摇」「吟唱时间」两行(取自 Valve `cast_points` /
|
||||
- 上分帝 Web技能详情新增「施法前摇」「吟唱时间」两行(取自 Valve `cast_points` /
|
||||
`channel_times`,逐级相同则合并为单值);`fetch_hero_abilities.py` 全量重抓后生效。
|
||||
- 关系预览技能详情按 dota2.com.cn 版式重排:移除右上重复的技能大图标(上方技能列表
|
||||
- 上分帝 Web技能详情按 dota2.com.cn 版式重排:移除右上重复的技能大图标(上方技能列表
|
||||
已高亮选中项);属性值(技能/伤害类型/免疫/驱散/施法距离等)双列网格、特殊数值
|
||||
自适应多列网格(消除右列大片空白);冷却/蓝量移到带图标的底栏——冷却用官方
|
||||
`cooldown.png`(新增 `assets/ui_icons/` + `/ui-icon/` 路由,静态导出一并拷贝),
|
||||
@@ -112,10 +267,10 @@
|
||||
不再被牵连出滚轮;压缩参数行距使典型技能在高屏零滚轮(无头 Chrome + CDP 实测
|
||||
`scrollHeight == clientHeight` 验证)。整块宽度上限 1000→1320px、视频 700→820px、
|
||||
详情列 720→760px。
|
||||
- 关系预览顶栏左上角新增 Dota 2 logo +「上分帝」标题(`assets/ui_icons/dota2_logo.png`
|
||||
- 上分帝 Web顶栏左上角新增 Dota 2 logo +「上分帝」标题(`assets/ui_icons/dota2_logo.png`
|
||||
透明 emblem),经 `/ui-icon/` 路由与静态导出统一分发;窄屏自动隐藏标题或整组标识,
|
||||
不影响居中分页布局。
|
||||
- 关系预览新增 **Hash 路由**:顶层标签(英雄 / 物品 / 版本)、选中英雄 + 详情子标签
|
||||
- 上分帝 Web新增 **Hash 路由**:顶层标签(英雄 / 物品 / 版本)、选中英雄 + 详情子标签
|
||||
(技能 / 核心装备 / 被克装备 / 版本变更)、物品页选中物品、版本页选中版本均同步进
|
||||
URL;英雄页的定位标签筛选与搜索框文本以 query 参数(`?tags=核心,辅助&q=axe`)进
|
||||
URL。形态如 `#/heroes/axe/core`、`#/items/black_king_bar`、`#/patches/7.41`
|
||||
@@ -179,15 +334,15 @@
|
||||
|
||||
### Added
|
||||
|
||||
- 关系预览英雄详情:基础属性条(力量/敏捷/智力 + 成长、初始生命/魔法与恢复;
|
||||
- 上分帝 Web英雄详情:基础属性条(力量/敏捷/智力 + 成长、初始生命/魔法与恢复;
|
||||
附攻击/护甲/移速/攻击距离)。数据来自 OpenDota(`fetch_cdn_templates.py` →
|
||||
`data/heroes.json`),主属性高亮。
|
||||
- `fetch_ability_videos.py`:从 Steam CDN 批量拉取官网技能演示(webm/mp4),
|
||||
默认限速约 1.5s+抖动,支持断点续传与 404 缓存;输出 `assets/ability_videos/`(不入库)。
|
||||
- 装备说明填充 Valve `%token%` 数值(如飓风长戟「飓风之力」);
|
||||
`fetch_items_meta.py --reformat-desc`,共用 `loc_format.py`。
|
||||
- 关系预览英雄详情底部分页:**技能** / **核心装备** / **被克装备**。
|
||||
- 关系预览新增顶级 **物品** 页(与英雄同级):对齐 dota2.com.cn 商店物品
|
||||
- 上分帝 Web英雄详情底部分页:**技能** / **核心装备** / **被克装备**。
|
||||
- 上分帝 Web新增顶级 **物品** 页(与英雄同级):对齐 dota2.com.cn 商店物品
|
||||
**11 列竖排**(基础分类 / 合成分类),详情在右侧;目录来自官网
|
||||
`itemscategory/json`(`fetch_item_shop.py` → `data/item_shop.json`),
|
||||
并展示合成组件(含卷轴)与「可升级为」。
|
||||
@@ -195,16 +350,16 @@
|
||||
详情(含装备合成)展示在下方详情区(非弹窗)。
|
||||
- 「害怕」装备规则完善:莲花 `spell_reflect`、点目标技能自动关联莲花/林肯;
|
||||
`data/hero_fear_overrides.json` 补经典克制(如剑圣→莲花)。
|
||||
- 关系预览技能区对齐官网:可点选技能 / 魔晶 / 神杖升级,右侧显示描述;
|
||||
- 上分帝 Web技能区对齐官网:可点选技能 / 魔晶 / 神杖升级,右侧显示描述;
|
||||
下方展示天赋树(25→10)。数据来自 Valve herodata(`fetch_hero_abilities.py`)。
|
||||
- 关系预览选中英雄后展示技能(图标 + 名称;可驱散角标;悬停看描述);
|
||||
- 上分帝 Web选中英雄后展示技能(图标 + 名称;可驱散角标;悬停看描述);
|
||||
数据来自 `data/hero_abilities.json`,图标按需缓存到 `assets/ability_icons/`。
|
||||
- 关系预览「怕的装备」:`fetch_items_meta.py`(Valve/OpenDota 描述 → 机制标签)、
|
||||
- 上分帝 Web「怕的装备」:`fetch_items_meta.py`(Valve/OpenDota 描述 → 机制标签)、
|
||||
`fetch_hero_abilities.py`(技能驱散等汇总)、`item_fears.py`(规则映射)→
|
||||
`data/items_meta.json` / `hero_abilities.json` / `hero_item_fears.json`;
|
||||
详情区在「核心」下增加「怕」行。例外标签见 `data/item_tag_overrides.json`。
|
||||
本阶段仅预览,无对局内出装推荐。
|
||||
- 关系预览选中英雄后,下方展示中文定位 `tags` 与核心成品装备(不再分阶段);
|
||||
本阶段仅 Web 站点,无对局内出装推荐。
|
||||
- 上分帝 Web选中英雄后,下方展示中文定位 `tags` 与核心成品装备(不再分阶段);
|
||||
`fetch_hero_items.py`:OpenDota 热度合并 + Valve 中文名,Steam CDN 图标 →
|
||||
`data/hero_items.json` / `assets/item_icons/`(过滤消耗品/中立/廉价件;
|
||||
坚韧球/夜叉等中间件若该英雄常继续合成则隐藏,停在该件则保留)。
|
||||
@@ -218,34 +373,34 @@
|
||||
- `data/heroes.json` 增加中文口语别名 `aliases`(如斯拉达→大鱼人、主宰→剑圣);
|
||||
`fetch_cdn_templates.py` 重拉时按 `key` 合并保留,不覆盖手工收录。
|
||||
- `data/heroes.json` 增加英文缩写 `abbr`(小写列表,如 nec / vs / pa);
|
||||
关系预览搜索支持缩写且不区分大小写;重拉模板时保留该字段。
|
||||
上分帝 Web搜索支持缩写且不区分大小写;重拉模板时保留该字段。
|
||||
- `fetch_stratz_matchups.py` + `audit_relations.py`:拉取 STRATZ 对位并与 OpenDota
|
||||
交叉审计定性关系,输出 `data/relations_audit.json`。
|
||||
- 定性关系库 `data/relations.json`(克制有向边 + 搭档 + 理由);
|
||||
`import_relations_xlsx.py` 可从选将笔记导入;`serve_relations.py` 三视图只读预览(改 JSON 后刷新)。
|
||||
- `fetch_hero_portraits.py` 下载 dota2.com/heroes 竖版头像到 `assets/hero_portraits/`,
|
||||
供关系预览网格使用(与匹配用 `templates/cdn/` 分离)。
|
||||
供上分帝 Web网格使用(与匹配用 `templates/cdn/` 分离)。
|
||||
- 英雄中文定位标签 `tags`(核心/辅助/推进/逃生/爆发/先手/控制/耐久/幻象);
|
||||
关系预览页可筛选,不参与推荐打分。
|
||||
- 关系预览属性图标改用 dota2.com 官方素材(`assets/attr_icons/`),不再手绘。
|
||||
上分帝 Web 页可筛选,不参与推荐打分。
|
||||
- 上分帝 Web属性图标改用 dota2.com 官方素材(`assets/attr_icons/`),不再手绘。
|
||||
- 选将 Top-3 推荐改为读定性关系:+克制敌方 / −被敌方克 / +己方搭档;
|
||||
叠层左上角青方标显示名次 1/2/3。
|
||||
- draft JSON / GSI `self` 透传 `accountid`、`steamid`。
|
||||
|
||||
### Changed
|
||||
|
||||
- 关系预览英雄详情(`#detail`):贴视口底部展示(`margin-top: auto`,高度仍按内容收缩);
|
||||
- 上分帝 Web英雄详情(`#detail`):贴视口底部展示(`margin-top: auto`,高度仍按内容收缩);
|
||||
与上方英雄选择区保留约 16px 间距;标题 / 属性条 / 分页 / 技能与装备行 / 检视区整体水平居中(最大宽约 820px),长描述在居中容器内仍左对齐。物品页布局未改。
|
||||
- 关系预览技能详情区不再重复放大技能图标;顶栏选中态即可识别,详情只保留名称 / 标签 / 驱散 / 描述(核心/怕装详情仍显示装备大图)。
|
||||
- 关系预览先天技能统一使用共用金色水滴图标 `assets/ability_icons/innate.png`
|
||||
- 上分帝 Web技能详情区不再重复放大技能图标;顶栏选中态即可识别,详情只保留名称 / 标签 / 驱散 / 描述(核心/怕装详情仍显示装备大图)。
|
||||
- 上分帝 Web先天技能统一使用共用金色水滴图标 `assets/ability_icons/innate.png`
|
||||
(多数 innate 在 Steam `dota_react/abilities` 上 404),技能行与详情区不再破图占位。
|
||||
- 关系预览英雄详情:技能图标与核心/怕装列表图标加大(技能 68px、装备 72×54),
|
||||
- 上分帝 Web英雄详情:技能图标与核心/怕装列表图标加大(技能 68px、装备 72×54),
|
||||
便于点选;商店页 `.shop-item` 尺寸未改。
|
||||
- 物品页详情面板改为与 11 列商店并排居中集群(`sticky`),贴在商店右侧约 16px,
|
||||
不再固定贴视口右缘;窄屏仍堆叠置顶。
|
||||
- 物品页:`#items-view` 限制为视口内滚动容器,避免 `sticky` 详情随窗口滚动盖住商店列;
|
||||
空状态去掉过大 `min-height`,面板高度随内容收缩。
|
||||
- 关系预览天赋树改为官网弹层风格(居中「天赋树」、25→10 圆形等级徽标、左右选项行)。
|
||||
- 上分帝 Web天赋树改为官网弹层风格(居中「天赋树」、25→10 圆形等级徽标、左右选项行)。
|
||||
- `heroes.json` 迁至 `data/heroes.json`;可再生成的对位/审计/报告文件改由 `.gitignore` 忽略。
|
||||
- `assets/role_icons/` 改用 Valve 游戏内选人筛选图标(`filter_*`);灰蓝底板 + 深色
|
||||
符号,透明角填品红 chroma,叠层不再出现黑边。
|
||||
@@ -268,24 +423,24 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- 关系预览英雄卡片 title 垃圾清理:`data/relations.json` 的 counters 被
|
||||
- 上分帝 Web英雄卡片 title 垃圾清理:`data/relations.json` 的 counters 被
|
||||
`relations_patch_draft.json` 注入 100 条 reason 为 `opendota_adv/stratz_adv`
|
||||
胜率数值的边(违反 `meta.note: no winrate` 与 AGENTS.md「不要用胜率/场次表达
|
||||
机制克制」),拼进 hover title 显示为「opendota_adv=0.13; stratz_adv=4.47」
|
||||
之类。删除这 100 条 numeric-reason 边(含水人 axe/hoodwink/windrunner →
|
||||
morphling 三条),保留 5 条定性 + 149 条空 reason counters + 11 条
|
||||
synergies(空 reason 经 `.filter(Boolean)` 不进 title)。
|
||||
- 关系预览英雄页:`#detail` 不再 `flex: 1` 撑满网格下方剩余视口(短内容时大片黑底空白);
|
||||
- 上分帝 Web英雄页:`#detail` 不再 `flex: 1` 撑满网格下方剩余视口(短内容时大片黑底空白);
|
||||
改为按内容收缩(`fit-content`),并用 `max-height` 限制长文案在面板内滚动。
|
||||
- 关系预览技能行对齐官网:左起「天赋树图标 → 先天圆形图标 → 方形技能」;天赋详情改为
|
||||
- 上分帝 Web技能行对齐官网:左起「天赋树图标 → 先天圆形图标 → 方形技能」;天赋详情改为
|
||||
悬停/点击浮动弹层(不再内联占高);图标见 `assets/ability_icons/talent_tree.png`。
|
||||
- 关系预览天赋树数值修复:天赋 `name_loc` 的 `{s:bonus_<sv>}` token 原全部
|
||||
- 上分帝 Web天赋树数值修复:天赋 `name_loc` 的 `{s:bonus_<sv>}` token 原全部
|
||||
显示为 `+?`/`-?`(`format_loc` 只读 `values_float`、`talent_rows` 只传天赋
|
||||
自身空 `special_values`)。改为跨技能按 talent key 索引 `bonuses` + sv 名
|
||||
/单 token 配对两层兜底填 `bonus.value`;重生成 `data/hero_abilities.json`,
|
||||
1016 条全部恢复。naga_siren「捕捞速度」上游缺 bonus,经
|
||||
`data/talent_overrides.json` 手工补 `+125`。
|
||||
- 关系预览核心装备「阿哈利姆福佑」→「阿哈利姆神杖」:`ultimate_scepter_2`
|
||||
- 上分帝 Web核心装备「阿哈利姆福佑」→「阿哈利姆神杖」:`ultimate_scepter_2`
|
||||
(id 271) 是神杖 (id 108) 的激活形态,OpenDota 分开计数导致 37 个英雄
|
||||
核心装备列表出现「福佑」甚至神杖+福佑并存。`fetch_hero_items.py` 新增
|
||||
`fold_blessing_into_scepter` 把 blessing 计数按 max 合并入 scepter,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Two-pass keyzoo helper: stash Feishu URL then run notify with Cloudflare.
|
||||
|
||||
Pass 1 (feishu asset): python _notify_pass_feishu.py stash
|
||||
Pass 2 (cloudflare): python _notify_pass_feishu.py run [--day ...] [--dry-run]
|
||||
|
||||
Temp file is user-only and deleted after run.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
STASH = Path(os.environ.get("TEMP") or os.environ.get("TMP") or ".") / "climperor_feishu_wh.url"
|
||||
|
||||
|
||||
def stash() -> None:
|
||||
url = os.environ.get("KEYZOO_ASSET_SECRET_FEISHU_WEBHOOK_URL") or os.environ.get(
|
||||
"FEISHU_WEBHOOK_URL"
|
||||
)
|
||||
if not url:
|
||||
raise SystemExit("no feishu webhook in env")
|
||||
STASH.write_text(url, encoding="utf-8")
|
||||
try:
|
||||
os.chmod(STASH, stat.S_IRUSR | stat.S_IWUSR)
|
||||
except OSError:
|
||||
pass
|
||||
print(f"stashed webhook -> {STASH.name} ({STASH.exists()})")
|
||||
|
||||
|
||||
def run(argv: list[str]) -> int:
|
||||
if not STASH.exists():
|
||||
raise SystemExit(f"missing stash {STASH}; run pass1 first")
|
||||
url = STASH.read_text(encoding="utf-8").strip()
|
||||
try:
|
||||
STASH.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
os.environ["FEISHU_WEBHOOK_URL"] = url
|
||||
# Import after env is set
|
||||
import notify_site_traffic as n
|
||||
|
||||
return n.main(argv)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("cmd", choices=("stash", "run"))
|
||||
ap.add_argument("--day")
|
||||
ap.add_argument("--dry-run", action="store_true")
|
||||
args, rest = ap.parse_known_args()
|
||||
if args.cmd == "stash":
|
||||
stash()
|
||||
return 0
|
||||
argv: list[str] = []
|
||||
if args.day:
|
||||
argv += ["--day", args.day]
|
||||
if args.dry_run:
|
||||
argv += ["--dry-run"]
|
||||
argv += rest
|
||||
return run(argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,370 @@
|
||||
"""Daily site traffic digest -> Feishu webhook.
|
||||
|
||||
Pulls Cloudflare edge analytics for dota2.refining.dev, estimates human
|
||||
homepage visits (known browser UA on `/`, plus RUM beacons), and posts a
|
||||
Feishu custom-bot card.
|
||||
|
||||
Credentials (env, prefer keyzoo inject names):
|
||||
CLOUDFLARE_EMAIL / KEYZOO_ASSET_META_USERNAME
|
||||
CLOUDFLARE_API_KEY / KEYZOO_ASSET_SECRET_GLOBAL_API_KEY
|
||||
FEISHU_WEBHOOK_URL / KEYZOO_ASSET_SECRET_FEISHU_WEBHOOK_URL
|
||||
|
||||
Usage:
|
||||
python notify_site_traffic.py # yesterday (UTC date)
|
||||
python notify_site_traffic.py --day 2026-07-27
|
||||
python notify_site_traffic.py --dry-run # print card, do not POST
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
HOST = "dota2.refining.dev"
|
||||
ZONE_NAME = "refining.dev"
|
||||
SITE_URL = "https://dota2.refining.dev"
|
||||
|
||||
HUMAN_BROWSERS = frozenset(
|
||||
{
|
||||
"Chrome",
|
||||
"Firefox",
|
||||
"Safari",
|
||||
"Edge",
|
||||
"Opera",
|
||||
"MobileSafari",
|
||||
"ChromeMobile",
|
||||
"ChromeMobileWebview",
|
||||
"FirefoxMobile",
|
||||
"SamsungInternet",
|
||||
"EdgeMobile",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _env(*names: str) -> str | None:
|
||||
for n in names:
|
||||
v = os.environ.get(n)
|
||||
if v:
|
||||
return v
|
||||
return None
|
||||
|
||||
|
||||
def cf_headers() -> dict[str, str]:
|
||||
email = _env("CLOUDFLARE_EMAIL", "KEYZOO_ASSET_META_USERNAME")
|
||||
key = _env("CLOUDFLARE_API_KEY", "KEYZOO_ASSET_SECRET_GLOBAL_API_KEY")
|
||||
if not email or not key:
|
||||
raise SystemExit(
|
||||
"missing Cloudflare credentials "
|
||||
"(CLOUDFLARE_EMAIL + CLOUDFLARE_API_KEY, or keyzoo inject)"
|
||||
)
|
||||
return {
|
||||
"X-Auth-Email": email,
|
||||
"X-Auth-Key": key,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
|
||||
def feishu_url() -> str:
|
||||
url = _env("FEISHU_WEBHOOK_URL", "KEYZOO_ASSET_SECRET_FEISHU_WEBHOOK_URL")
|
||||
if not url:
|
||||
raise SystemExit(
|
||||
"missing Feishu webhook "
|
||||
"(FEISHU_WEBHOOK_URL or KEYZOO_ASSET_SECRET_FEISHU_WEBHOOK_URL)"
|
||||
)
|
||||
return url
|
||||
|
||||
|
||||
def http_json(url: str, *, method: str = "GET", body: dict | None = None, headers: dict | None = None) -> dict:
|
||||
data = None if body is None else json.dumps(body).encode()
|
||||
req = urllib.request.Request(url, data=data, method=method, headers=headers or {})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
return json.loads(r.read().decode())
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read().decode(errors="replace")
|
||||
raise SystemExit(f"HTTP {e.code} {url}: {raw[:800]}") from e
|
||||
|
||||
|
||||
def gql(query: str, variables: dict) -> dict:
|
||||
data = http_json(
|
||||
"https://api.cloudflare.com/client/v4/graphql",
|
||||
method="POST",
|
||||
body={"query": query, "variables": variables},
|
||||
headers=cf_headers(),
|
||||
)
|
||||
if data.get("errors"):
|
||||
raise SystemExit("GraphQL errors: " + json.dumps(data["errors"], ensure_ascii=False)[:1500])
|
||||
return data
|
||||
|
||||
|
||||
@dataclass
|
||||
class DayStats:
|
||||
day: date
|
||||
all_req: int = 0
|
||||
all_vis: int = 0
|
||||
human_home_req: int = 0
|
||||
human_home_vis: int = 0
|
||||
other_home_vis: int = 0
|
||||
rum_human: int = 0
|
||||
rum_other: int = 0
|
||||
bytes: int = 0
|
||||
top_countries: list[tuple[str, int, int]] = field(default_factory=list) # name, req, vis
|
||||
status: list[tuple[Any, int]] = field(default_factory=list)
|
||||
|
||||
|
||||
def zone_id() -> str:
|
||||
d = http_json(
|
||||
f"https://api.cloudflare.com/client/v4/zones?name={ZONE_NAME}",
|
||||
headers=cf_headers(),
|
||||
)
|
||||
rows = d.get("result") or []
|
||||
if not rows:
|
||||
raise SystemExit(f"zone not found: {ZONE_NAME}")
|
||||
return rows[0]["id"]
|
||||
|
||||
|
||||
def fetch_day(zid: str, day: date) -> DayStats:
|
||||
q = """
|
||||
query($zoneTag: string!, $day: Date!, $host: string!) {
|
||||
viewer {
|
||||
zones(filter: { zoneTag: $zoneTag }) {
|
||||
all: httpRequestsAdaptiveGroups(
|
||||
limit: 1
|
||||
filter: { date: $day, clientRequestHTTPHost: $host }
|
||||
) { count sum { visits edgeResponseBytes } }
|
||||
homeByUA: httpRequestsAdaptiveGroups(
|
||||
limit: 40
|
||||
filter: {
|
||||
date: $day
|
||||
clientRequestHTTPHost: $host
|
||||
clientRequestPath: "/"
|
||||
}
|
||||
orderBy: [count_DESC]
|
||||
) {
|
||||
dimensions { userAgentBrowser }
|
||||
count
|
||||
sum { visits }
|
||||
}
|
||||
rumByUA: httpRequestsAdaptiveGroups(
|
||||
limit: 20
|
||||
filter: {
|
||||
date: $day
|
||||
clientRequestHTTPHost: $host
|
||||
clientRequestPath: "/cdn-cgi/rum"
|
||||
}
|
||||
orderBy: [count_DESC]
|
||||
) {
|
||||
dimensions { userAgentBrowser }
|
||||
count
|
||||
}
|
||||
homeByCountry: httpRequestsAdaptiveGroups(
|
||||
limit: 8
|
||||
filter: {
|
||||
date: $day
|
||||
clientRequestHTTPHost: $host
|
||||
clientRequestPath: "/"
|
||||
}
|
||||
orderBy: [count_DESC]
|
||||
) {
|
||||
dimensions { clientCountryName }
|
||||
count
|
||||
sum { visits }
|
||||
}
|
||||
byStatus: httpRequestsAdaptiveGroups(
|
||||
limit: 8
|
||||
filter: { date: $day, clientRequestHTTPHost: $host }
|
||||
orderBy: [count_DESC]
|
||||
) {
|
||||
dimensions { edgeResponseStatus }
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
data = gql(q, {"zoneTag": zid, "day": day.isoformat(), "host": HOST})
|
||||
z = data["data"]["viewer"]["zones"][0]
|
||||
st = DayStats(day=day)
|
||||
|
||||
all_rows = z.get("all") or []
|
||||
if all_rows:
|
||||
r = all_rows[0]
|
||||
st.all_req = r.get("count") or 0
|
||||
st.all_vis = (r.get("sum") or {}).get("visits") or 0
|
||||
st.bytes = (r.get("sum") or {}).get("edgeResponseBytes") or 0
|
||||
|
||||
for r in z.get("homeByUA") or []:
|
||||
ua = (r.get("dimensions") or {}).get("userAgentBrowser")
|
||||
c = r.get("count") or 0
|
||||
v = (r.get("sum") or {}).get("visits") or 0
|
||||
if ua in HUMAN_BROWSERS:
|
||||
st.human_home_req += c
|
||||
st.human_home_vis += v
|
||||
else:
|
||||
st.other_home_vis += v
|
||||
|
||||
for r in z.get("rumByUA") or []:
|
||||
ua = (r.get("dimensions") or {}).get("userAgentBrowser")
|
||||
c = r.get("count") or 0
|
||||
if ua in HUMAN_BROWSERS:
|
||||
st.rum_human += c
|
||||
else:
|
||||
st.rum_other += c
|
||||
|
||||
for r in z.get("homeByCountry") or []:
|
||||
name = (r.get("dimensions") or {}).get("clientCountryName") or "?"
|
||||
v = (r.get("sum") or {}).get("visits") or 0
|
||||
st.top_countries.append((name, r.get("count") or 0, v))
|
||||
|
||||
for r in z.get("byStatus") or []:
|
||||
code = (r.get("dimensions") or {}).get("edgeResponseStatus")
|
||||
st.status.append((code, r.get("count") or 0))
|
||||
|
||||
return st
|
||||
|
||||
|
||||
def fmt_bytes(n: int) -> str:
|
||||
x = float(n)
|
||||
for unit in ("B", "KB", "MB", "GB"):
|
||||
if abs(x) < 1024:
|
||||
return f"{x:.1f} {unit}"
|
||||
x /= 1024
|
||||
return f"{x:.1f} TB"
|
||||
|
||||
|
||||
def build_card(st: DayStats) -> dict:
|
||||
"""Feishu interactive card (msg_type=interactive)."""
|
||||
countries = "、".join(
|
||||
f"{name} {vis}访/{req}次" for name, req, vis in st.top_countries[:5]
|
||||
) or "—"
|
||||
status_line = "、".join(f"{code}×{n}" for code, n in st.status[:6]) or "—"
|
||||
now = datetime.now(timezone(timedelta(hours=8))).strftime("%Y-%m-%d %H:%M CST")
|
||||
|
||||
# Free plan: no BotScore. Human ≈ known-browser homepage visits.
|
||||
estimate = st.human_home_vis
|
||||
rum = st.rum_human
|
||||
|
||||
elements: list[dict] = [
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "lark_md",
|
||||
"content": (
|
||||
f"**真实访问(估)**:约 **{estimate}** 次首页打开\n"
|
||||
f"(已知浏览器 UA 打开 `/`;不含 Unknown/Headless/爬虫)\n\n"
|
||||
f"**RUM 页面浏览**:{rum}(人类浏览器执行 JS)"
|
||||
+ (f" + {st.rum_other} 其他" if st.rum_other else "")
|
||||
+ "\n"
|
||||
f"**边缘总量**:{st.all_req} 请求 / {st.all_vis} visits / {fmt_bytes(st.bytes)}\n"
|
||||
f"**非人类首页命中**:{st.other_home_vis} visits"
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "lark_md",
|
||||
"content": f"**首页地区 Top**:{countries}\n**状态码**:{status_line}",
|
||||
},
|
||||
},
|
||||
{
|
||||
"tag": "note",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "plain_text",
|
||||
"content": (
|
||||
f"{HOST} · Free 档无 BotScore,按 UA+路径估算 · {now}"
|
||||
),
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"tag": "action",
|
||||
"actions": [
|
||||
{
|
||||
"tag": "button",
|
||||
"text": {"tag": "plain_text", "content": "打开站点"},
|
||||
"type": "primary",
|
||||
"url": SITE_URL,
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
"msg_type": "interactive",
|
||||
"card": {
|
||||
"header": {
|
||||
"title": {
|
||||
"tag": "plain_text",
|
||||
"content": f"上分帝 Web 日活 · {st.day.isoformat()}",
|
||||
},
|
||||
"template": "blue" if estimate > 0 else "grey",
|
||||
},
|
||||
"elements": elements,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def post_feishu(payload: dict) -> dict:
|
||||
url = feishu_url()
|
||||
return http_json(
|
||||
url,
|
||||
method="POST",
|
||||
body=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
|
||||
def parse_day(s: str | None) -> date:
|
||||
if not s:
|
||||
# Yesterday in China time (site audience).
|
||||
cst = timezone(timedelta(hours=8))
|
||||
return (datetime.now(cst) - timedelta(days=1)).date()
|
||||
return date.fromisoformat(s)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--day", help="YYYY-MM-DD (default: yesterday CST)")
|
||||
ap.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="print JSON card only, do not call Feishu",
|
||||
)
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
day = parse_day(args.day)
|
||||
zid = zone_id()
|
||||
st = fetch_day(zid, day)
|
||||
payload = build_card(st)
|
||||
|
||||
print(
|
||||
f"{HOST} {day}: human_home_vis={st.human_home_vis} "
|
||||
f"rum_human={st.rum_human} all_req={st.all_req}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
if args.dry_run:
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
resp = post_feishu(payload)
|
||||
# Feishu returns {code:0, msg:success} on OK
|
||||
code = resp.get("code", resp.get("StatusCode"))
|
||||
if code not in (0, None) and code != 200:
|
||||
print("Feishu error:", json.dumps(resp, ensure_ascii=False)[:800], file=sys.stderr)
|
||||
return 1
|
||||
print("feishu ok:", json.dumps(resp, ensure_ascii=False)[:200])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,38 @@
|
||||
# Local / scheduled runner: inject both keyzoo secrets then post Feishu digest.
|
||||
#
|
||||
# Requires Keyzoo desktop daemon (MCP asset_exec). From an agent:
|
||||
# run this script is not enough alone — use notify_site_traffic.py via
|
||||
# dual asset_exec, or set env manually:
|
||||
#
|
||||
# $env:CLOUDFLARE_EMAIL = "..."
|
||||
# $env:CLOUDFLARE_API_KEY = "..."
|
||||
# $env:FEISHU_WEBHOOK_URL = "..."
|
||||
# python notify_site_traffic.py
|
||||
#
|
||||
# Windows Task Scheduler example (after env is available in the task):
|
||||
# Program: python
|
||||
# Arguments: notify_site_traffic.py
|
||||
# Start in: <repo>
|
||||
# Trigger: Daily 09:00
|
||||
|
||||
param(
|
||||
[string]$Day = "",
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
Set-Location $Root
|
||||
|
||||
$argsList = @()
|
||||
if ($Day) { $argsList += @("--day", $Day) }
|
||||
if ($DryRun) { $argsList += "--dry-run" }
|
||||
|
||||
if (-not $env:CLOUDFLARE_API_KEY -and -not $env:KEYZOO_ASSET_SECRET_GLOBAL_API_KEY) {
|
||||
Write-Error "Missing Cloudflare API key in env (set CLOUDFLARE_* or run via keyzoo)."
|
||||
}
|
||||
if (-not $env:FEISHU_WEBHOOK_URL -and -not $env:KEYZOO_ASSET_SECRET_FEISHU_WEBHOOK_URL) {
|
||||
Write-Error "Missing Feishu webhook in env (set FEISHU_WEBHOOK_URL or run via keyzoo)."
|
||||
}
|
||||
|
||||
python notify_site_traffic.py @argsList
|
||||
Reference in New Issue
Block a user