@tapdb/tapdb-data-analysis 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/package.json +27 -0
- package/tapdb-data-analysis/SKILL.md +205 -0
- package/tapdb-data-analysis/references/analysis_guide.md +271 -0
- package/tapdb-data-analysis/references/metrics_glossary.md +143 -0
- package/tapdb-data-analysis/references/output_rules.md +185 -0
- package/tapdb-data-analysis/references/preflight_check.md +9 -0
- package/tapdb-data-analysis/scripts/tapdb_query.py +838 -0
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""TapDB 数据查询工具 - 通过 TapDB MCP 服务查询游戏运营数据。
|
|
3
|
+
|
|
4
|
+
支持国内(cn)和海外(sg)两套部署,endpoint 已内置,只需配置认证密钥。
|
|
5
|
+
所有查询走 /mcp/op/* 代理接口,无需 SQL。
|
|
6
|
+
|
|
7
|
+
环境变量:
|
|
8
|
+
TAPDB_MCP_KEY_CN 国内认证密钥
|
|
9
|
+
TAPDB_MCP_KEY_SG 海外认证密钥
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
import urllib.request
|
|
18
|
+
import urllib.error
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
BUILTIN_ENDPOINTS = {
|
|
22
|
+
"cn": "https://www.tapdb.com/api",
|
|
23
|
+
"sg": "https://console.ap-sg.tapdb.developer.taptap.com/api",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
REGION_KEY_VARS = {
|
|
28
|
+
"cn": "TAPDB_MCP_KEY_CN",
|
|
29
|
+
"sg": "TAPDB_MCP_KEY_SG",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_config(region):
|
|
34
|
+
region = (region or "cn").lower()
|
|
35
|
+
base_url = BUILTIN_ENDPOINTS.get(region)
|
|
36
|
+
if not base_url:
|
|
37
|
+
print(f"错误: 不支持的区域 '{region}',可选: cn, sg", file=sys.stderr)
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
env_var = REGION_KEY_VARS[region]
|
|
40
|
+
key = os.environ.get(env_var)
|
|
41
|
+
if not key:
|
|
42
|
+
print(f"错误: 请设置环境变量 {env_var}", file=sys.stderr)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
return key, base_url
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def http_request(method, url, headers, body=None):
|
|
48
|
+
try:
|
|
49
|
+
data = json.dumps(body).encode("utf-8") if body else None
|
|
50
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
51
|
+
if data:
|
|
52
|
+
req.add_header("Content-Type", "application/json")
|
|
53
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
54
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
55
|
+
except urllib.error.HTTPError as e:
|
|
56
|
+
err_body = e.read().decode("utf-8", errors="replace") if e.fp else ""
|
|
57
|
+
return {"error": True, "status": e.code, "message": err_body}
|
|
58
|
+
except urllib.error.URLError as e:
|
|
59
|
+
return {"error": True, "message": f"连接失败: {e.reason}"}
|
|
60
|
+
except Exception as e:
|
|
61
|
+
return {"error": True, "message": str(e)}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def output(data):
|
|
65
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ── 数据截断 ────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
_RE_DR = re.compile(r'^DR(\d+)')
|
|
71
|
+
_RE_N_LTV = re.compile(r'^(\d+)_LTV$')
|
|
72
|
+
_RETENTION_KEEP = {1, 3, 7, 14, 30, 60, 90}
|
|
73
|
+
_LTV_KEEP = {1, 3, 7, 14, 30, 60, 90}
|
|
74
|
+
_TIME_FIELDS = frozenset(("date", "time", "activation_time", "start_time",
|
|
75
|
+
"date_", "time_", "activation_time_", "start_time_"))
|
|
76
|
+
|
|
77
|
+
_MAX_TIME_ROWS = 30
|
|
78
|
+
_MAX_GROUP_ROWS = 20
|
|
79
|
+
_MAX_WHALE_ROWS = 20
|
|
80
|
+
_HEAD = 5
|
|
81
|
+
_TAIL = 5
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _numeric_summary(rows, limit=6):
|
|
85
|
+
"""Compute min/max/avg for first N numeric columns."""
|
|
86
|
+
if not rows:
|
|
87
|
+
return {}
|
|
88
|
+
keys = [k for k, v in rows[0].items() if isinstance(v, (int, float))][:limit]
|
|
89
|
+
out = {}
|
|
90
|
+
for k in keys:
|
|
91
|
+
vals = [r[k] for r in rows if isinstance(r.get(k), (int, float))]
|
|
92
|
+
if vals:
|
|
93
|
+
out[k] = {"min": min(vals), "max": max(vals),
|
|
94
|
+
"avg": round(sum(vals) / len(vals), 2)}
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _slim_columns(rows, cmd_type):
|
|
99
|
+
"""Drop intermediate DR/LTV columns, keep key day columns only."""
|
|
100
|
+
if not rows or not isinstance(rows[0], dict):
|
|
101
|
+
return rows, None
|
|
102
|
+
sample = rows[0]
|
|
103
|
+
if cmd_type == "retention":
|
|
104
|
+
regex, keep_set, label = _RE_DR, _RETENTION_KEEP, "DR"
|
|
105
|
+
elif cmd_type == "user_value":
|
|
106
|
+
regex, keep_set, label = _RE_N_LTV, _LTV_KEEP, "LTV"
|
|
107
|
+
else:
|
|
108
|
+
return rows, None
|
|
109
|
+
|
|
110
|
+
matched = {}
|
|
111
|
+
for k in sample:
|
|
112
|
+
m = regex.match(k)
|
|
113
|
+
if m:
|
|
114
|
+
matched[k] = int(m.group(1))
|
|
115
|
+
kept_count = sum(1 for d in matched.values() if d in keep_set)
|
|
116
|
+
if len(matched) <= kept_count + 2:
|
|
117
|
+
return rows, None
|
|
118
|
+
keep_keys = {k for k in sample if k not in matched or matched.get(k) in keep_set}
|
|
119
|
+
removed = len(matched) - kept_count
|
|
120
|
+
rows = [{k: v for k, v in r.items() if k in keep_keys} for r in rows]
|
|
121
|
+
days = "/".join(str(d) for d in sorted(keep_set))
|
|
122
|
+
return rows, f"kept {label}{days}, removed {removed} other {label} columns"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _slim_rows(rows, cmd_type):
|
|
126
|
+
"""Truncate row count; time-series keeps head+tail, others keep head."""
|
|
127
|
+
if not rows:
|
|
128
|
+
return rows, None
|
|
129
|
+
if not isinstance(rows[0], dict):
|
|
130
|
+
cap = _MAX_WHALE_ROWS if cmd_type == "whale_user" else _MAX_GROUP_ROWS
|
|
131
|
+
if len(rows) <= cap:
|
|
132
|
+
return rows, None
|
|
133
|
+
total = len(rows)
|
|
134
|
+
omit = total - cap
|
|
135
|
+
rows = rows[:cap] + [f"... 省略 {omit} 条 ..."]
|
|
136
|
+
return rows, {"total_rows": total, "omitted": omit}
|
|
137
|
+
|
|
138
|
+
has_time = bool(_TIME_FIELDS & set(rows[0].keys()))
|
|
139
|
+
if cmd_type == "whale_user":
|
|
140
|
+
cap = _MAX_WHALE_ROWS
|
|
141
|
+
elif has_time:
|
|
142
|
+
cap = _MAX_TIME_ROWS
|
|
143
|
+
else:
|
|
144
|
+
cap = _MAX_GROUP_ROWS
|
|
145
|
+
if len(rows) <= cap:
|
|
146
|
+
return rows, None
|
|
147
|
+
|
|
148
|
+
total = len(rows)
|
|
149
|
+
summary = _numeric_summary(rows)
|
|
150
|
+
if has_time:
|
|
151
|
+
omit = total - _HEAD - _TAIL
|
|
152
|
+
rows = rows[:_HEAD] + [{"_": f"... 省略 {omit} 行 ..."}] + rows[-_TAIL:]
|
|
153
|
+
else:
|
|
154
|
+
omit = total - cap
|
|
155
|
+
rows = rows[:cap] + [{"_": f"... 省略 {omit} 行 ..."}]
|
|
156
|
+
return rows, {"total_rows": total, "omitted": omit, "summary": summary}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _list_of_lists_to_dicts(lol):
|
|
160
|
+
"""Convert [[header...], [row...], ...] to [{header: val, ...}, ...]."""
|
|
161
|
+
headers = [str(h) for h in lol[0]]
|
|
162
|
+
return [dict(zip(headers, row)) for row in lol[1:]]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _locate_data(obj):
|
|
166
|
+
"""Find main data list in API response. Returns (list, path_str) or (None, None)."""
|
|
167
|
+
if isinstance(obj, list):
|
|
168
|
+
if obj and isinstance(obj[0], list):
|
|
169
|
+
return _list_of_lists_to_dicts(obj), "root"
|
|
170
|
+
return obj, "root"
|
|
171
|
+
if not isinstance(obj, dict):
|
|
172
|
+
return None, None
|
|
173
|
+
data = obj.get("data")
|
|
174
|
+
if isinstance(data, list) and data:
|
|
175
|
+
if isinstance(data[0], list):
|
|
176
|
+
return _list_of_lists_to_dicts(data), "data"
|
|
177
|
+
if isinstance(data[0], dict):
|
|
178
|
+
return data, "data"
|
|
179
|
+
if isinstance(data, dict):
|
|
180
|
+
for key in ("items", "list", "rows", "records"):
|
|
181
|
+
sub = data.get(key)
|
|
182
|
+
if isinstance(sub, list) and sub:
|
|
183
|
+
if isinstance(sub[0], list):
|
|
184
|
+
return _list_of_lists_to_dicts(sub), f"data.{key}"
|
|
185
|
+
if isinstance(sub[0], dict):
|
|
186
|
+
return sub, f"data.{key}"
|
|
187
|
+
return None, None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _rebuild(resp, path, rows, info):
|
|
191
|
+
"""Reconstruct response with truncated rows and info."""
|
|
192
|
+
if path == "root":
|
|
193
|
+
return {"data": rows, "_truncation": info}
|
|
194
|
+
result = dict(resp)
|
|
195
|
+
result["_truncation"] = info
|
|
196
|
+
if path == "data":
|
|
197
|
+
result["data"] = rows
|
|
198
|
+
elif path.startswith("data."):
|
|
199
|
+
subkey = path[5:]
|
|
200
|
+
result["data"] = dict(resp["data"])
|
|
201
|
+
result["data"][subkey] = rows
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def truncate_response(resp, cmd_type=None):
|
|
206
|
+
"""Truncate API response to save context window tokens."""
|
|
207
|
+
if not resp or (isinstance(resp, dict) and resp.get("error")):
|
|
208
|
+
return resp
|
|
209
|
+
rows, path = _locate_data(resp)
|
|
210
|
+
if not rows:
|
|
211
|
+
return resp
|
|
212
|
+
|
|
213
|
+
info = {}
|
|
214
|
+
rows, col_msg = _slim_columns(rows, cmd_type)
|
|
215
|
+
if col_msg:
|
|
216
|
+
info["columns"] = col_msg
|
|
217
|
+
rows, row_info = _slim_rows(rows, cmd_type)
|
|
218
|
+
if row_info:
|
|
219
|
+
info.update(row_info)
|
|
220
|
+
if not info:
|
|
221
|
+
return resp
|
|
222
|
+
return _rebuild(resp, path, rows, info)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ── 通用请求体构造 ──────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
COL_ALIAS_MAP = {
|
|
228
|
+
"time": "date",
|
|
229
|
+
"activation_channel": "ch",
|
|
230
|
+
"first_ad_conversion_link_id": "id",
|
|
231
|
+
"utmsrc": "utmsrc",
|
|
232
|
+
"activation_os": "os",
|
|
233
|
+
"activation_os_version": "syver",
|
|
234
|
+
"activation_device_model": "dev",
|
|
235
|
+
"activation_resolution": "res",
|
|
236
|
+
"activation_network": "net",
|
|
237
|
+
"activation_provider": "pvd",
|
|
238
|
+
"activation_province": "rgn",
|
|
239
|
+
"activation_country": "cy",
|
|
240
|
+
"login_type": "login_type",
|
|
241
|
+
"lang_system": "lang_system",
|
|
242
|
+
"payment_source": "payment_source",
|
|
243
|
+
"activation_time": "activation_time",
|
|
244
|
+
"activation_app_version": "activation_app_version",
|
|
245
|
+
"first_server": "first_server",
|
|
246
|
+
"current_server": "current_server",
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
COUNTRY_GROUP_DIMS = {"activation_country", "activation_province"}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ── 各接口能力描述(基于源码 + 实测验证) ────────────────────
|
|
253
|
+
#
|
|
254
|
+
# 分组/过滤中的"翻译字段"(trans_dims): activation_channel, activation_device_model,
|
|
255
|
+
# activation_network, activation_provider, activation_province, activation_country,
|
|
256
|
+
# first_ad_conversion_link_id, lang_system
|
|
257
|
+
# 这些字段分组时显示的是翻译后的展示名(来自 tapdb_group_dim 表),
|
|
258
|
+
# 但作为过滤条件时需使用视图中的原始值。
|
|
259
|
+
# 其中 lang_system 的过滤值会自动翻译(MergeGroupDimTranConstant), 可直接用展示名。
|
|
260
|
+
|
|
261
|
+
ENDPOINT_CAPS = {
|
|
262
|
+
"active": {
|
|
263
|
+
"description": "活跃数据: DAU/WAU/MAU/HAU",
|
|
264
|
+
"quotas": ["dau", "wau", "mau", "hau"],
|
|
265
|
+
"subjects": ["device", "user"],
|
|
266
|
+
"returned_metrics": {
|
|
267
|
+
"dau": ["dau", "dau_new", "dau_2", "dau_3", "dau_7", "dau_14", "dau_30",
|
|
268
|
+
"dau_new_rate", "dau_2_rate", "dau_3_rate", "dau_7_rate", "dau_14_rate", "dau_30_rate"],
|
|
269
|
+
"wau": ["wau", "wau_new", "wau_2", "wau_3", "wau_4", "wau_5"],
|
|
270
|
+
"mau": ["mau"],
|
|
271
|
+
"hau": ["hau", "hau_new", "hau_2", "hau_3", "hau_7", "hau_14", "hau_30"],
|
|
272
|
+
},
|
|
273
|
+
"groups": [
|
|
274
|
+
"time", "activation_channel", "activation_os", "activation_os_version",
|
|
275
|
+
"activation_device_model", "activation_resolution", "activation_network",
|
|
276
|
+
"activation_provider", "activation_province", "activation_country",
|
|
277
|
+
"first_ad_conversion_link_id", "lang_system",
|
|
278
|
+
],
|
|
279
|
+
"filters": [
|
|
280
|
+
"activation_os", "activation_channel", "activation_os_version",
|
|
281
|
+
"activation_device_model", "activation_resolution", "activation_network",
|
|
282
|
+
"activation_provider", "activation_province", "activation_country",
|
|
283
|
+
"first_ad_conversion_link_id", "lang_system",
|
|
284
|
+
],
|
|
285
|
+
"filter_notes": {
|
|
286
|
+
"activation_device_model": "翻译字段,过滤值需使用原始值(可先 group-by 此字段查看)",
|
|
287
|
+
"activation_province": "翻译字段,过滤值需使用原始值",
|
|
288
|
+
"activation_country": "翻译字段,值如'中国'可直接使用;港澳台会自动映射",
|
|
289
|
+
"first_ad_conversion_link_id": "翻译字段,过滤值需使用原始值(非展示名'自然用户')",
|
|
290
|
+
"lang_system": "过滤值会自动翻译,可直接用展示名(如'中文')",
|
|
291
|
+
},
|
|
292
|
+
"group_notes": {
|
|
293
|
+
"activation_province": "需传 --language",
|
|
294
|
+
"activation_country": "需传 --language 和 --group-dim (cy 或 scon)",
|
|
295
|
+
},
|
|
296
|
+
"unsupported_note": "不支持 activation_time/utmsrc/login_type/payment_source/activation_app_version/first_server/current_server",
|
|
297
|
+
},
|
|
298
|
+
"retention": {
|
|
299
|
+
"description": "留存数据: DR1-DR180 / WR / MR",
|
|
300
|
+
"time_field": "activation_time",
|
|
301
|
+
"subjects": ["device", "user"],
|
|
302
|
+
"extra_params": ["interval_unit (day|week|month)", "all_retention (bool)"],
|
|
303
|
+
"groups": [
|
|
304
|
+
"activation_time", "activation_channel", "activation_os", "activation_os_version",
|
|
305
|
+
"activation_device_model", "activation_resolution", "activation_network",
|
|
306
|
+
"activation_provider", "activation_province", "activation_country",
|
|
307
|
+
"first_ad_conversion_link_id", "lang_system", "activation_app_version",
|
|
308
|
+
],
|
|
309
|
+
"filters": [
|
|
310
|
+
"activation_os", "activation_channel", "activation_os_version",
|
|
311
|
+
"activation_device_model", "activation_resolution", "activation_network",
|
|
312
|
+
"activation_provider", "activation_province", "activation_country",
|
|
313
|
+
"first_ad_conversion_link_id", "lang_system", "activation_app_version",
|
|
314
|
+
],
|
|
315
|
+
"filter_notes": {
|
|
316
|
+
"activation_device_model": "翻译字段,过滤值需使用原始值",
|
|
317
|
+
"activation_province": "翻译字段,过滤值需使用原始值",
|
|
318
|
+
"activation_country": "翻译字段",
|
|
319
|
+
"first_ad_conversion_link_id": "翻译字段,过滤值需使用原始值",
|
|
320
|
+
"lang_system": "过滤值会自动翻译,可直接用展示名",
|
|
321
|
+
},
|
|
322
|
+
"group_notes": {
|
|
323
|
+
"activation_province": "需传 --language",
|
|
324
|
+
"activation_country": "需传 --language 和 --group-dim (cy 或 scon)",
|
|
325
|
+
},
|
|
326
|
+
"unsupported_note": "不支持 time/utmsrc/login_type/payment_source/first_server/current_server(retention_view 无此列)",
|
|
327
|
+
},
|
|
328
|
+
"income": {
|
|
329
|
+
"description": "收入/付费数据: 收入/付费人数/ARPU/ARPPU",
|
|
330
|
+
"returned_metrics": [
|
|
331
|
+
"incomes", "pay_times", "refunds", "refund_times",
|
|
332
|
+
"payers_num", "refunders_num", "active_users", "vp_incomes", "payment_rate",
|
|
333
|
+
],
|
|
334
|
+
"extra_params": ["charge_subject (user|device)"],
|
|
335
|
+
"groups": [
|
|
336
|
+
"time", "activation_channel", "activation_os", "activation_os_version",
|
|
337
|
+
"activation_device_model", "activation_resolution", "activation_network",
|
|
338
|
+
"activation_provider", "activation_province", "activation_country",
|
|
339
|
+
"first_ad_conversion_link_id", "lang_system",
|
|
340
|
+
"payment_source", "activation_app_version", "first_server", "current_server",
|
|
341
|
+
],
|
|
342
|
+
"filters": [
|
|
343
|
+
"activation_os", "activation_channel", "activation_os_version",
|
|
344
|
+
"activation_device_model", "activation_resolution", "activation_network",
|
|
345
|
+
"activation_provider", "activation_province", "activation_country",
|
|
346
|
+
"first_ad_conversion_link_id", "lang_system",
|
|
347
|
+
"payment_source", "activation_app_version", "first_server", "current_server",
|
|
348
|
+
],
|
|
349
|
+
"filter_notes": {
|
|
350
|
+
"activation_device_model": "翻译字段,过滤值需使用原始值",
|
|
351
|
+
"activation_province": "翻译字段,过滤值需使用原始值",
|
|
352
|
+
"activation_country": "翻译字段",
|
|
353
|
+
"first_ad_conversion_link_id": "翻译字段,过滤值需使用原始值",
|
|
354
|
+
"lang_system": "过滤值会自动翻译,可直接用展示名",
|
|
355
|
+
},
|
|
356
|
+
"group_notes": {
|
|
357
|
+
"activation_province": "需传 --language",
|
|
358
|
+
"activation_country": "需传 --language 和 --group-dim (cy 或 scon)",
|
|
359
|
+
},
|
|
360
|
+
"unsupported_note": "不支持 activation_time/utmsrc/login_type",
|
|
361
|
+
},
|
|
362
|
+
"source": {
|
|
363
|
+
"description": "来源数据: 新增设备/用户/转化率/首充/留存",
|
|
364
|
+
"time_field": "activation_time",
|
|
365
|
+
"returned_metrics": [
|
|
366
|
+
"newDevice", "convertedDevice", "newUser",
|
|
367
|
+
"newChargeUser", "newTotalChargeAmount",
|
|
368
|
+
"firstChargeuser", "firstChargeAmount",
|
|
369
|
+
"DR1", "DR7", "DR1_newDevice", "DR7_newDevice",
|
|
370
|
+
"converted_rate", "DR1_rate", "DR7_rate",
|
|
371
|
+
"new_charge_user_rate", "first_charge_user_rate",
|
|
372
|
+
],
|
|
373
|
+
"groups": [
|
|
374
|
+
"activation_time", "activation_channel", "activation_os",
|
|
375
|
+
"activation_os_version", "activation_device_model", "activation_resolution",
|
|
376
|
+
"activation_network", "activation_provider", "activation_province",
|
|
377
|
+
"activation_country", "first_ad_conversion_link_id", "lang_system",
|
|
378
|
+
"activation_app_version", "first_server",
|
|
379
|
+
],
|
|
380
|
+
"filters": [
|
|
381
|
+
"activation_os", "activation_channel", "activation_os_version",
|
|
382
|
+
"activation_device_model", "activation_resolution", "activation_network",
|
|
383
|
+
"activation_provider", "activation_province", "activation_country",
|
|
384
|
+
"first_ad_conversion_link_id", "lang_system", "activation_app_version",
|
|
385
|
+
"first_server",
|
|
386
|
+
],
|
|
387
|
+
"filter_notes": {
|
|
388
|
+
"activation_device_model": "翻译字段,过滤值需使用原始值(可先 group-by 此字段查看)",
|
|
389
|
+
"activation_province": "翻译字段,过滤值需使用原始值",
|
|
390
|
+
"activation_country": "翻译字段,值如'中国'可直接使用",
|
|
391
|
+
"first_ad_conversion_link_id": "翻译字段,过滤值需使用原始值(非展示名'自然用户')",
|
|
392
|
+
"lang_system": "过滤值会自动翻译,可直接用展示名(如'中文')",
|
|
393
|
+
},
|
|
394
|
+
"group_notes": {
|
|
395
|
+
"activation_province": "需传 --language",
|
|
396
|
+
"activation_country": "需传 --language 和 --group-dim (cy 或 scon)",
|
|
397
|
+
},
|
|
398
|
+
"unsupported_note": "不支持 time/utmsrc/login_type/payment_source/current_server 字段(source_view 无此列)",
|
|
399
|
+
},
|
|
400
|
+
"player_behavior": {
|
|
401
|
+
"description": "玩家行为: 游戏时长/启动次数",
|
|
402
|
+
"quotas": ["behavior", "duration"],
|
|
403
|
+
"returned_metrics": {
|
|
404
|
+
"behavior": ["total_duration", "total_times", "total_users"],
|
|
405
|
+
"duration": ["duration", "player_num", "play_times"],
|
|
406
|
+
},
|
|
407
|
+
"groups": [
|
|
408
|
+
"time", "activation_channel", "activation_os", "activation_os_version",
|
|
409
|
+
"activation_device_model", "activation_resolution", "activation_network",
|
|
410
|
+
"activation_provider", "activation_province", "activation_country",
|
|
411
|
+
"first_ad_conversion_link_id", "lang_system", "first_server",
|
|
412
|
+
],
|
|
413
|
+
"filters": [
|
|
414
|
+
"activation_os", "activation_channel", "activation_os_version",
|
|
415
|
+
"activation_device_model", "activation_resolution", "activation_network",
|
|
416
|
+
"activation_provider", "activation_province", "activation_country",
|
|
417
|
+
"first_ad_conversion_link_id", "lang_system", "first_server",
|
|
418
|
+
],
|
|
419
|
+
"filter_notes": {
|
|
420
|
+
"activation_device_model": "翻译字段,过滤值需使用原始值",
|
|
421
|
+
"activation_province": "翻译字段,过滤值需使用原始值",
|
|
422
|
+
"activation_country": "翻译字段",
|
|
423
|
+
"first_ad_conversion_link_id": "翻译字段,过滤值需使用原始值",
|
|
424
|
+
"lang_system": "过滤值会自动翻译,可直接用展示名",
|
|
425
|
+
},
|
|
426
|
+
"group_notes": {
|
|
427
|
+
"activation_province": "需传 --language",
|
|
428
|
+
"activation_country": "需传 --language 和 --group-dim (cy 或 scon)",
|
|
429
|
+
},
|
|
430
|
+
"unsupported_note": "不支持 activation_time/utmsrc/login_type/payment_source/activation_app_version/current_server",
|
|
431
|
+
},
|
|
432
|
+
"version_distri": {
|
|
433
|
+
"description": "版本分布: 各版本活跃/新增设备数(专用接口,忽略 group 参数,固定按版本分组)",
|
|
434
|
+
"returned_metrics": ["version", "allDevices", "newDevices", "upgradeDevices", "activeDevices", "NUDevices"],
|
|
435
|
+
},
|
|
436
|
+
"overview": {
|
|
437
|
+
"description": "运营概览: 收入/活跃/新增汇总(需 app_id,独立接口不走通用分组)",
|
|
438
|
+
"quotas": ["income", "active", "activation"],
|
|
439
|
+
"extra_params": ["app_id (必需)", "interval (minute|hour|day|week|month)"],
|
|
440
|
+
},
|
|
441
|
+
"user_value": {
|
|
442
|
+
"description": "用户价值(LTV): N日贡献",
|
|
443
|
+
"returned_metrics": ["activation", "N_LTV (1-60,90,120,150,180,210,240,270,300,330,360)"],
|
|
444
|
+
"groups": [
|
|
445
|
+
"time", "activation_time", "activation_channel", "activation_os",
|
|
446
|
+
"activation_os_version", "activation_device_model", "activation_resolution",
|
|
447
|
+
"activation_network", "activation_provider", "activation_province",
|
|
448
|
+
"activation_country", "first_ad_conversion_link_id", "lang_system",
|
|
449
|
+
"activation_app_version",
|
|
450
|
+
],
|
|
451
|
+
"filters": [
|
|
452
|
+
"activation_os", "activation_channel", "activation_os_version",
|
|
453
|
+
"activation_device_model", "activation_resolution", "activation_network",
|
|
454
|
+
"activation_provider", "activation_province", "activation_country",
|
|
455
|
+
"first_ad_conversion_link_id", "lang_system", "activation_app_version",
|
|
456
|
+
"first_server",
|
|
457
|
+
],
|
|
458
|
+
"filter_notes": {
|
|
459
|
+
"activation_device_model": "翻译字段,过滤值需使用原始值",
|
|
460
|
+
"activation_province": "翻译字段,过滤值需使用原始值",
|
|
461
|
+
"activation_country": "翻译字段",
|
|
462
|
+
"first_ad_conversion_link_id": "翻译字段,过滤值需使用原始值",
|
|
463
|
+
"lang_system": "过滤值会自动翻译,可直接用展示名",
|
|
464
|
+
"first_server": "仅支持过滤,不支持分组",
|
|
465
|
+
},
|
|
466
|
+
"group_notes": {
|
|
467
|
+
"activation_province": "需传 --language",
|
|
468
|
+
"activation_country": "需传 --language 和 --group-dim (cy 或 scon)",
|
|
469
|
+
},
|
|
470
|
+
"unsupported_note": "不支持 utmsrc/login_type/payment_source/current_server;first_server 仅可用于过滤",
|
|
471
|
+
},
|
|
472
|
+
"whale_user": {
|
|
473
|
+
"description": "鲸鱼用户: 高付费用户排行(特殊接口,无分组/过滤,直接返回用户列表)",
|
|
474
|
+
"returned_metrics": [
|
|
475
|
+
"user_id", "user_name", "total_charge_amount", "server",
|
|
476
|
+
"LEVEL", "first_charge_time", "last_charge_time", "last_login_time",
|
|
477
|
+
],
|
|
478
|
+
},
|
|
479
|
+
"life_cycle": {
|
|
480
|
+
"description": "用户生命周期: 付费转化/金额/累计",
|
|
481
|
+
"quotas": ["payment_cvs_rate", "payment_cvs", "payment_amount", "acc_payment"],
|
|
482
|
+
"returned_metrics": {
|
|
483
|
+
"payment_amount": ["newUsers", "PA0", "PA1", "PA2", "PA3", "PA4", "PA5", "PA6"],
|
|
484
|
+
},
|
|
485
|
+
"groups": [
|
|
486
|
+
"time", "activation_time", "activation_os",
|
|
487
|
+
],
|
|
488
|
+
"filters": [
|
|
489
|
+
"activation_os", "activation_channel", "activation_os_version",
|
|
490
|
+
"activation_device_model", "activation_resolution", "activation_network",
|
|
491
|
+
"activation_provider", "activation_province", "activation_country",
|
|
492
|
+
"first_ad_conversion_link_id", "lang_system",
|
|
493
|
+
],
|
|
494
|
+
"filter_notes": {
|
|
495
|
+
"activation_device_model": "翻译字段,过滤值需使用原始值",
|
|
496
|
+
"activation_province": "翻译字段,过滤值需使用原始值",
|
|
497
|
+
"activation_country": "翻译字段",
|
|
498
|
+
"first_ad_conversion_link_id": "翻译字段,过滤值需使用原始值",
|
|
499
|
+
"lang_system": "过滤值会自动翻译,可直接用展示名",
|
|
500
|
+
},
|
|
501
|
+
"group_notes": {
|
|
502
|
+
"activation_os": "分组仅支持 time/activation_time/activation_os 三个维度",
|
|
503
|
+
},
|
|
504
|
+
"unsupported_note": "分组仅支持 time/activation_time/activation_os;过滤不支持 activation_app_version/first_server/current_server/utmsrc/login_type/payment_source",
|
|
505
|
+
},
|
|
506
|
+
"ad_monet": {
|
|
507
|
+
"description": "广告变现数据(MCP 代理路径 /mcp/op/ad_monet 返回 404,可能未开通或路径不同)",
|
|
508
|
+
},
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def cmd_describe(args):
|
|
513
|
+
"""输出指定接口(或全部接口)的能力描述。"""
|
|
514
|
+
target = args.target
|
|
515
|
+
|
|
516
|
+
if target and target not in ENDPOINT_CAPS:
|
|
517
|
+
print(json.dumps({"error": f"未知接口 '{target}',可选: {list(ENDPOINT_CAPS.keys())}"},
|
|
518
|
+
ensure_ascii=False))
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
caps = {target: ENDPOINT_CAPS[target]} if target else ENDPOINT_CAPS
|
|
522
|
+
|
|
523
|
+
result = {}
|
|
524
|
+
for name, cap in caps.items():
|
|
525
|
+
info = {"description": cap["description"]}
|
|
526
|
+
if "quotas" in cap:
|
|
527
|
+
info["quotas"] = cap["quotas"]
|
|
528
|
+
if "subjects" in cap:
|
|
529
|
+
info["subjects"] = cap["subjects"]
|
|
530
|
+
if "groups" in cap:
|
|
531
|
+
info["supported_groups"] = [
|
|
532
|
+
{"col_name": g, "col_alias": COL_ALIAS_MAP.get(g, g),
|
|
533
|
+
**({"note": cap.get("group_notes", {}).get(g)} if g in cap.get("group_notes", {}) else {})}
|
|
534
|
+
for g in cap["groups"]
|
|
535
|
+
]
|
|
536
|
+
if "filters" in cap:
|
|
537
|
+
info["supported_filters"] = [
|
|
538
|
+
{"col_name": f,
|
|
539
|
+
**({"note": cap.get("filter_notes", {}).get(f)} if f in cap.get("filter_notes", {}) else {})}
|
|
540
|
+
for f in cap["filters"]
|
|
541
|
+
]
|
|
542
|
+
if "extra_params" in cap:
|
|
543
|
+
info["extra_params"] = cap["extra_params"]
|
|
544
|
+
if "time_field" in cap:
|
|
545
|
+
info["time_field"] = cap["time_field"]
|
|
546
|
+
if "returned_metrics" in cap:
|
|
547
|
+
info["returned_metrics"] = cap["returned_metrics"]
|
|
548
|
+
if "unsupported_note" in cap:
|
|
549
|
+
info["unsupported_note"] = cap["unsupported_note"]
|
|
550
|
+
result[name] = info
|
|
551
|
+
|
|
552
|
+
output(result)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def build_group(group_by, group_unit):
|
|
556
|
+
if not group_by:
|
|
557
|
+
return None
|
|
558
|
+
is_time = group_by in ("time", "activation_time")
|
|
559
|
+
return {
|
|
560
|
+
"col_name": group_by,
|
|
561
|
+
"col_alias": COL_ALIAS_MAP.get(group_by, group_by),
|
|
562
|
+
"is_time": is_time,
|
|
563
|
+
"trunc_unit": group_unit or "day",
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def build_base_body(args):
|
|
568
|
+
body = {"project_id": int(args.project_id)}
|
|
569
|
+
if hasattr(args, "start") and args.start:
|
|
570
|
+
body["start_time"] = f"{args.start} 00:00:00.000"
|
|
571
|
+
if hasattr(args, "end") and args.end:
|
|
572
|
+
body["end_time"] = f"{args.end} 23:59:59.999"
|
|
573
|
+
group_by = getattr(args, "group_by", None) or "time"
|
|
574
|
+
group_unit = getattr(args, "group_unit", None)
|
|
575
|
+
body["group"] = build_group(group_by, group_unit)
|
|
576
|
+
if group_by in COUNTRY_GROUP_DIMS:
|
|
577
|
+
body["language"] = getattr(args, "language", None) or "cn"
|
|
578
|
+
group_dim = getattr(args, "group_dim", None)
|
|
579
|
+
if group_dim:
|
|
580
|
+
body["group_dim"] = group_dim
|
|
581
|
+
elif group_by == "activation_country":
|
|
582
|
+
body["group_dim"] = "cy"
|
|
583
|
+
body["is_de_water"] = getattr(args, "de_water", False)
|
|
584
|
+
if getattr(args, "filters", None):
|
|
585
|
+
body["filters"] = json.loads(args.filters)
|
|
586
|
+
else:
|
|
587
|
+
body["filters"] = []
|
|
588
|
+
if getattr(args, "charge_subject", None):
|
|
589
|
+
body["charge_subject"] = args.charge_subject
|
|
590
|
+
exchange_to = getattr(args, "exchange_to_currency", None)
|
|
591
|
+
if exchange_to and exchange_to.lower() != "none":
|
|
592
|
+
body["real_time_currency"] = True
|
|
593
|
+
body["exchange_to_currency"] = exchange_to.upper()
|
|
594
|
+
body["use_cache"] = not getattr(args, "no_cache", False)
|
|
595
|
+
if getattr(args, "limit", None):
|
|
596
|
+
body["limit_num"] = args.limit
|
|
597
|
+
return body
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def do_query(args, endpoint_path, extra=None, cmd_type=None):
|
|
601
|
+
key, base_url = get_config(args.region)
|
|
602
|
+
body = build_base_body(args)
|
|
603
|
+
if extra:
|
|
604
|
+
body.update(extra)
|
|
605
|
+
url = f"{base_url}/mcp/op/{endpoint_path}"
|
|
606
|
+
result = http_request("POST", url, {"MCP-KEY": key}, body)
|
|
607
|
+
if not getattr(args, "no_truncate", False):
|
|
608
|
+
result = truncate_response(result, cmd_type or endpoint_path)
|
|
609
|
+
output(result)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
# ── 子命令 ──────────────────────────────────────────────────
|
|
613
|
+
|
|
614
|
+
def cmd_list_projects(args):
|
|
615
|
+
key, endpoint = get_config(args.region)
|
|
616
|
+
result = http_request("GET", f"{endpoint}/mcp/list_projects", {"MCP-KEY": key})
|
|
617
|
+
output(result)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def cmd_active(args):
|
|
621
|
+
do_query(args, "active", {
|
|
622
|
+
"subject": args.subject,
|
|
623
|
+
"quota": args.quota,
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def cmd_retention(args):
|
|
628
|
+
if not args.group_by or args.group_by == "time":
|
|
629
|
+
args.group_by = "activation_time"
|
|
630
|
+
extra = {
|
|
631
|
+
"subject": args.subject,
|
|
632
|
+
"interval_unit": args.interval_unit,
|
|
633
|
+
"percent": args.percent,
|
|
634
|
+
}
|
|
635
|
+
if args.all_retention:
|
|
636
|
+
extra["extend_day"] = True
|
|
637
|
+
do_query(args, "retention", extra)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def cmd_income(args):
|
|
641
|
+
do_query(args, "income_data", cmd_type="income")
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def cmd_source(args):
|
|
645
|
+
if not args.group_by or args.group_by == "time":
|
|
646
|
+
args.group_by = "activation_time"
|
|
647
|
+
do_query(args, "source")
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def cmd_player_behavior(args):
|
|
651
|
+
do_query(args, "player_behavior", {
|
|
652
|
+
"quota": args.quota,
|
|
653
|
+
"duration_unit": args.duration_unit,
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def cmd_version_distri(args):
|
|
658
|
+
do_query(args, "version_distri")
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def cmd_overview(args):
|
|
662
|
+
key, base_url = get_config(args.region)
|
|
663
|
+
body = {
|
|
664
|
+
"project_id": int(args.project_id),
|
|
665
|
+
"app_id": args.app_id,
|
|
666
|
+
"start_date": args.start,
|
|
667
|
+
"end_date": args.end,
|
|
668
|
+
"interval": args.interval,
|
|
669
|
+
"quota": args.quota,
|
|
670
|
+
"use_cache": not getattr(args, "no_cache", False),
|
|
671
|
+
}
|
|
672
|
+
result = http_request("POST", f"{base_url}/mcp/op/op_overview", {"MCP-KEY": key}, body)
|
|
673
|
+
if not getattr(args, "no_truncate", False):
|
|
674
|
+
result = truncate_response(result, "overview")
|
|
675
|
+
output(result)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def cmd_user_value(args):
|
|
679
|
+
do_query(args, "user_value")
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def cmd_whale_user(args):
|
|
683
|
+
do_query(args, "whale_user")
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def cmd_life_cycle(args):
|
|
687
|
+
do_query(args, "life_cycle", {"quota": args.quota})
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def cmd_ad_monet(args):
|
|
691
|
+
do_query(args, "ad_monet")
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def cmd_raw(args):
|
|
695
|
+
key, base_url = get_config(args.region)
|
|
696
|
+
body = json.loads(args.body) if args.body else None
|
|
697
|
+
method = "POST" if body else "GET"
|
|
698
|
+
url = f"{base_url}/mcp{args.path}"
|
|
699
|
+
result = http_request(method, url, {"MCP-KEY": key}, body)
|
|
700
|
+
output(result)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
# ── CLI 定义 ────────────────────────────────────────────────
|
|
704
|
+
|
|
705
|
+
def add_common_args(p):
|
|
706
|
+
p.add_argument("-p", "--project-id", required=True, help="项目ID")
|
|
707
|
+
p.add_argument("-s", "--start", required=True, help="开始日期 YYYY-MM-DD")
|
|
708
|
+
p.add_argument("-e", "--end", required=True, help="结束日期 YYYY-MM-DD")
|
|
709
|
+
p.add_argument("-g", "--group-by", help="分组字段: time, activation_channel, activation_country, activation_os, activation_os_version, activation_device_model, activation_resolution, activation_network, activation_provider, activation_province, login_type, lang_system, payment_source, first_ad_conversion_link_id, utmsrc, activation_time, activation_app_version, first_server, current_server")
|
|
710
|
+
p.add_argument("--group-unit", default="day", help="时间分组粒度: hour|day|week|month (默认 day)")
|
|
711
|
+
p.add_argument("--group-dim", help="分组维度名(国家/地区用cy, 次大陆用scon)")
|
|
712
|
+
p.add_argument("--language", default="cn", choices=["cn", "en", "tw", "jp"],
|
|
713
|
+
help="语言(分组为国家/地区/中国大陆时必填, 默认 cn)")
|
|
714
|
+
p.add_argument("--filters", help='过滤条件JSON, 例: \'[{"col_name":"activation_os","data_type":"string","calculate_symbol":"include","ftv":["Android"]}]\'')
|
|
715
|
+
p.add_argument("--charge-subject", default="user", help="付费主体: user|device (默认 user)")
|
|
716
|
+
p.add_argument("--exchange-to-currency", default="CNY",
|
|
717
|
+
help="金额转换目标货币代码 (默认 CNY; 常用: CNY/USD/JPY/EUR; 传 none 禁用转换)")
|
|
718
|
+
p.add_argument("--de-water", action="store_true", help="去水(默认不去水)")
|
|
719
|
+
p.add_argument("--no-cache", action="store_true", help="不使用缓存")
|
|
720
|
+
p.add_argument("--limit", type=int, help="结果数量上限 (默认 5000)")
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def main():
|
|
724
|
+
parser = argparse.ArgumentParser(
|
|
725
|
+
description="TapDB 数据查询工具 - 查询游戏运营数据(活跃/留存/付费/来源等)",
|
|
726
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
727
|
+
)
|
|
728
|
+
parser.add_argument("-r", "--region", default="cn", choices=["cn", "sg"],
|
|
729
|
+
help="部署区域: cn(国内) / sg(海外) (默认 cn)")
|
|
730
|
+
parser.add_argument("--no-truncate", action="store_true",
|
|
731
|
+
help="输出完整数据,不截断(默认自动截断长数据以节省上下文)")
|
|
732
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
733
|
+
|
|
734
|
+
sub.add_parser("list_projects", help="列出当前可访问的项目")
|
|
735
|
+
|
|
736
|
+
# active
|
|
737
|
+
p = sub.add_parser("active", help="活跃数据: DAU/WAU/MAU/HAU")
|
|
738
|
+
add_common_args(p)
|
|
739
|
+
p.add_argument("--subject", default="device", choices=["device", "user"],
|
|
740
|
+
help="统计维度 (默认 device; 用户问'用户''人数'时用 user)")
|
|
741
|
+
p.add_argument("--quota", default="dau", choices=["dau", "wau", "mau", "hau"],
|
|
742
|
+
help="活跃指标 (默认 dau)")
|
|
743
|
+
|
|
744
|
+
# retention
|
|
745
|
+
p = sub.add_parser("retention", help="留存数据: DR1-DR180 / WR / MR")
|
|
746
|
+
add_common_args(p)
|
|
747
|
+
p.add_argument("--subject", default="device", choices=["device", "user"],
|
|
748
|
+
help="统计维度 (默认 device)")
|
|
749
|
+
p.add_argument("--interval-unit", default="day", choices=["day", "week", "month"],
|
|
750
|
+
help="留存间隔: day|week|month (默认 day)")
|
|
751
|
+
p.add_argument("--percent", default=True, type=lambda x: x.lower() != "false",
|
|
752
|
+
help="返回百分比数据 (默认 true)")
|
|
753
|
+
p.add_argument("--all-retention", action="store_true",
|
|
754
|
+
help="返回所有留存指标(DR1-DR180); 默认只返回关键指标")
|
|
755
|
+
|
|
756
|
+
# income
|
|
757
|
+
p = sub.add_parser("income", help="收入数据: 收入/付费人数/ARPU/ARPPU")
|
|
758
|
+
add_common_args(p)
|
|
759
|
+
|
|
760
|
+
# source
|
|
761
|
+
p = sub.add_parser("source", help="来源数据: 新增设备/用户/转化率")
|
|
762
|
+
add_common_args(p)
|
|
763
|
+
|
|
764
|
+
# player_behavior
|
|
765
|
+
p = sub.add_parser("player_behavior", help="玩家行为: 游戏时长/启动次数")
|
|
766
|
+
add_common_args(p)
|
|
767
|
+
p.add_argument("--quota", default="behavior", choices=["behavior", "duration"],
|
|
768
|
+
help="指标类型 (默认 behavior)")
|
|
769
|
+
p.add_argument("--duration-unit", default="minute", choices=["minute", "10_minute", "hour"],
|
|
770
|
+
help="时长单位 (默认 minute)")
|
|
771
|
+
|
|
772
|
+
# version_distri
|
|
773
|
+
p = sub.add_parser("version_distri", help="版本分布: 各版本活跃设备数")
|
|
774
|
+
add_common_args(p)
|
|
775
|
+
|
|
776
|
+
# overview
|
|
777
|
+
p = sub.add_parser("overview", help="运营概览: 收入/活跃/新增汇总")
|
|
778
|
+
p.add_argument("-p", "--project-id", required=True, help="项目ID")
|
|
779
|
+
p.add_argument("--app-id", required=True, help="应用ID (从 list_projects 获取)")
|
|
780
|
+
p.add_argument("-s", "--start", required=True, help="开始日期")
|
|
781
|
+
p.add_argument("-e", "--end", required=True, help="结束日期")
|
|
782
|
+
p.add_argument("--interval", default="day", choices=["minute", "hour", "day", "week", "month"],
|
|
783
|
+
help="时间粒度 (默认 day)")
|
|
784
|
+
p.add_argument("--quota", default="income", choices=["income", "active", "activation"],
|
|
785
|
+
help="概览类型 (默认 income)")
|
|
786
|
+
p.add_argument("--no-cache", action="store_true")
|
|
787
|
+
|
|
788
|
+
# user_value (LTV)
|
|
789
|
+
p = sub.add_parser("user_value", help="用户价值(LTV): N日贡献")
|
|
790
|
+
add_common_args(p)
|
|
791
|
+
|
|
792
|
+
# whale_user
|
|
793
|
+
p = sub.add_parser("whale_user", help="鲸鱼用户: 高付费用户排行")
|
|
794
|
+
add_common_args(p)
|
|
795
|
+
|
|
796
|
+
# life_cycle
|
|
797
|
+
p = sub.add_parser("life_cycle", help="用户生命周期: 付费转化/金额/累计")
|
|
798
|
+
add_common_args(p)
|
|
799
|
+
p.add_argument("--quota", default="payment_amount",
|
|
800
|
+
choices=["payment_cvs_rate", "payment_cvs", "payment_amount", "acc_payment"],
|
|
801
|
+
help="生命周期指标 (默认 payment_amount)")
|
|
802
|
+
|
|
803
|
+
# ad_monet
|
|
804
|
+
p = sub.add_parser("ad_monet", help="广告变现数据")
|
|
805
|
+
add_common_args(p)
|
|
806
|
+
|
|
807
|
+
# describe
|
|
808
|
+
p = sub.add_parser("describe", help="查看各接口支持的指标、分组和过滤条件")
|
|
809
|
+
p.add_argument("target", nargs="?", help="接口名 (留空查看全部)")
|
|
810
|
+
|
|
811
|
+
# raw (灵活模式)
|
|
812
|
+
p = sub.add_parser("raw", help="原始请求(灵活模式)")
|
|
813
|
+
p.add_argument("path", help="API路径 (例: /op/active)")
|
|
814
|
+
p.add_argument("body", nargs="?", help="JSON请求体")
|
|
815
|
+
|
|
816
|
+
args = parser.parse_args()
|
|
817
|
+
|
|
818
|
+
cmd_map = {
|
|
819
|
+
"list_projects": cmd_list_projects,
|
|
820
|
+
"describe": cmd_describe,
|
|
821
|
+
"active": cmd_active,
|
|
822
|
+
"retention": cmd_retention,
|
|
823
|
+
"income": cmd_income,
|
|
824
|
+
"source": cmd_source,
|
|
825
|
+
"player_behavior": cmd_player_behavior,
|
|
826
|
+
"version_distri": cmd_version_distri,
|
|
827
|
+
"overview": cmd_overview,
|
|
828
|
+
"user_value": cmd_user_value,
|
|
829
|
+
"whale_user": cmd_whale_user,
|
|
830
|
+
"life_cycle": cmd_life_cycle,
|
|
831
|
+
"ad_monet": cmd_ad_monet,
|
|
832
|
+
"raw": cmd_raw,
|
|
833
|
+
}
|
|
834
|
+
cmd_map[args.command](args)
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
if __name__ == "__main__":
|
|
838
|
+
main()
|