@tapdb/tapdb-data-analysis 0.1.30 → 0.1.34

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 CHANGED
@@ -65,6 +65,7 @@ python3 tapdb-data-analysis/scripts/tapdb_query.py -r sg list_projects
65
65
  - "用 TapDB 分析一下 XXXX 游戏近 30 天的留存趋势有没有异常"
66
66
  - "用 TapDB 对比 XXX 游戏 和 XXX 游戏 的收入数据"
67
67
  - "用 TapDB 查下 XXX 游戏鲸鱼用户排行"
68
+ - "用 TapDB 查一下 XXX 游戏最近 30 天各媒体的买量成本和 ROI"
68
69
 
69
70
  AI 会自动调用 `tapdb_query.py` 脚本查询数据并生成分析报告。
70
71
 
@@ -81,4 +82,6 @@ AI 会自动调用 `tapdb_query.py` 脚本查询数据并生成分析报告。
81
82
  | 用户价值 | `user_value` | LTV (N日贡献) |
82
83
  | 鲸鱼用户 | `whale_user` | 高付费用户排行 |
83
84
  | 生命周期 | `life_cycle` | 付费转化率/金额/累计 |
85
+ | 买量成本 | `cost` | 花费/展示/点击/获客/留存/ROI(支持 `--measurement-criteria device\|account` 切换统计口径) |
86
+ | 广告投放 | `ad_data` | 广告投放数据(cost 无数据时的回退方案) |
84
87
  | 广告变现 | `ad_monet` | 广告收入数据 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tapdb/tapdb-data-analysis",
3
- "version": "0.1.30",
3
+ "version": "0.1.34",
4
4
  "description": "TapDB 游戏数据分析 AI Agent Skill - 查询和分析 TapDB 中的游戏运营数据(活跃/留存/付费/来源/LTV 等)",
5
5
  "keywords": [
6
6
  "tapdb",
@@ -3,15 +3,16 @@ name: tapdb-data-analysis
3
3
  description: >
4
4
  TapDB 游戏数据分析技能。用于查询和分析 TapDB 中的游戏运营数据,包括活跃(DAU/WAU/MAU)、
5
5
  留存(1日留存-180日留存)、付费(收入/ARPU/ARPPU)、来源(新增/转化)、用户价值(LTV)、版本分布、
6
- 玩家行为、广告变现等指标。
7
- 当用户需要查询游戏数据、分析运营指标、对比项目表现、检测数据异常、生成数据报告时使用此技能。
6
+ 玩家行为、广告变现、买量成本(CPI/CPA/ROI)等指标。
7
+ 当用户需要查询游戏数据、分析运营指标、对比项目表现、检测数据异常、生成数据报告、
8
+ 分析买量成本与 ROI 时使用此技能。
8
9
  触发关键词:TapDB、DAU、MAU、留存、付费、收入、ARPU、LTV、活跃、新增、来源、玩家行为、
9
- 版本分布、鲸鱼用户、广告变现、游戏数据分析。
10
+ 版本分布、鲸鱼用户、广告变现、游戏数据分析、买量、投放、广告投放、买量成本、CPI、CPA、ROI、ROAS、花费。
10
11
  ---
11
12
 
12
13
  # TapDB 数据分析
13
14
 
14
- > Skill 版本:v0.1.30
15
+ > Skill 版本:v0.1.33
15
16
 
16
17
  通过 Python 脚本调用 TapDB 运营数据查询接口,获取游戏指标数据并分析。
17
18
 
@@ -93,6 +94,16 @@ npm view @tapdb/tapdb-data-analysis version --registry https://registry.npmjs.or
93
94
  2. 候选日期展示给用户确认
94
95
  3. 按确认的周期分别查 active/income/retention/source,对比输出
95
96
 
97
+ ### F: 广告投放(买量)数据查询
98
+
99
+ **触发**:"买量数据/投放效果/CPI/CPA/广告花费/ROI/ROAS"
100
+
101
+ #### 优先使用 `cost` 子命令(主路径)
102
+
103
+ #### 当 `cost` 无数据或报错时,切换到 `ad_data` 子命令
104
+
105
+
106
+
96
107
  ## 脚本使用
97
108
 
98
109
  ### 基础命令
@@ -141,7 +152,13 @@ python3 <SKILL_DIR>/scripts/tapdb_query.py life_cycle -p 2588 -s 2026-02-01 -e 2
141
152
  python3 <SKILL_DIR>/scripts/tapdb_query.py whale_user -p 2588 -s 2026-01-01 -e 2026-02-25
142
153
  python3 <SKILL_DIR>/scripts/tapdb_query.py version_distri -p 2588 -s 2026-02-01 -e 2026-02-25
143
154
  python3 <SKILL_DIR>/scripts/tapdb_query.py player_behavior -p 2588 -s 2026-02-01 -e 2026-02-25 -g time
155
+ python3 <SKILL_DIR>/scripts/tapdb_query.py cost -p 2588 -s 2026-02-01 -e 2026-02-25 -g dt
156
+ python3 <SKILL_DIR>/scripts/tapdb_query.py cost -p 2588 -s 2026-02-01 -e 2026-02-25 -g media
157
+ python3 <SKILL_DIR>/scripts/tapdb_query.py cost -p 2588 -s 2026-02-01 -e 2026-02-25 -g dt --measurement-criteria device
144
158
  python3 <SKILL_DIR>/scripts/tapdb_query.py raw /op/active '{"project_id":2588,"start_time":"2026-02-01 00:00:00.000","end_time":"2026-02-25 23:59:59.999","subject":"device","quota":"dau","group":{"col_name":"time","col_alias":"date","is_time":true,"trunc_unit":"day"},"is_de_water":false,"filters":[]}'
159
+ # 广告投放数据(独立参数,不复用通用参数)
160
+ python3 <SKILL_DIR>/scripts/tapdb_query.py ad_data -p 2588 -s 2026-03-01 -e 2026-03-10 --quotas cost,display,activation,activationCost -g time
161
+ python3 <SKILL_DIR>/scripts/tapdb_query.py ad_data -p 2588 -s 2026-03-01 -e 2026-03-10 --quotas cost,activation,day1LTV,day7LTV,day30LTV -g platform_id
145
162
  ```
146
163
 
147
164
  ## 子命令速查
@@ -157,6 +174,8 @@ python3 <SKILL_DIR>/scripts/tapdb_query.py raw /op/active '{"project_id":2588,"s
157
174
  | `user_value` | LTV | 通用参数 | `activation_time` |
158
175
  | `whale_user` | 鲸鱼用户 | 通用参数 | 无分组 |
159
176
  | `life_cycle` | 生命周期 | `--quota payment_amount\|payment_cvs_rate\|payment_cvs\|acc_payment` | `activation_time` |
177
+ | `cost` | 买量成本(全链路) **← 广告数据首选** | 通用参数;分组维度: dt/media/os/country/campaign_id/ad_id 等;`--measurement-criteria device\|account`(统计口径:设备/账户,默认 device) | `dt` |
178
+ | `ad_data` | 广告投放(买量回退) **← cost 无数据时回退** | `--quotas cost,display,...`, `-g platform_id\|time\|...`(独立参数,不复用通用参数) | `time` |
160
179
  | `ad_monet` | 广告变现 | 通用参数 | 可能返回 404(未开通或路径不同) |
161
180
 
162
181
  ## 数据量控制策略(先小后大,必须遵守)
@@ -196,6 +215,12 @@ python3 <SKILL_DIR>/scripts/tapdb_query.py raw /op/active '{"project_id":2588,"s
196
215
  - `life_cycle` 在 `-g activation_os` 时仅支持 `--quota payment_cvs_rate`(其他 quota 会 500)
197
216
  - filters 格式: `{"col_name":"...", "data_type":"string|number|bool|date", "calculate_symbol":"include|un_include", "ftv":[...]}`
198
217
  - 各接口维度不同,不支持的维度返回 500。**先 `describe` 确认**
218
+ - **`ad_data` 广告投放接口注意**:
219
+ - 走 `/mcp/ad/multiple_display_web`,非 `/mcp/op/*`,独立参数体系
220
+ - 响应为二维数组格式(非 dict),脚本内部自动转为 dict 列表
221
+ - 时间粒度固定为 day,不支持 week/month
222
+ - cost/display/click 等花费类指标仅在 platform_id/advertisement_id/tag_id 等广告维度分组时有意义
223
+ - 需项目已开通 ad_plus 功能,未开通会返回错误
199
224
 
200
225
  ## 分析与报告
201
226
 
@@ -14,10 +14,11 @@
14
14
 
15
15
  ### 数据失败处理
16
16
 
17
- 1. **部分成功**:基于已有数据输出分析,注明缺失部分
18
- 2. **全部失败**:如实告知原因,不编造分析
17
+ 1. **部分成功必须继续输出**:只要任一查询成功返回数据,就必须基于已有数据继续输出分析;在输出开头披露缺失(如 `⚠️ XX 数据未获取到(原因:…),以下分析基于已获取数据`)。**绝对不允许**在有成功数据的情况下声称“没有数据/没查到”。
18
+ 2. **全部失败才停止**:全部查询失败时,如实告知失败原因,格式:`查询失败:[具体错误原因],请稍后重试或联系管理员`。**禁止**编造分析或让用户“自行准备数据”。
19
19
  3. **重试收敛**:同一查询连续失败 2 次即停止,标记为"数据未获取到",继续用已有数据完成任务
20
20
  4. ❌ 禁止通过拆分时间范围来"绕过"失败的查询
21
+ 5. 更细的输出纪律见 `references/output_rules.md` 的「失败/输出纪律(最高优先级)」
21
22
 
22
23
  ---
23
24
 
@@ -101,6 +101,54 @@ API 直接返回 DRx_rate/WRx_rate/MRx_rate(小数形式,如 0.7656=76.56%
101
101
  - ❌ 错误:周总收入(12,230,904) ÷ 周去重活跃(195,663) = 62.46 → 这不是ARPU
102
102
  - ✅ 正确:日均收入(1,747,272) ÷ 日均DAU(155,768) = 11.22 → 这才是日均ARPU
103
103
 
104
+ ### 投放 ROI / ROAS(cost 子命令,重要!)
105
+
106
+ cost 接口返回的金额字段有两套口径,计算 ROI 时必须区分:
107
+
108
+ | 指标 | 公式 | 含义 | 使用场景 |
109
+ |------|------|------|----------|
110
+ | **ROI** | `dayX_paid_amount / real_cost` | 付费金额 ÷ 实际消耗 | **默认使用**,投放效果评估 |
111
+ | **真实 ROI** | `dayX_real_income / real_cost` | 扣除渠道分成后的真实收入 ÷ 实际消耗 | 仅在用户明确要求"真实ROI"时使用 |
112
+
113
+ **字段说明**:
114
+ - `cost`:消耗(含平台服务费等)
115
+ - `real_cost`:实际消耗(扣除返点/折扣后的真实花费),**ROI 分母统一用 real_cost**
116
+ - `dayX_paid_amount`:新增设备上,注册后 X 天内累计付费总额(未扣渠道分成)
117
+ - `dayX_real_income`:新增设备上,注册后 X 天内累计真实收入(已扣渠道分成)
118
+
119
+ **硬规则**:
120
+ - ❌ 禁止用 `cost` 作为任何指标的分母或成本基准(应统一使用 `real_cost`)
121
+ - ❌ 禁止用 `dayX_real_income` 计算默认 ROI(那是"真实 ROI")
122
+ - ❌ 禁止在用户未明确要求"真实收入"时使用 `dayX_real_income` 作为收入展示值
123
+ - ✅ 所有涉及成本计算的指标(CPA、ROI、ROAS 等)统一使用 `real_cost` 作为成本值
124
+ - ✅ 展示"消耗/成本"时优先展示 `real_cost`(真实成本),而非 `cost`
125
+ - ✅ **展示"收入/付费金额"时默认使用 `dayX_paid_amount`**,仅在用户明确要求"真实收入"时才使用 `dayX_real_income`
126
+ - ✅ 默认 ROI = `dayX_paid_amount / real_cost`
127
+ - ✅ 真实 ROI = `dayX_real_income / real_cost`(需用户明确要求)
128
+
129
+ ### 成本数据统计口径(measurement_criteria)
130
+
131
+ cost 接口支持两种统计口径,通过请求体中的 `measurement_criteria` 字段控制:
132
+
133
+ | 值 | 含义 | 对应表 |
134
+ |---|------|--------|
135
+ | `"account"` | 账户口径 | `dwd_autogrowth_game_agent_retention_charge_fully_hr_v3` |
136
+ | 不传(默认) | 设备口径 | `dwd_autogrowth_game_agent_retention_charge_fully_hr_v2_device` |
137
+
138
+ **项目路由规则(用户未明确指定 account/device 口径时)**:
139
+ - ✅ **香肠派对(705)、火炬之光(2588)**:默认传 `measurement_criteria=account`(走 v3 账户口径表)
140
+ - ✅ 其余项目:不传此字段,走默认 v2 设备口径表
141
+ - ⚠️ 若用户明确要求使用 device 或 account 口径,以用户指定为准
142
+
143
+ **注意**:`cmd_cost` 子命令当前可能无法正确生效 `measurement_criteria`,需使用 `raw /op/omp-cost` 并在 JSON body 中显式传入 `"measurement_criteria":"account"` 以确保查询到 v3 表数据。
144
+
145
+ ### 成本/投放数据分析准则(硬规则,必须遵守)
146
+
147
+ 1. **所有数值必须来源于查询结果**:CPA、CTR、ROI、留存率等衍生指标必须基于接口返回的原始字段计算,禁止凭经验估算或编造任何数值
148
+ 2. **过滤自然量**:分析投放效果时,必须排除 `media` 为"自然量"的数据行(自然量 cost/real_cost 为 0,纳入会严重扭曲 CPA、ROI 等指标)。仅在用户明确要求"包含自然量"或查看"全量数据"时才保留
149
+ 3. **零消耗渠道不计算 ROI/CPA**:当某渠道 `real_cost = 0` 时,ROI 和 CPA 无意义,应标注"—"或"无投放",不能输出 0% 或 Inf
150
+ 4. **汇总指标以 API 返回为准**:若接口返回了汇总行(分组字段为 `null`),直接使用该行数据,不要在本地重复汇总(可能因截断导致不一致)
151
+
104
152
  ### LTV (Life Time Value)
105
153
 
106
154
  N日内人均累计付费金额(LTV1/3/7/14/30/60/90)。通过 `user_value` 子命令查询。
@@ -4,6 +4,13 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## 0) 失败/输出纪律(最高优先级)
8
+
9
+ - **部分成功必须继续输出**:只要任一工具成功返回了数据,就必须基于已获取数据继续完成输出;在开头用一句话披露缺失,例如:`⚠️ XX 数据未获取到(原因:…),以下基于已获取数据`。**绝对不允许**在有成功数据的情况下声称“没有数据/没查到”,也不得要求用户补充数据才能继续。
10
+ - **全部失败才允许停止**:全部工具失败时,如实告知失败原因,格式:`查询失败:[具体错误原因],请稍后重试或联系管理员`。**禁止**编造数据、禁止输出“通用分析框架”、禁止让用户“自行准备数据再来”。
11
+ - **禁止输出中间分析过程/过渡话术**:工具调用完成后,直接输出最终报告。❌ 禁止输出“让我分析一下”“数据已齐全”“现在生成报告”等过渡性文字。
12
+ - **禁止“先错后改”的更正表**:发现计算/汇总有误时,不输出“修正汇总/更正/补充”表;先在内部把派生指标(如 ARPU/ARPPU)算对,最终只输出一张正确的表。
13
+
7
14
  ## 1) 报告结构(结论优先)
8
15
 
9
16
  - 先给 **3~7 条关键结论**(每条尽量带 1 个关键数字或时间点),再给数据表,最后给建议/下一步
@@ -182,6 +182,38 @@ def _rebuild(resp, path, rows, info):
182
182
  return result
183
183
 
184
184
 
185
+ def _parse_ad_response(resp):
186
+ """Parse ad API response into (rows_as_dicts, summary_row_or_None).
187
+
188
+ The ad API returns a flat list:
189
+ [ total_count, [header1, header2, ...], [row1_val, ...], ..., [total_val, ...] ]
190
+ or wrapped in {"data": [...]}.
191
+ The first element is a total_count integer, followed by headers, data rows,
192
+ and a summary/total row at the end.
193
+ """
194
+ data = resp
195
+ if isinstance(resp, dict):
196
+ if resp.get("error"):
197
+ return None, None
198
+ data = resp.get("data", resp)
199
+ if not isinstance(data, list) or len(data) < 2:
200
+ return None, None
201
+ # Skip leading scalar (total_count) if present
202
+ start = 0
203
+ if not isinstance(data[0], list):
204
+ start = 1
205
+ if start >= len(data):
206
+ return None, None
207
+ headers = [str(h) for h in data[start]]
208
+ rows = [dict(zip(headers, r)) for r in data[start + 1:]]
209
+ if not rows:
210
+ return [], None
211
+ # Last row is the summary/total row
212
+ summary = rows[-1]
213
+ rows = rows[:-1]
214
+ return rows, summary
215
+
216
+
185
217
  def truncate_response(resp, cmd_type=None, group_alias=None):
186
218
  """Truncate API response to save context window tokens."""
187
219
  if not resp or (isinstance(resp, dict) and resp.get("error")):
@@ -221,10 +253,47 @@ COL_ALIAS_MAP = {
221
253
  "activation_app_version": "activation_app_version",
222
254
  "first_server": "first_server",
223
255
  "current_server": "current_server",
256
+ # cost 维度
257
+ "dt": "date",
258
+ "media": "media",
259
+ "media_source": "msrc",
260
+ "campaign_id": "camp",
261
+ "ad_id": "ad",
262
+ "creative_id": "crtv",
263
+ "account_id": "acct",
264
+ "material_id": "mtrl",
265
+ "medium_id": "mdm",
266
+ "ad_platform_id": "adplt",
267
+ "opt_obj": "optobj",
268
+ "scene": "scene",
269
+ "scene_final": "scnf",
270
+ "city": "city",
271
+ "country": "country",
272
+ "country_code": "cycd",
273
+ "province": "province",
274
+ "os": "os",
275
+ "channel": "channel",
276
+ "app_id": "appid",
277
+ "tap_app_id": "tapid",
278
+ "google_opt_obj": "gopt",
279
+ "china_opt_obj": "copt",
280
+ "project_name": "proj",
224
281
  }
225
282
 
226
283
  COUNTRY_GROUP_DIMS = {"activation_country", "activation_province"}
227
284
 
285
+ AD_COL_ALIAS_MAP = {
286
+ "time": "date_",
287
+ "platform_id": "pf",
288
+ "advertisement_id": "ad",
289
+ "tag_id": "tag",
290
+ "first_ad_sub_channel1": "subc",
291
+ "first_ad_sub_channel2": "subc",
292
+ "first_ad_sub_channel3": "subc",
293
+ "first_ad_sub_channel4": "subc",
294
+ "first_ad_sub_channel5": "subc",
295
+ }
296
+
228
297
 
229
298
  # ── 各接口能力描述(基于源码 + 实测验证) ────────────────────
230
299
  #
@@ -475,9 +544,94 @@ ENDPOINT_CAPS = {
475
544
  },
476
545
  "unsupported_note": "分组仅支持 time/activation_time/activation_os;过滤不支持 activation_app_version/first_server/current_server/utmsrc/login_type/payment_source",
477
546
  },
547
+ "cost": {
548
+ "description": "买量成本数据: 花费/展示/点击/获客/留存/付费/ROI 全链路",
549
+ "time_field": "dt",
550
+ "returned_metrics": [
551
+ "cost", "real_cost", "show_cnt", "click_cnt", "reserve_cnt",
552
+ "new_device", "new_user", "new_device_safe", "new_user_safe",
553
+ "reattr_new_device", "reattr_new_user", "paid_user_uv",
554
+ "retention_2d", "retention_3d", "retention_7d", "retention_14d",
555
+ "retention_30d", "retention_60d", "retention_90d", "retention_180d",
556
+ "day1_paid_amount", "day2_paid_amount", "day3_paid_amount",
557
+ "day7_paid_amount", "day14_paid_amount", "day30_paid_amount",
558
+ "day60_paid_amount", "day90_paid_amount", "day180_paid_amount",
559
+ "day1_real_income", "day2_real_income", "day3_real_income",
560
+ "day7_real_income", "day14_real_income", "day30_real_income",
561
+ "day60_real_income", "day90_real_income", "day180_real_income",
562
+ ],
563
+ "groups": [
564
+ "dt", "media", "media_source", "os", "country", "country_code", "province", "city",
565
+ "channel", "campaign_id", "ad_id", "creative_id",
566
+ "account_id", "material_id", "medium_id", "ad_platform_id",
567
+ "app_id", "tap_app_id", "opt_obj", "scene", "scene_final",
568
+ "google_opt_obj", "china_opt_obj", "project_name",
569
+ ],
570
+ "filters": [
571
+ "media", "media_source", "os", "country", "country_code", "province", "city",
572
+ "channel", "campaign_id", "ad_id", "creative_id",
573
+ "account_id", "material_id", "medium_id", "ad_platform_id",
574
+ "app_id", "tap_app_id", "opt_obj", "scene", "scene_final",
575
+ "google_opt_obj", "china_opt_obj", "project_name",
576
+ "material_type", "material_mode",
577
+ "name", "tags_str", "jump_url", "account_alias",
578
+ ],
579
+ "unsupported_note": "不支持现有运营维度(activation_channel等),使用广告维度(media/campaign_id等)",
580
+ },
478
581
  "ad_monet": {
479
582
  "description": "广告变现数据(MCP 代理路径 /mcp/op/ad_monet 返回 404,可能未开通或路径不同)",
480
583
  },
584
+ "ad_data": {
585
+ "description": "广告投放(买量)数据: 花费/展示/点击/激活/留存/LTV等(仅用户明确要求广告/买量数据时使用)",
586
+ "quotas": {
587
+ "cost_display": ["cost", "display"],
588
+ "click": ["click", "uClick", "clickRate", "clickCost"],
589
+ "activation": [
590
+ "activation", "activeRate", "activationCost",
591
+ "newUser", "convertRate", "convertDevice",
592
+ "payUserNew", "payRateNew", "payAmountNew",
593
+ ],
594
+ "device_retention": [
595
+ "day1Retain", "day1RetainRate",
596
+ "day6Retain", "day6RetainRate",
597
+ "day13Retain", "day13RetainRate",
598
+ "day29Retain", "day29RetainRate",
599
+ ],
600
+ "payment": ["income", "payUser", "payRate", "payTimes"],
601
+ "ltv": [
602
+ "day1LTV", "day3LTV", "day7LTV", "day14LTV",
603
+ "day30LTV", "day60LTV", "day90LTV", "day180LTV", "day360LTV",
604
+ ],
605
+ },
606
+ "groups": [
607
+ "time", "platform_id", "advertisement_id", "tag_id",
608
+ "first_ad_sub_channel1", "first_ad_sub_channel2",
609
+ "first_ad_sub_channel3", "first_ad_sub_channel4",
610
+ "first_ad_sub_channel5",
611
+ ],
612
+ "filters": [
613
+ "activation_os", "activation_country", "activation_continent",
614
+ "activation_province", "activation_network", "activation_provider",
615
+ "activation_device_model", "activation_app_version",
616
+ "first_ad_platform_id", "first_ad_advertiser_id",
617
+ "first_campaign_id", "first_adgroup_id", "first_creative_id",
618
+ "first_ad_conversion_link_id", "activation_channel",
619
+ ],
620
+ "extra_params": [
621
+ "ad_increment (bool, 默认true: 仅广告增量)",
622
+ "tz_offset (int, 默认8: 时区偏移)",
623
+ "sort_field / sort_order (排序字段和顺序)",
624
+ "charge_subject (user|device, 默认user)",
625
+ "exchange_to_currency (str, 如 USD/CNY/JPY,默认CNY)",
626
+ ],
627
+ "notes": [
628
+ "走 /mcp/ad/multiple_display_web 接口,非 /mcp/op/*",
629
+ "时间粒度固定为 day,不支持 week/month",
630
+ "cost/display/click 等花费类指标仅在 platform_id/advertisement_id/tag_id 分组时有意义",
631
+ "响应格式为二维数组(非 dict),脚本内部会转为 dict 列表",
632
+ "需项目已开通 ad_plus 功能",
633
+ ],
634
+ },
481
635
  }
482
636
 
483
637
 
@@ -500,8 +654,9 @@ def cmd_describe(args):
500
654
  if "subjects" in cap:
501
655
  info["subjects"] = cap["subjects"]
502
656
  if "groups" in cap:
657
+ alias_map = AD_COL_ALIAS_MAP if name == "ad_data" else COL_ALIAS_MAP
503
658
  info["supported_groups"] = [
504
- {"col_name": g, "col_alias": COL_ALIAS_MAP.get(g, g),
659
+ {"col_name": g, "col_alias": alias_map.get(g, g),
505
660
  **({"note": cap.get("group_notes", {}).get(g)} if g in cap.get("group_notes", {}) else {})}
506
661
  for g in cap["groups"]
507
662
  ]
@@ -519,6 +674,8 @@ def cmd_describe(args):
519
674
  info["returned_metrics"] = cap["returned_metrics"]
520
675
  if "unsupported_note" in cap:
521
676
  info["unsupported_note"] = cap["unsupported_note"]
677
+ if "notes" in cap:
678
+ info["notes"] = cap["notes"]
522
679
  result[name] = info
523
680
 
524
681
  output(result)
@@ -536,6 +693,19 @@ def build_group(group_by, group_unit):
536
693
  }
537
694
 
538
695
 
696
+ def build_ad_group(group_by):
697
+ """Build group dict for the ad API (fixed day granularity)."""
698
+ if not group_by:
699
+ return None
700
+ is_time = group_by == "time"
701
+ return {
702
+ "col_name": group_by,
703
+ "col_alias": AD_COL_ALIAS_MAP.get(group_by, group_by),
704
+ "is_time": is_time,
705
+ "trunc_unit": "day",
706
+ }
707
+
708
+
539
709
  def build_base_body(args):
540
710
  body = {"project_id": int(args.project_id)}
541
711
  if hasattr(args, "start") and args.start:
@@ -651,6 +821,67 @@ def cmd_life_cycle(args):
651
821
  do_query(args, "life_cycle", {"quota": args.quota})
652
822
 
653
823
 
824
+ def cmd_cost(args):
825
+ if not args.group_by or args.group_by == "time":
826
+ args.group_by = "dt"
827
+ extra = {}
828
+ mc = getattr(args, "measurement_criteria", None)
829
+ if mc:
830
+ extra["measurement_criteria"] = mc
831
+ do_query(args, "omp-cost", extra=extra or None, cmd_type="cost")
832
+
833
+
834
+ def cmd_ad_data(args):
835
+ """查询广告投放(买量)数据 — 走 /mcp/ad/multiple_display_web。"""
836
+ key, base_url = get_config(args.region)
837
+ group_by = args.group_by or "time"
838
+ quotas = [q.strip() for q in args.quotas.split(",") if q.strip()]
839
+ body = {
840
+ "project_id": int(args.project_id),
841
+ "start_time": f"{args.start} 00:00:00",
842
+ "end_time": f"{args.end} 23:59:59",
843
+ "group": build_ad_group(group_by),
844
+ "quotas": quotas,
845
+ "ad_increment": args.ad_increment.lower() != "false",
846
+ "tz_offset": args.tz_offset,
847
+ "charge_subject": args.charge_subject,
848
+ "filters": json.loads(args.filters) if args.filters else [],
849
+ "use_cache": True,
850
+ "page": 1,
851
+ "page_size": 5000,
852
+ }
853
+ if args.sort_field:
854
+ body["sort"] = {"field": args.sort_field, "order": args.sort_order}
855
+ exchange_to = getattr(args, "exchange_to_currency", None)
856
+ if exchange_to and exchange_to.lower() != "none":
857
+ body["exchange_to_currency"] = exchange_to.upper()
858
+
859
+ url = f"{base_url}/mcp/ad/multiple_display_web"
860
+ resp = http_request("POST", url, {"MCP-KEY": key}, body)
861
+
862
+ if isinstance(resp, dict) and resp.get("error"):
863
+ output(resp)
864
+ return
865
+
866
+ rows, summary = _parse_ad_response(resp)
867
+ if rows is None:
868
+ output(resp)
869
+ return
870
+
871
+ group_alias = AD_COL_ALIAS_MAP.get(group_by, group_by)
872
+ if not getattr(args, "no_truncate", False):
873
+ rows, trunc_info = _slim_rows(rows, "ad_data", group_alias=group_alias)
874
+ else:
875
+ trunc_info = None
876
+
877
+ result = {"data": rows}
878
+ if summary is not None:
879
+ result["summary"] = summary
880
+ if trunc_info:
881
+ result["_truncation"] = trunc_info
882
+ output(result)
883
+
884
+
654
885
  def cmd_ad_monet(args):
655
886
  do_query(args, "ad_monet")
656
887
 
@@ -752,6 +983,34 @@ def main():
752
983
  choices=["payment_cvs_rate", "payment_cvs", "payment_amount", "acc_payment"],
753
984
  help="生命周期指标 (默认 payment_amount)")
754
985
 
986
+ # cost
987
+ p = sub.add_parser("cost", help="买量成本数据: 花费/展示/点击/获客/留存/ROI")
988
+ add_common_args(p)
989
+ p.add_argument("--measurement-criteria", default="device", choices=["device", "account"],
990
+ help="统计口径: device(按设备,默认) | account(按账户)")
991
+
992
+ # ad_data (广告投放/买量)
993
+ p = sub.add_parser("ad_data", help="广告投放(买量)数据: 花费/展示/点击/激活/留存/LTV")
994
+ p.add_argument("-p", "--project-id", required=True, help="项目ID")
995
+ p.add_argument("-s", "--start", required=True, help="开始日期 YYYY-MM-DD")
996
+ p.add_argument("-e", "--end", required=True, help="结束日期 YYYY-MM-DD")
997
+ p.add_argument("-g", "--group-by", default="time",
998
+ help="分组: time/platform_id/advertisement_id/tag_id/first_ad_sub_channel1-5 (默认 time)")
999
+ p.add_argument("--quotas", required=True,
1000
+ help="逗号分隔指标列表 (如 cost,display,activation,activationCost)")
1001
+ p.add_argument("--ad-increment", default="true",
1002
+ help="仅广告增量 (默认 true; false 包含自然量)")
1003
+ p.add_argument("--tz-offset", type=int, default=8,
1004
+ help="时区偏移 (默认 8, 即 UTC+8)")
1005
+ p.add_argument("--sort-field", help="排序字段 (如 cost, activation)")
1006
+ p.add_argument("--sort-order", default="desc", choices=["asc", "desc"],
1007
+ help="排序方向 (默认 desc)")
1008
+ p.add_argument("--filters", help='过滤条件JSON')
1009
+ p.add_argument("--charge-subject", default="user", choices=["user", "device"],
1010
+ help="付费主体 (默认 user)")
1011
+ p.add_argument("--exchange-to-currency", default="CNY",
1012
+ help="金额目标货币 (如 USD/CNY/JPY,默认CNY)")
1013
+
755
1014
  # ad_monet
756
1015
  p = sub.add_parser("ad_monet", help="广告变现数据")
757
1016
  add_common_args(p)
@@ -779,6 +1038,8 @@ def main():
779
1038
  "user_value": cmd_user_value,
780
1039
  "whale_user": cmd_whale_user,
781
1040
  "life_cycle": cmd_life_cycle,
1041
+ "cost": cmd_cost,
1042
+ "ad_data": cmd_ad_data,
782
1043
  "ad_monet": cmd_ad_monet,
783
1044
  "raw": cmd_raw,
784
1045
  }