Add Cloudflare traffic helper scripts and commercialization notes.

Track reusable CF analytics utilities and the commercial prospects document while keeping local probe/sample artifacts untracked.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-29 12:44:31 +08:00
co-authored by Cursor
parent 3ec8007077
commit 96a9312194
4 changed files with 922 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
"""Human homepage opens: path=/ AND known browser UA (exclude bots/unknown)."""
from __future__ import annotations
import json
import os
import urllib.request
from datetime import date, timedelta
HOST = "dota2.refining.dev"
ZONE = "refining.dev"
HUMAN_BROWSERS = [
"Chrome",
"Firefox",
"Safari",
"Edge",
"Opera",
"MobileSafari",
"ChromeMobile",
"ChromeMobileWebview",
"FirefoxMobile",
"SamsungInternet",
"EdgeMobile",
]
def headers() -> dict[str, str]:
email = os.environ["KEYZOO_ASSET_META_USERNAME"]
key = os.environ["KEYZOO_ASSET_SECRET_GLOBAL_API_KEY"]
return {
"X-Auth-Email": email,
"X-Auth-Key": key,
"Content-Type": "application/json",
}
def gql(query: str, variables: dict) -> dict:
req = urllib.request.Request(
"https://api.cloudflare.com/client/v4/graphql",
data=json.dumps({"query": query, "variables": variables}).encode(),
method="POST",
headers=headers(),
)
with urllib.request.urlopen(req) as r:
return json.loads(r.read().decode())
def main() -> None:
zid = json.loads(
urllib.request.urlopen(
urllib.request.Request(
f"https://api.cloudflare.com/client/v4/zones?name={ZONE}",
headers=headers(),
)
).read()
)["result"][0]["id"]
q = """
query($zoneTag:string!,$day:Date!,$host:string!){
viewer{zones(filter:{zoneTag:$zoneTag}){
homeByUA:httpRequestsAdaptiveGroups(
limit:30
filter:{date:$day,clientRequestHTTPHost:$host,clientRequestPath:"/"}
orderBy:[count_DESC]
){dimensions{userAgentBrowser clientCountryName} count sum{visits}}
rumByUA:httpRequestsAdaptiveGroups(
limit:20
filter:{
date:$day
clientRequestHTTPHost:$host
clientRequestPath:"/cdn-cgi/rum"
}
orderBy:[count_DESC]
){dimensions{userAgentBrowser} count sum{visits}}
homeByCountry:httpRequestsAdaptiveGroups(
limit:15
filter:{date:$day,clientRequestHTTPHost:$host,clientRequestPath:"/"}
orderBy:[count_DESC]
){dimensions{clientCountryName} count sum{visits}}
}}
}
"""
print(f"{HOST} homepage (/) by UA | RUM by UA\n")
tot_human_home_req = tot_human_home_vis = 0
tot_rum = 0
tot_bot_home_vis = 0
for i in range(7, -1, -1):
day = date.today() - timedelta(days=i)
data = gql(q, {"zoneTag": zid, "day": day.isoformat(), "host": HOST})
if data.get("errors"):
print(day, data["errors"][0]["message"][:120])
continue
z = data["data"]["viewer"]["zones"][0]
home = z["homeByUA"] or []
rum = z["rumByUA"] or []
if not home and not rum:
continue
human_req = human_vis = bot_vis = rum_c = 0
print(f"=== {day} ===")
print(" homepage / by UA:")
for r in home:
ua = (r["dimensions"] or {}).get("userAgentBrowser")
c = r["count"] or 0
v = (r.get("sum") or {}).get("visits") or 0
tag = "HUMAN" if ua in HUMAN_BROWSERS else "bot/other"
print(f" {c:>4} req {v:>4} vis [{tag}] {ua}")
if ua in HUMAN_BROWSERS:
human_req += c
human_vis += v
else:
bot_vis += v
print(" RUM by UA:")
for r in rum:
ua = (r["dimensions"] or {}).get("userAgentBrowser")
c = r["count"] or 0
rum_c += c
print(f" {c:>4} beacons {ua}")
print(" homepage / by country:")
for r in z["homeByCountry"] or []:
cc = (r["dimensions"] or {}).get("clientCountryName")
v = (r.get("sum") or {}).get("visits") or 0
print(f" {r['count']:>4} req {v:>4} vis {cc}")
print(
f" day human home: {human_req} req / {human_vis} visits | "
f"bot-other home visits: {bot_vis} | rum: {rum_c}"
)
tot_human_home_req += human_req
tot_human_home_vis += human_vis
tot_bot_home_vis += bot_vis
tot_rum += rum_c
print("\n======== SUMMARY (8 days) ========")
print(f" Human browser homepage requests: {tot_human_home_req}")
print(f" Human browser homepage visits: {tot_human_home_vis}")
print(f" Non-human homepage visits: {tot_bot_home_vis}")
print(f" RUM beacons (JS pageviews): {tot_rum}")
print()
print(" Verdict (exclude crawlers):")
print(f" ~{tot_human_home_vis} real homepage visits (known browser UA + /)")
print(f" ~{tot_rum} RUM pageviews (browser executed JS)")
# If RUM > human home visits, same session may fire multiple beacons
# or SPA navigations; take human home visits as unique-ish sessions,
# RUM as pageview count.
lo = min(tot_human_home_vis, tot_rum) if tot_rum else tot_human_home_vis
hi = max(tot_human_home_vis, tot_rum) if tot_rum else tot_human_home_vis
print(f" Best estimate: ~{lo}{hi} real human opens")
if __name__ == "__main__":
main()
+386
View File
@@ -0,0 +1,386 @@
"""Estimate human traffic for dota2.refining.dev (exclude obvious bots).
Free plan has no BotScore; we use:
- HTML document hits (/) vs asset/bot paths
- RUM beacons (/cdn-cgi/rum) as JS-executing browsers
- UA browser family when available
- Country + path heuristics
"""
from __future__ import annotations
import json
import os
import urllib.error
import urllib.request
from datetime import date, timedelta
HOST = "dota2.refining.dev"
ZONE_NAME = "refining.dev"
def headers() -> dict[str, str]:
email = os.environ.get("CLOUDFLARE_EMAIL") or os.environ.get(
"KEYZOO_ASSET_META_USERNAME"
)
key = os.environ.get("CLOUDFLARE_API_KEY") or os.environ.get(
"KEYZOO_ASSET_SECRET_GLOBAL_API_KEY"
)
if not email or not key:
raise SystemExit("missing credentials in env")
return {
"X-Auth-Email": email,
"X-Auth-Key": key,
"Content-Type": "application/json",
}
def api(path: str) -> dict:
req = urllib.request.Request(
"https://api.cloudflare.com/client/v4" + path,
method="GET",
headers=headers(),
)
with urllib.request.urlopen(req) as r:
return json.loads(r.read().decode())
def gql(query: str, variables: dict) -> dict:
req = urllib.request.Request(
"https://api.cloudflare.com/client/v4/graphql",
data=json.dumps({"query": query, "variables": variables}).encode(),
method="POST",
headers=headers(),
)
try:
with urllib.request.urlopen(req) as r:
return json.loads(r.read().decode())
except urllib.error.HTTPError as e:
return {"errors": [{"message": e.read().decode()[:1500]}]}
def try_query(label: str, query: str, variables: dict) -> dict | None:
data = gql(query, variables)
if data.get("errors"):
msg = data["errors"][0].get("message", "")[:200]
print(f" [{label}] unavailable: {msg}")
return None
return data
def main() -> None:
zid = api(f"/zones?name={ZONE_NAME}")["result"][0]["id"]
days = [date.today() - timedelta(days=i) for i in range(7, -1, -1)]
print(f"\n{HOST} human-traffic estimate (Free plan, no BotScore)\n")
# --- probe schema fields once ---
day0 = days[-2] if len(days) > 1 else days[-1] # prefer yesterday
probes = [
(
"UA browser",
"""
query($zoneTag:string!,$day:Date!,$host:string!){
viewer{zones(filter:{zoneTag:$zoneTag}){
g:httpRequestsAdaptiveGroups(
limit:20
filter:{date:$day,clientRequestHTTPHost:$host}
orderBy:[count_DESC]
){dimensions{userAgentBrowser} count sum{visits}}
}}
}
""",
),
(
"device type",
"""
query($zoneTag:string!,$day:Date!,$host:string!){
viewer{zones(filter:{zoneTag:$zoneTag}){
g:httpRequestsAdaptiveGroups(
limit:10
filter:{date:$day,clientRequestHTTPHost:$host}
orderBy:[count_DESC]
){dimensions{clientDeviceType} count sum{visits}}
}}
}
""",
),
(
"botScore",
"""
query($zoneTag:string!,$day:Date!,$host:string!){
viewer{zones(filter:{zoneTag:$zoneTag}){
g:httpRequestsAdaptiveGroups(
limit:10
filter:{date:$day,clientRequestHTTPHost:$host,botScore_geq:30}
orderBy:[count_DESC]
){count sum{visits} dimensions{botScore}}
}}
}
""",
),
(
"content type",
"""
query($zoneTag:string!,$day:Date!,$host:string!){
viewer{zones(filter:{zoneTag:$zoneTag}){
g:httpRequestsAdaptiveGroups(
limit:15
filter:{date:$day,clientRequestHTTPHost:$host}
orderBy:[count_DESC]
){dimensions{edgeResponseContentTypeName} count sum{visits}}
}}
}
""",
),
]
print(f"Schema probes on {day0}:")
available = {}
for label, q in probes:
data = try_query(
label,
q,
{"zoneTag": zid, "day": day0.isoformat(), "host": HOST},
)
if data:
available[label] = data["data"]["viewer"]["zones"][0]["g"]
print(f" [{label}] OK")
for label, rows in available.items():
print(f"\n--- {label} ({day0}) ---")
for r in rows[:12]:
dims = r.get("dimensions") or {}
key = next(iter(dims.values()), "?") if dims else "?"
v = (r.get("sum") or {}).get("visits") or 0
print(f" {r['count']:>6} req {v:>5} vis {key}")
# --- per-day: total / homepage / rum / likely-bot paths ---
day_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}}
home:httpRequestsAdaptiveGroups(
limit:1
filter:{date:$day,clientRequestHTTPHost:$host,clientRequestPath:"/"}
){count sum{visits}}
rum:httpRequestsAdaptiveGroups(
limit:1
filter:{date:$day,clientRequestHTTPHost:$host,clientRequestPath:"/cdn-cgi/rum"}
){count sum{visits}}
html:httpRequestsAdaptiveGroups(
limit:1
filter:{
date:$day
clientRequestHTTPHost:$host
edgeResponseContentTypeName:"text/html"
}
){count sum{visits}}
byPath:httpRequestsAdaptiveGroups(
limit:40
filter:{date:$day,clientRequestHTTPHost:$host}
orderBy:[count_DESC]
){dimensions{clientRequestPath} count sum{visits}}
byUA:httpRequestsAdaptiveGroups(
limit:25
filter:{date:$day,clientRequestHTTPHost:$host}
orderBy:[count_DESC]
){dimensions{userAgentBrowser} count sum{visits}}
}}
}
"""
BOT_PATH_MARKERS = (
"wp-",
"wordpress",
"xmlrpc",
"wlwmanifest",
".env",
"phpmyadmin",
"robots.txt",
"sitemap",
"favicon.ico", # often bots; keep separate
)
HUMAN_UA = {
"Chrome",
"Firefox",
"Safari",
"Edge",
"Opera",
"Samsung Internet",
"Mobile Safari",
"Chrome Mobile",
"Firefox Mobile",
"Edg",
"Chromium",
}
BOT_UA = {
"Bot",
"Spider",
"Crawler",
"curl",
"Go-http-client",
"python",
"Python",
"Scrapy",
"Headless",
"Empty",
"Unknown",
"",
None,
}
print(f"\n{'date':<12} {'all_req':>8} {'all_vis':>8} {'home_vis':>8} {'rum':>6} {'html_vis':>8}")
totals = {
"all_req": 0,
"all_vis": 0,
"home_vis": 0,
"rum": 0,
"html_vis": 0,
"human_ua_vis": 0,
"bot_path_req": 0,
}
best_day = None
best_z = None
best_home = 0
for day in days:
data = try_query(
day.isoformat(),
day_q,
{"zoneTag": zid, "day": day.isoformat(), "host": HOST},
)
if not data:
# retry without html / UA if fields fail
day_q_min = """
query($zoneTag:string!,$day:Date!,$host:string!){
viewer{zones(filter:{zoneTag:$zoneTag}){
all:httpRequestsAdaptiveGroups(
limit:1 filter:{date:$day,clientRequestHTTPHost:$host}
){count sum{visits}}
home:httpRequestsAdaptiveGroups(
limit:1
filter:{date:$day,clientRequestHTTPHost:$host,clientRequestPath:"/"}
){count sum{visits}}
rum:httpRequestsAdaptiveGroups(
limit:1
filter:{
date:$day
clientRequestHTTPHost:$host
clientRequestPath:"/cdn-cgi/rum"
}
){count sum{visits}}
byPath:httpRequestsAdaptiveGroups(
limit:40
filter:{date:$day,clientRequestHTTPHost:$host}
orderBy:[count_DESC]
){dimensions{clientRequestPath} count sum{visits}}
}}
}
"""
data = try_query(
day.isoformat() + "-min",
day_q_min,
{"zoneTag": zid, "day": day.isoformat(), "host": HOST},
)
if not data:
continue
z = data["data"]["viewer"]["zones"][0]
def one(name: str) -> tuple[int, int]:
rows = z.get(name) or []
if not rows:
return 0, 0
r = rows[0]
return r.get("count") or 0, (r.get("sum") or {}).get("visits") or 0
all_c, all_v = one("all")
home_c, home_v = one("home")
rum_c, _ = one("rum")
html_c, html_v = one("html") if "html" in z else (0, 0)
print(
f"{day.isoformat():<12} {all_c:>8} {all_v:>8} {home_v:>8} {rum_c:>6} {html_v:>8}"
)
totals["all_req"] += all_c
totals["all_vis"] += all_v
totals["home_vis"] += home_v
totals["rum"] += rum_c
totals["html_vis"] += html_v
# bot-looking paths
for r in z.get("byPath") or []:
p = (r["dimensions"] or {}).get("clientRequestPath") or ""
if any(m in p.lower() for m in BOT_PATH_MARKERS):
totals["bot_path_req"] += r["count"] or 0
# UA sum visits for human browsers
for r in z.get("byUA") or []:
ua = (r["dimensions"] or {}).get("userAgentBrowser")
v = (r.get("sum") or {}).get("visits") or 0
if ua in HUMAN_UA or (
ua
and not any(
str(b).lower() in str(ua).lower()
for b in BOT_UA
if b
)
and ua
not in ("", None)
):
# count known browsers only
if ua in HUMAN_UA or (
isinstance(ua, str)
and any(
x in ua
for x in (
"Chrome",
"Firefox",
"Safari",
"Edge",
"Opera",
)
)
):
totals["human_ua_vis"] += v
if home_v >= best_home:
best_home = home_v
best_day = day
best_z = z
print("\n--- 7-day totals ---")
print(f" all edge requests: {totals['all_req']}")
print(f" all edge visits: {totals['all_vis']}")
print(f" homepage visits (/): {totals['home_vis']}")
print(f" RUM beacons: {totals['rum']} (JS ran = real browser)")
print(f" HTML content visits: {totals['html_vis']}")
print(f" obvious bot-path req: {totals['bot_path_req']}")
# Best estimate narrative
rum = totals["rum"]
home = totals["home_vis"]
# Conservative human sessions: min(home visits, rum) .. max, with rum as best
print("\n=== Real-human estimate ===")
print(f" Best proxy (RUM / JS pageviews): ~{rum}")
print(f" Homepage edge visits (/): ~{home}")
print(
f" Suggested range: ~{min(rum, home) if rum and home else max(rum, home)}"
f" .. ~{max(rum, home)} unique-ish real opens"
)
print(
" (RUM fires only when a real browser runs JS; scrapers hitting wp-*/assets do not.)"
)
if best_z and best_day:
print(f"\n--- UA browsers on {best_day} (if available) ---")
for r in (best_z.get("byUA") or [])[:15]:
ua = (r["dimensions"] or {}).get("userAgentBrowser")
v = (r.get("sum") or {}).get("visits") or 0
print(f" {r['count']:>6} req {v:>5} vis {ua!r}")
if __name__ == "__main__":
main()
+217
View File
@@ -0,0 +1,217 @@
"""Pull traffic for dota2.refining.dev via Cloudflare GraphQL Analytics API.
Credentials from env (keyzoo inject). Read-only; prints a short summary.
"""
from __future__ import annotations
import json
import os
import urllib.error
import urllib.request
from datetime import date, timedelta
HOST = "dota2.refining.dev"
ZONE_NAME = "refining.dev"
DAYS = 14
def headers() -> dict[str, str]:
email = os.environ.get("CLOUDFLARE_EMAIL") or os.environ.get(
"KEYZOO_ASSET_META_USERNAME"
)
key = os.environ.get("CLOUDFLARE_API_KEY") or os.environ.get(
"KEYZOO_ASSET_SECRET_GLOBAL_API_KEY"
)
if not email or not key:
raise SystemExit("missing credentials in env")
return {
"X-Auth-Email": email,
"X-Auth-Key": key,
"Content-Type": "application/json",
}
def api(path: str) -> dict:
req = urllib.request.Request(
"https://api.cloudflare.com/client/v4" + path,
method="GET",
headers=headers(),
)
with urllib.request.urlopen(req) as r:
return json.loads(r.read().decode())
def gql(query: str, variables: dict) -> dict:
req = urllib.request.Request(
"https://api.cloudflare.com/client/v4/graphql",
data=json.dumps({"query": query, "variables": variables}).encode(),
method="POST",
headers=headers(),
)
try:
with urllib.request.urlopen(req) as r:
return json.loads(r.read().decode())
except urllib.error.HTTPError as e:
body = e.read().decode()
raise SystemExit(f"HTTP {e.code}: {body[:2000]}") from e
def fmt_bytes(n: int | float | None) -> str:
if n is None:
return "-"
n = float(n)
for unit in ("B", "KB", "MB", "GB", "TB"):
if abs(n) < 1024:
return f"{n:.1f} {unit}"
n /= 1024
return f"{n:.1f} PB"
def main() -> None:
zd = api(f"/zones?name={ZONE_NAME}")
zone = (zd.get("result") or [None])[0]
if not zone:
raise SystemExit(f"zone not found: {zd.get('errors')}")
zid = zone["id"]
end = date.today()
start = end - timedelta(days=DAYS)
# Free plan: httpRequests1dGroups works for multi-day zone totals.
# Adaptive (host/path filter) is limited to a 1-day window.
zone_q = """
query($zoneTag: string!, $start: Date!, $end: Date!) {
viewer {
zones(filter: {zoneTag: $zoneTag}) {
httpRequests1dGroups(
limit: 20
filter: {date_geq: $start, date_leq: $end}
orderBy: [date_ASC]
) {
dimensions { date }
sum { requests bytes cachedRequests threats }
uniq { uniques }
}
}
}
}
"""
data = gql(
zone_q,
{"zoneTag": zid, "start": start.isoformat(), "end": end.isoformat()},
)
if data.get("errors"):
raise SystemExit(json.dumps(data["errors"], indent=2)[:4000])
rows = data["data"]["viewer"]["zones"][0]["httpRequests1dGroups"]
print(f"\n{ZONE_NAME} zone (all hosts) {start} .. {end}")
print(f"{'date':<12} {'requests':>10} {'uniques':>10} {'bytes':>12} {'threats':>8}")
tot_r = tot_u = tot_b = tot_t = 0
for row in rows:
d = row["dimensions"]["date"]
s = row["sum"]
u = row["uniq"]["uniques"]
print(
f"{d:<12} {s['requests']:>10} {u:>10} "
f"{fmt_bytes(s['bytes']):>12} {s['threats']:>8}"
)
tot_r += s["requests"]
tot_u += u
tot_b += s["bytes"]
tot_t += s["threats"]
print(
f"{'TOTAL':<12} {tot_r:>10} {tot_u:>10} "
f"{fmt_bytes(tot_b):>12} {tot_t:>8}"
)
# Host-scoped detail: one day at a time (Free quota).
host_q = """
query($zoneTag: string!, $day: Date!, $host: string!) {
viewer {
zones(filter: {zoneTag: $zoneTag}) {
total: httpRequestsAdaptiveGroups(
limit: 1
filter: {date: $day, clientRequestHTTPHost: $host}
) {
count
sum { edgeResponseBytes visits }
}
byPath: httpRequestsAdaptiveGroups(
limit: 15
filter: {date: $day, clientRequestHTTPHost: $host}
orderBy: [count_DESC]
) {
dimensions { clientRequestPath }
count
sum { visits }
}
byCountry: httpRequestsAdaptiveGroups(
limit: 8
filter: {date: $day, clientRequestHTTPHost: $host}
orderBy: [count_DESC]
) {
dimensions { clientCountryName }
count
sum { visits }
}
byStatus: httpRequestsAdaptiveGroups(
limit: 8
filter: {date: $day, clientRequestHTTPHost: $host}
orderBy: [count_DESC]
) {
dimensions { edgeResponseStatus }
count
}
}
}
}
"""
print(f"\n{HOST} host detail (last {min(DAYS, 7)} days, Free=1d/query)")
print(f"{'date':<12} {'requests':>10} {'visits':>10} {'bytes':>12}")
host_days = []
for i in range(min(DAYS, 7), -1, -1):
day = end - timedelta(days=i)
hd = gql(
host_q,
{"zoneTag": zid, "day": day.isoformat(), "host": HOST},
)
if hd.get("errors"):
print(f"{day.isoformat():<12} error: {hd['errors'][0].get('message')}")
continue
z = hd["data"]["viewer"]["zones"][0]
tot = (z["total"] or [{}])[0]
c = tot.get("count") or 0
v = (tot.get("sum") or {}).get("visits") or 0
b = (tot.get("sum") or {}).get("edgeResponseBytes") or 0
print(f"{day.isoformat():<12} {c:>10} {v:>10} {fmt_bytes(b):>12}")
host_days.append((day, z, c, v, b))
# Detail for the busiest recent day.
pick = max(host_days, key=lambda x: x[2], default=None)
if pick and pick[2] > 0:
day, z, _, _, _ = pick
print(f"\nDetail for busiest day {day.isoformat()} ({HOST})")
print("Top paths:")
for row in z["byPath"][:15]:
path = row["dimensions"].get("clientRequestPath") or "/"
v = (row.get("sum") or {}).get("visits") or 0
print(f" {row['count']:>8} req {v:>6} visits {path}")
print("Top countries:")
for row in z["byCountry"]:
name = row["dimensions"].get("clientCountryName") or "?"
v = (row.get("sum") or {}).get("visits") or 0
print(f" {row['count']:>8} req {v:>6} visits {name}")
print("Status codes:")
for row in z["byStatus"]:
st = row["dimensions"].get("edgeResponseStatus")
print(f" {st}: {row['count']}")
print(
"\nNote: edge requests/visits via Cloudflare proxy (not RUM pageviews). "
"Includes assets/bots. Zone totals mix all refining.dev hosts; "
"host table is dota2-only."
)
if __name__ == "__main__":
main()
+165
View File
@@ -0,0 +1,165 @@
# 上分帝(Climperor)商业化前景
基于当前产品形态(局内选将识别 + 关系/物品/版本知识站)与 Dota 2 市场结构的判断。
- 分析日期:2026-07-28
- 主战场假设:**中国大陆简中天梯玩家**(Overwolf 需科学上网,大陆侧基本不可用)
---
## 总判断
**大陆市场窗口更清晰——可赚钱的利基工具,仍非融资级平台。**
痛点真实、合规路径清晰。Overwolf 系助手在中国大陆需科学上网,实际不可用,局内免费广告竞品几乎空窗。主战场应锁定简中天梯用户;天花板仍受 Dota 付费层规模限制,适合「社区/内容 + Freemium」小而稳生意。
| 维度 | 评级 |
|------|------|
| 规模化潜力 | 中低 |
| 大陆利基变现 | 偏高 |
| 合规护城河 | 高 |
| 本地化窗口 | 中高 |
---
## 一、市场与需求
### TAM 量级(以大陆为主)
全球 Steam 并发约 42–60 万(2026 上半年均值波动,来源:Steam Charts / Steambase)。上分帝的有效市场是简中客户端天梯玩家。认真打定位局、愿为选将决策付费的人仍窄,但大陆侧几乎没有可用的 Overwolf 级局内助手,付费转化阻力低于海外。
### 真实痛点
Valve 已关闭普通玩家视角的实时 draft GSI。大陆玩家的替代路径主要是网页查表、直播间口播、自制笔记——选将阶段几秒内很难用。识别 + Overlay 填的是「局内实时」空缺,不是和 Overwolf 抢同一批用户。
### 渠道前提
产品已天然适配大陆:简体位置字模板、中文 aliases/tags、定性理由文案、国内可访问的知识站(Cloudflare Pages + 阿里云 OSS)。分发与内容都不必依赖科学上网。**不要走 Overwolf 商店分发。**
### 产品资产
| 资产层 | 现状 | 商业含义 |
|--------|------|----------|
| 局内识别 + Overlay | GSI 触发 + CDN 模板匹配 + Top-3 青标 | 差异化入口;安装成本高,转化漏斗长 |
| 定性关系数据 | `relations.json` 克制/搭档边 + 理由 | 可成为内容 IP;维护成本高,胜率库难替代 |
| 知识站 | 英雄/物品/版本,已部署 `dota2.refining.dev` | 获客与品牌面;可走广告/会员,不必装客户端 |
| 合规边界 | 仅 GSI + 截屏,禁读内存 | 降低封号恐惧,利于信任与渠道合作 |
---
## 二、竞争格局(大陆视角)
| 玩家 | 模式 | 大陆可用性 | 对上分帝的压力 |
|------|------|------------|----------------|
| Overwolf DotaPlus | 免费 + 广告;选禁建议 | 需科学上网,基本不可用 | 海外强竞品;大陆侧可忽略 |
| Valve Dota Plus | ~$3.99/月官方订阅 | 可用(Steam) | 官方光环;偏生涯/助手,无双方阵容识别 |
| Dotabuff / Stratz | 网页 + Plus 订阅 | 访问不稳 / 体验差 | 赛后数据强;选将实时几乎无威胁 |
| 中文 Wiki / 攻略站 / B 站 | 流量 + 广告 / 内容 | 完全可用 | 知识站主竞品;局内 overlay 仍空缺 |
| 本地脚本 / 群文件工具 | 免费、分散、常违规 | 可用但信任差 | 识别可被复刻;合规+体验可拉开差距 |
**相对优势:** 大陆「局内实时选将助手」几乎空白。① 合规视觉方案填补 GSI draft 空洞;② 中文定性克制(非胜率表);③ 识别→推荐→知识站闭环,且全链路不依赖科学上网。真正要赢的是中文内容站与信任感,不是打赢 Overwolf。
### 附录:大陆中文 Wiki / 攻略渠道
玩家实际查资料时,并不是单一站点垄断,而是「MAX+ 查数 + NGA/B 站看说法 + 官网百科对技能」。
#### 百科 / 官方资料
| 站点 | 地址 | 定位 |
|------|------|------|
| 刀塔百科 | https://wiki.dota2.com.cn/ | 国服官方向中文 Wiki(英雄/机制资料) |
| 完美世界官网 | https://www.dota2.com.cn/ | 英雄页、物品页、版本公告、活动 |
| Liquipedia | https://liquipedia.net/dota2/ | 赛事/机制权威 Wiki;英文为主,大陆可访问但不算中文站 |
#### 数据 + 攻略一体
| 站点 | 地址 | 定位 |
|------|------|------|
| MAX+ | https://maxjia.com/ | 战绩 + 英雄胜率/出装 + 社区攻略,移动端心智最强 |
#### 社区 UGC
| 站点 | 地址 | 定位 |
|------|------|------|
| NGA 刀塔区 | https://bbs.nga.cn/thread.php?fid=321 | 长文攻略、版本讨论、精华帖;深度最高 |
| 百度 Dota2 吧 | 贴吧 | 碎片讨论、整活、初级问答,质量参差 |
| B 站 | 搜索「DOTA2 版本 / 上分」 | 事实上最大的攻略形态(视频) |
#### 传统门户(影响力较弱)
- [17173 DOTA2 专区](https://dota2.17173.com/) — 旧式图文攻略,更新慢、常过期
- 游民星空 / 多玩等也曾有专区,现很少当主信息源
**与上分帝的关系:** 知识站主要抢注意力的是 MAX+ 英雄页与 NGA/B 站版本内容;局内实时 Overlay 上述站点都覆盖不到。
---
## 三、商业模式对照
### A. 知识站 Freemium(优先)
- 免费:浏览关系/物品/版本
- 付费:完整理由库、分路定制、导出、无广告、更新优先
- 获客成本低(SEO / Hash 深度链接),与安装客户端解耦
### B. 桌面端一次性 / 年费(优先)
- 识别 + Overlay 基础免费或低价
- 高级:实时推荐、会话复盘、分路过滤强度
- 定价锚:¥68128/季 或 ¥168298/年(对标 Dotabuff / 官方 Plus 心理账户)
### C. 国内渠道获客 + 轻广告(大陆适配)
- 小红书 / B 站 / 抖音 / QQ 群 / 贴吧口碑
- 知识站可挂非侵入广告或赞赏
- 安装包自托管(GitHub Release 或国内网盘/OSS),支付用微信/支付宝
- **不要走 Overwolf 商店**
### D. B2B / 内容合作(远期)
- 教练团、主播选人面板、俱乐部内部工具;版本更新内容授权
- 单客价值高、销售周期长;需产品包装与支持能力
### 收入情景(示意,非承诺)
情景已按「Overwolf 不构成大陆竞争」略上调基准;仍是量级框架,非财务预测。乐观情景仍属个人/小团队生意规模。
| 情景 | 付费用户 | ARPU/年 | 年收入粗估 | 前提 |
|------|----------|---------|------------|------|
| 保守 | 300800 | ¥120200 | ¥5–12 万 | 知识站会员为主,桌面口碑未起量 |
| 基准 | 1.5k4k | ¥150250 | ¥25–80 万 | 大陆局内助手空窗被认知 + 稳定更新 |
| 乐观 | 5k12k | ¥200300 | ¥120300 万 | 头部主播带安装或社区爆款 |
---
## 四、风险与门槛
| 类别 | 说明 |
|------|------|
| 政策 / 合规 | 截屏 overlay 通常比读内存安全,但仍可能被社区视为「选将外挂」。需持续公开合规说明,避免自动化点击/注入。 |
| 产品摩擦 | GSI 配置、无边框、标定、宽高比、皮肤帧——安装到「第一局好用」的路径长,付费转化杀手。 |
| 内容运营 | 定性边与版本同步是人力活;停更即失信。胜率自动源便宜但与产品定位冲突,不能偷懒换库。 |
其他约束:Windows 单平台;TAM 仍窄;大陆无 Overwolf 对手;中文攻略站抢知识流量;识别偶发失败影响信任;安装/支付需自建国内链路。
---
## 五、建议路径
### Now
大陆优先:知识站获客 + 明确「免科学上网」卖点。对外文案直接对比「国外助手要翻墙」。强化版本速览与可分享关系页;桌面端先免费验证识别稳定性,再收费。
### Next
一键安装包(国内下载)+ 微信/支付宝年费。降低 GSI/标定摩擦;收费绑「定位局实时 Top-3 + 会话复盘」。分发走自有安装包/OSS,支付走国内通道——不要依赖 Overwolf 或海外订阅基建。
### Later
主播/社群带量与轻 B2B,避免烧钱扩品类。版本专栏、教练工具包、主播选人面板。护城河在定性数据质量与「大陆能用的局内助手」心智,不在功能堆叠或出海抢 Overwolf。
---
## 一句话结论
在中国大陆,上分帝面对的是「局内实时选将助手近乎空白」的窗口,商业土壤比全球视角更乐观;但仍是利基生意,不是融资级平台。用免翻墙的识别 Overlay 做钩子,用定性关系内容做留存与收费,是最匹配当前资产的路径。