Initial Daily Campaign Agent release

This commit is contained in:
joliehill294-hue
2026-09-16 21:00:13 +08:00
commit 14fc94b3f0
86 changed files with 10766 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
---
name: ads-analysis
description: 分析广告账户投放数据,识别异常 Campaign,并形成优化建议。用于广告投放复盘和异常诊断。
---
# Ads Analysis
先调用 `mock_ads_metrics` 获取指定账户和时间范围的数据,再进行分析。数值结论必须来自工具结果;本 Skill 不拥有、也不声称拥有任何真实广告平台数据。
分析账户汇总指标,找出 CTR、CPA、ROAS 或转化显著异常的 Campaign,并给出有数据依据的优化建议。用户附图只能作为辅助信号,不得臆造图片中不可见的数据。
@@ -0,0 +1,21 @@
name: validate
on:
push:
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- name: Run standard-library tests
run: python3 -m unittest discover -s tests -v
- name: Compile scripts
run: python3 -m py_compile scripts/*.py tests/*.py
@@ -0,0 +1,13 @@
__pycache__/
*.py[cod]
.pytest_cache/
.venv/
.DS_Store
# User data and generated artifacts
*.xlsx
*.xls
*.docx
output/
delivery-profile.local.md
*.analysis.json
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Daily Campaign Triage contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,126 @@
# VK Daily Campaign Triage
一个面向广告优化人员的 Codex Skill:读取 `.xlsx` / `.csv` / `.tsv` 投放日报,
先确认 KPI、成熟日期和消耗目标,再从 **Ad 层级**划分学习期池与稳定期池,产出
当天需要人工干预的对象、证据和可执行动作。
**亮点:它不只报数。** 针对 VK 广告生态,它会把 7/14 天趋势翻译成可以在平台中
落实的优化指导,例如暂停差 Ad、预算回退观察、保护成熟赢家、拆分词包/受众假设、
创建单变量素材测试,以及按 3 / 5 / 7 天决定去留和扩量。
它只做分析和建议,不会擅自修改 VK 或其他广告平台的预算、出价、状态和素材。
## 它解决什么
- 将当天获取指标与滞后成熟指标分开,避免用未成熟次留误判;
- 对学习期 Ad 按 3 / 5 / 7 天检查点排名和优胜劣汰;
- 对稳定期 Ad 按 `S-优质 / A-稳定 / B-波动待优化 / C-劣质` 排名;
- 结合近 7 天、前 7 天和 14 天证据识别偶发波动、持续恶化和成熟赢家;
- 输出暂停/降量、观察、恢复测试、扩量、新素材需求和预算缺口;
- 生成四段式 `.docx` 简报,保持 Ad 级视角,不输出 Campaign 排名。
## 为什么适合 VK 投放
普通日报往往只告诉你指标涨跌;这个 Skill 会进一步回答“今天具体动什么”:
| 数据现象 | VK 可落地指导 |
|---|---|
| 新 Ad 同时上线,样本尚未成熟 | 放入同一学习池排名,按第3/5/7天优胜劣汰 |
| 扩量后连续成熟日恶化 | 回到最近稳定预算,保持其他变量不变,设定恢复测试退出条件 |
| 历史强组合重组后失效 | 分离素材疲劳与词包/受众变化,优先建立干净对照测试 |
| 原素材稳定但已投放较久 | 保护存量,不继续硬推;复制方向并只改变一个创意变量 |
| 有消耗缺口但没有成熟赢家 | 用有上限的新素材探索池补量,不把预算强塞给差 Ad |
| Ad 零消耗 | 提示检查 VK 审核状态、出价竞争力、受众规模和父级投放约束 |
每条建议都会尽量包含:**对象、证据窗口、建议动作、保持不变的变量、预算影响、
复查条件和退出条件**。缺少预算、出价、状态或操作历史时,会明确标注限制,不会
虚构精确的平台设置。
## 匿名日报模板预览
下图中的账户、Ad 名称、日期、数值和 KPI 阈值均为虚构内容:
![匿名 VK 广告投放日报模板](assets/daily-brief-preview.png)
## 安装
将仓库克隆到 Codex Skills 目录:
```bash
git clone https://github.com/joliehill294-hue/daily-campaign-triage.git \
~/.codex/skills/daily-campaign-triage
```
安装后重新启动 Codex。也可以在 Codex 中使用
`$skill-installer`,要求它从该 GitHub 仓库安装。
## 使用
在 Codex 中附上或提供报表路径,并明确调用:
```text
$daily-campaign-triage 分析这份日报。KPI:CPI ≤ 5;D1次留成本 ≤ 35;
D1次留率 ≥ 14%;日消耗目标 500。输出 Word 到 ./output。
```
Skill 会先检查数据结构。若 KPI 的公式、方向、适用范围或成熟规则缺失,会先向你
确认,绝不会从代码库或其他账户静默读取 KPI。以上调用中的阈值仅为虚构示例。
## 确定性工具
无需安装第三方 Python 包:
```bash
python3 scripts/analyze_report.py report.xlsx \
--analysis-date 2026-08-25 \
--retention-lag-days 1 \
--output /tmp/report.analysis.json
python3 scripts/build_brief.py examples/brief.example.json \
--output /tmp/example-brief.docx
```
第一个脚本只生成 Ad 级事实,不代替 Skill 做业务判断;第二个脚本将已经完成判断的
JSON 渲染为 Word。字段映射和 JSON 合同见
[references/script-usage.md](references/script-usage.md)。
## 日报输出结构
1. 是否达标
2. 问题分析:学习期池排名、稳定期池排名
3. 建议动作
4. 近 7 天表现好的 Ads(严格筛选,允许少于 3 个)
仓库内的示例和模板均为匿名数据。`.gitignore` 默认排除真实 Excel、生成的 Word、
分析 JSON 和本地交付配置。
## 验证
```bash
python3 -m unittest discover -s tests -v
```
还可以直接让 `$skill-creator` 验证当前目录中的 Skill 结构。
## 目录
```text
SKILL.md Skill 主说明
agents/openai.yaml Codex 展示元数据
references/ 输入、决策、排名、输出与交付合同
scripts/analyze_report.py CSV/TSV/XLSX 事实归一化
scripts/build_brief.py DOCX 生成器
examples/ 匿名 JSON 示例
assets/ GitHub 日报 PNG 预览
tests/ 标准库自动化测试与匿名 fixture
```
## 隐私与安全
- 不要提交真实广告报表、账户名称、个人绝对路径或生成的客户简报;
- 不要把固定 KPI 写进 Skill,KPI 必须由每次任务或本地配置提供;
- 所有建议仍需人工确认,本 Skill 不执行平台变更。
## License
[MIT](LICENSE)
@@ -0,0 +1,192 @@
---
name: "daily-campaign-triage"
description: "Analyze advertising daily reports or Excel/CSV exports at Ad level, especially VK Ads ecosystem reports, using confirmed KPI, maturity dates, and 7/14-day trends to produce actionable pause, recovery, creative-test, and scale guidance. Analysis-only: do not change platform state."
---
# Daily Campaign Triage
Turn an advertising report into a short, evidence-backed operator queue focused on
Ads. Use Campaign and Ad Group only as source-mapping or identity context when needed;
do not add Campaign-level rankings to the daily brief. Support raw spreadsheets,
CSV exports, pasted tables, and narrative briefs without assuming a platform, schema,
currency, timezone, attribution model, or KPI.
This is read-only decision support. Never change budgets, bids, status, targeting,
creative, or platform state unless the user separately requests and authorizes those
exact actions.
## Operating modes
- **Daily triage:** identify today's manual-intervention queue from the latest mature
KPI day plus recent 7/14-day evidence.
- **Follow-up diagnosis:** answer why a named entity changed and whether to rollback,
hold, retest, pause/reduce, or consider scaling.
- **VK optimization brief:** translate Ad-level evidence into VK-relevant operator
guidance for budget rollback, learning protection, creative-variable tests, audience
or word-pack isolation, and controlled scaling without pretending the action ran.
Use daily triage by default. Add follow-up diagnosis when the user names an entity,
describes a recent intervention, or asks a causal/recovery question.
## Required input
Establish this contract before making business verdicts:
| Input | Required content | If absent |
|---|---|---|
| Report | Readable spreadsheet/CSV/table; date plus stable hierarchy or names; spend and KPI components | Ask for a readable source or explain which analyses are impossible |
| KPI card | Metric, formula, direction, target, scope, and maturity rule for every decision KPI | Stop before pass/fail or action labels; ask once in a compact list |
| Analysis date | Requested report date, or permission to use the latest complete delivery date; each lagged KPI's latest mature cohort | Derive only when definitions and coverage make it unambiguous; otherwise confirm |
| Pacing card | Spend target, currency, scope, period, and whether it is a minimum, desired level, cap, or range | Required only for pacing/scale-allocation conclusions; otherwise mark pacing unavailable |
Useful optional input:
- minimum sample or persistence rule per KPI;
- budget adjustment rule or standard test budget per new Ad;
- timezone, currency, attribution window, status, budget, and launch date;
- dated budget/bid/targeting/creative/structure/tracking changes;
- platform constraints, such as whether status or budget can change at Ad level.
Accept natural language, or use this standard intake card when prompting is needed:
```text
数据文件/表格:
要分析的日报日期: # 留空则使用最新完整投放日
KPI:指标|公式|方向|目标|适用范围|成熟规则
日消耗目标:金额|币种|范围|最低/期望/上限/区间
最低样本/持续性规则(可选):
预算调整规则/新 Ad 标准测试预算(可选):
近期操作(可选):日期|对象|预算/出价/词包/定向/素材/结构|改前→改后
本次特别关注(可选):
```
Do not ask the user to repeat values already supplied in the current request or
available conversation context. Restate them in `分析口径确认` and ask only about
missing or contradictory fields that can materially change the verdict.
If minimum-sample or persistence rules are not supplied, use the framework defaults
only as provisional evidence rules and disclose them. Never manufacture missing KPI,
budget history, or platform capability.
Read [references/input-contract.md](references/input-contract.md) when mapping a new
report, resolving KPI or pacing definitions, or handling hierarchical rows and
different maturity dates. Read
[references/decision-framework.md](references/decision-framework.md) before making
the triage. For every daily brief, read
[references/learning-stable-ranking.md](references/learning-stable-ranking.md) to
build separate learning-period and stable-period rankings. Read
[references/output-standard.md](references/output-standard.md) before producing the
final report. When the user has configured file delivery, also read
[references/delivery-profile.md](references/delivery-profile.md) and deliver the
brief through that format/path instead of placing the full brief in chat. Also read
[references/change-response.md](references/change-response.md) when the user reports
recent scaling, rollback, restructuring, targeting/creative changes, a sudden drop,
a former winner that stopped working, or asks how to rescue an entity.
When the report comes from VK or the user asks for VK-specific execution guidance,
also read [references/vk-optimization.md](references/vk-optimization.md).
For local `.xlsx`, `.csv`, or `.tsv` inputs, prefer the deterministic standard-library
analyzer in `scripts/analyze_report.py`; it produces normalized Ad-level evidence but
does not invent business decisions. Read [references/script-usage.md](references/script-usage.md)
before running it. After applying the decision framework, place the final judgments in
the brief JSON contract and use `scripts/build_brief.py` to create the DOCX. The scripts
have no third-party Python dependencies.
## Confirmation gates
Use these as decision gates, not ceremonial questions.
### Gate A — source and grain
Inspect the file first. Proceed without asking when sheet, row grain, hierarchy,
dates, units, and totals are unambiguous. Ask for confirmation only when competing
mappings would materially change aggregation or attribution. State the proposed
mapping in the question. If the mapping is clear, record it in the output and continue
in the same turn; do not wait for ceremonial approval.
### Gate B — KPI and maturity
This gate is mandatory before `达标/未达标`, pause/reduce, rollback, or scale labels.
Confirm any missing KPI target/direction/formula/scope and the latest mature date or
maturity rule. Never retrieve targets from unrelated code or silently reuse another
account's values. Until confirmed, continue only with data-quality and neutral trend
analysis labeled `无业务 KPI,仅分析趋势`.
### Gate C — change causality
Use only for follow-up diagnosis. If the recommendation depends on whether a budget,
bid, word pack, audience, creative, or structure changed, confirm the material change
and approximate effective date when the report cannot establish it. Do not block a
plain performance classification that does not rely on causality.
### Gate D — execution boundary
Recommendations do not authorize platform changes. If the user later asks to execute,
confirm the exact entities and actions immediately before mutation under the relevant
platform workflow. This skill itself remains analysis-only.
## Standard workflow
1. **Intake:** identify mode, requested date, source coverage, and supplied KPI/pacing
cards. List only missing inputs that can change today's verdict.
2. **Validate:** inspect sheets, columns, row grain, identities, units, duplicates,
blanks versus zero, parent totals, missing dates, and reconciliation risk. Preserve
the source file.
3. **Normalize:** aggregate from the finest trustworthy leaf rows; never double-count
parent totals. Recompute ratio KPIs from summed numerators and denominators.
4. **Align maturity:** set a separate analysis date and comparison windows for each
lagged KPI. Exclude immature cohorts from negative judgments.
5. **Build windows:** calculate analysis day, recent 7 mature days, previous 7 mature
days, and 14-day context for each trustworthy Ad.
6. **Split and rank pools:** separate learning-period from stable-period entities.
Apply the 3/5/7-day checkpoints to the learning pool and the
quality/stability/KPI tiers to the stable pool.
7. **Diagnose Ads:** rank and diagnose Ads directly. Preserve parent names only to
disambiguate Ads with duplicate names; keep Campaign aggregates out of the report.
8. **Analyze changes when relevant:** compare pre-change control, transition, and
post-change mature cohorts; separate observed facts from causal hypotheses.
9. **Classify action:** choose one state—pause/reduce, rollback/recovery,
hold/protect, clean-control retest, scale candidate, investigate data, or no action.
10. **Prioritize:** rank by impact, confidence, and urgency; quantify pacing impact and
avoid filling spend gaps with unvalidated winners.
11. **Report:** render the fixed output contract below, including recheck and exit
conditions. Never imply that a recommendation was executed.
12. **Deliver:** when a delivery profile is active, create and verify the requested
artifact, save it using the analysis date—not the generation date—and return only
a concise completion message plus the file link in chat.
Keep generated evidence JSON, source exports, local delivery overrides, and output
documents outside the reusable Skill repository. Never ship an advertiser's raw data
or historical named-Ad diagnosis as an example.
## Output contract
Always output these sections in order:
1. **是否达标** — list every confirmed KPI as `KPI:值(达标/未达标)`, using its
correct mature date; include spend target attainment and material caveats here.
2. **问题分析** — show the Ad-level learning-period ranking and stable-period ranking
with evidence, tier, and decision. Do not include a Campaign-level ranking.
3. **建议动作** — list pause, budget reduction, hold/recheck, scale, new-Ad demand,
and creative-refresh alerts; quantify released budget or spend capacity when valid.
4. **近7天表现好的 Ads(3–5个)** — name, KPI data, spend/sample, stage, consistency,
and why each Ad is good. Return fewer than three rather than lower the standard.
Use the exact table fields and concise writing rules in
[references/output-standard.md](references/output-standard.md). Omit empty detail
rows, but never omit a section heading; state `无` so the absence is explicit.
## Decision boundaries
- Recommend an Ad pause/reduction candidate only when that Ad has persistent mature
underperformance, meaningful exposure, adequate confirmed sample, and no plausible
data-lag or recent-change explanation.
- Recommend at Ad level. Mention an Ad Group or Campaign only when the platform lever
is unavailable at Ad level or parent context is required to identify the Ad.
- A daily spend shortfall is not permission to scale failing traffic. Fill pacing gaps
through verified winners or bounded exploration; if neither exists, report the gap.
- Prefer a controlled rollback or clean retest over an immediate pause when a known
configuration change plausibly explains deterioration and a credible prior control
exists. Treat rollback as a recovery test, not proof of recovery.
- Do not use an unvalidated new winner to fill a pacing gap. One exceptional cohort or
one to two good days is not durable scale evidence.
@@ -0,0 +1,4 @@
interface:
display_name: "Daily Ad Triage"
short_description: "Turn VK ad reports into actionable daily decisions"
default_prompt: "Use $daily-campaign-triage to analyze this VK report at Ad level, confirm KPI and maturity dates, rank learning and stable pools, recommend executable optimization actions, and export the daily brief."
Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

@@ -0,0 +1,32 @@
{
"title": "2026-08-25 投放简报",
"account_label": "示例账户|广告投放日报",
"analysis_date": "2026-08-25",
"retention_mature_date": "2026-08-24",
"portfolio_kpis": [
{"metric": "CPI", "actual": "4.20", "target": "≤ 5.00", "verdict": "达标", "basis": "投放数据日;汇总消耗 / 安装"},
{"metric": "D1次留成本", "actual": "28.00", "target": "≤ 35.00", "verdict": "达标", "basis": "成熟日 cohort;汇总消耗 / 次留人数"},
{"metric": "D1次留率", "actual": "15.00%", "target": "≥ 14.00%", "verdict": "达标", "basis": "成熟日 cohort;次留人数 / 安装"},
{"metric": "日消耗", "actual": "420.00", "target": "500.00", "verdict": "未达标", "basis": "缺口 80.00"}
],
"kpi_note": "不同指标使用各自成熟日期;未成熟次留不用于负面判断。",
"learning_pool": [
{"rank": "1", "ad": "Creative Alpha", "stage": "第3天", "evidence": "CPI达标;次留样本尚未成熟", "decision": "继续观察至第5天"}
],
"stable_pool": [
{"rank": "1", "tier": "S-优质", "ad": "Creative Cedar", "evidence": "近7日KPI超额完成且波动小", "decision": "保护存量;作为新变体母版"},
{"rank": "2", "tier": "B-波动待优化", "ad": "Creative Birch", "evidence": "CPI达标,次留成本波动", "decision": "保持低量并复查3天"}
],
"actions": [
{"action": "暂停候选", "ad": "Creative Delta", "evidence": "稳定期且连续成熟样本严重不达标", "budget_impact": "释放约30/日"},
{"action": "新建变体", "ad": "Creative Cedar方向", "evidence": "近7日稳定且素材变量可拆分", "budget_impact": "测试预算20/日"}
],
"action_notes": [
{"label": "预算影响", "text": "执行止损与新测后仍有消耗缺口;不要为填满目标而强推未验证素材。"},
{"label": "3 / 5 / 7天", "text": "第3天优胜劣汰,第5天保留前列,第7天再决定是否进入稳定池。"}
],
"good_ads": [
{"rank": "1", "ad": "Creative Cedar", "data": "消耗252;60安装;9次留;CPI 4.20", "why_good": "样本足、KPI达标且结果不集中", "next_step": "保护原Ad;新建单变量变体"}
],
"data_note": "这是完全匿名的格式示例,不代表任何真实账户、KPI或广告数据。"
}
@@ -0,0 +1,150 @@
# Change response and recovery analysis
Use this reference only when a recent intervention or suspected change point matters:
budget/bid scaling, rollback, targeting or word-pack changes, creative replacement,
Ad Group restructuring, a sudden deterioration, a new “black horse,” or a request to
rescue a previously successful entity.
## 1. Reconstruct the timeline before assigning cause
Create a compact event timeline from the report and intervention log. Split relevant
history into:
1. **Pre-change control:** the last sufficiently stable configuration and its mature
KPI cohorts.
2. **Transition:** delivery dates whose outcomes are immature or mix old and new
configuration effects.
3. **Post-change:** only cohorts mature enough for each KPI.
Compare like-for-like phases at the lowest trustworthy entity. Show exact dates,
spend, sample, KPI results, and configuration when known. Rolling 7D/14D windows remain
useful context, but must not blur a sharp phase break.
Timing is not causality. Use language such as `与变更时间一致` until competing
explanations are checked. If deterioration began before restructuring, say so: the
restructure may have amplified or failed to fix the problem, but is not the sole
demonstrated cause.
## 2. Separate expansion shock from organic deterioration
Call an expansion-related deterioration plausible only when a confirmed budget, bid,
reach, allocation, or traffic-mix change precedes the mature decline. Observed spend
alone is not budget history.
- If budget/delivery increased and downstream quality fell after the change, test an
expansion or traffic-mix hypothesis.
- If spend stayed broadly stable while acquisition remained efficient but retention,
revenue, or another downstream KPI fell, classify it first as downstream quality
deterioration. Investigate audience, inventory, placement, cohort concentration,
tracking, and mix shifts; do not label it a budget shock.
- If acquisition and downstream quality both worsen, keep creative fatigue,
targeting saturation, competition, and structural changes in the hypothesis set.
Never invent a causal mechanism from aggregate metrics. State the breakdown needed to
confirm it, such as placement, device, audience, search term, keyword/match type, geo,
frequency, or auction diagnostics.
## 3. Decide rollback, containment, or pause
Prefer a controlled rollback/recovery test when all are true:
- a dated intervention plausibly precedes the mature deterioration;
- a prior configuration is known and operationally restorable;
- that prior phase has credible evidence, not merely one good day;
- the rollback does not destroy a currently verified child winner or introduce a
broader structural risk.
Rollback is not recovery. If the prior configuration also missed current KPI, label
the rollback `止损/风险收敛`, not `恢复到有效状态`. Compare it against both the damaged
phase and the current KPI contract.
During the recovery test, hold other controllable variables fixed. Use the operator's
configured maturity/sample rule. If none exists, a small run of multiple mature
cohorts may serve as a provisional checkpoint, followed by a complete recent-window
validation; clearly label this as a default evidence rule, not a universal truth.
Define exits in advance:
- **Recover:** multiple mature cohorts return to target without a guardrail breach;
- **Continue cautiously:** directional improvement exists but sample is insufficient;
- **Escalate/retest:** results remain poor or mixed and a clean hypothesis is testable;
- **Pause/reduce:** persistent mature failure continues with meaningful exposure and
no unresolved data issue.
Avoid daily edits during an observation window unless a hard loss, compliance, or
tracking guardrail is breached.
## 4. Validate a new or apparent “black horse”
Early averages can be dominated by one cohort. Separate:
- launch/early cohorts;
- the standout cohort or event;
- subsequent mature cohorts;
- immature cohorts, which must not be treated as zeros.
Show how much of the retained outcomes, revenue, or other success measure comes from
the best day/segment. One strong cohort or one to two good days supports `promising`,
not `durable winner`. Require the configured minimum sample and multi-cohort
consistency before scale.
When an early winner weakens but has not accumulated persistent failure, prefer
hold/protect at a stable test level over immediate scale or pause. Specify the next
mature checkpoint and the failure condition.
## 5. Rescue a restructured Campaign or Ad Group
Do not decide `replace everything` or `abandon the Campaign` from its aggregate. Build
a comparison across:
- the prior clean control structure;
- copied/reused groups;
- new word packs, audiences, creatives, or match types;
- current child winners worth protecting.
Measure whether weakness is broad or concentrated and whether the decline started
before or after each structure change. Historical success is context, but reassess it
against the current KPI contract; a phase that met acquisition KPI but failed
downstream quality is not an all-KPI control.
A clean rescue design should:
1. preserve one interpretable control;
2. isolate one material hypothesis per test group;
3. reduce duplicate/overlapping audiences, keywords, or creatives where evidence
supports overlap risk;
4. protect verified child winners from being reset or drowned by the test;
5. use mature, predeclared success and stop conditions.
Do not recommend swapping a whole word pack without keyword/search-term and match-type
evidence when those are the real levers. Request deeper dimensions only when they can
change the decision.
## 6. Report the change decision
For an explicitly requested named-Ad follow-up, the optional presentation template is:
```text
<Ad名称>近7日专项判断
近7日消耗 | 安装 / 成熟结果 | 主要KPI | 下游KPI | 达标天数
趋势:<量级、KPI和变更前后走势>
结论:<保持、回退、低量观察、制作新变体、暂停或扩量判断>
```
Populate it only from the current source and confirmed context. This is a follow-up
template, not a standard daily-brief section. Never preserve an example Ad name or
example metrics as default content.
For each change-related recommendation include:
- entity path and stable ID;
- declared or inferred change and timestamp;
- pre-change, transition, and post-change mature evidence;
- observed fact versus causal hypothesis;
- action state: rollback/recovery, hold/protect, clean-control retest,
pause/reduce, or scale candidate;
- variables to hold fixed;
- pacing impact, including the spend gap created by rollback/pause;
- next mature review point and explicit recover/escalate/exit condition.
Never claim the intervention was executed. This skill remains analysis-only.
@@ -0,0 +1,221 @@
# Daily campaign triage decision framework
Read this reference before producing a hierarchical intervention and scale queue.
## 1. Construct comparable windows
For each KPI, let `D` be that KPI's latest mature date:
- analysis day: `D`;
- recent 7D: `D-6..D` using seven mature reporting days;
- previous 7D: the seven mature reporting days immediately before recent 7D;
- 14D context: both complete windows together.
If dates are missing, do not quietly substitute seven non-contiguous rows for seven
calendar/reporting days. Explain the coverage and downgrade confidence. Compare only
the same metric definition, currency, timezone, attribution window, entity scope, and
target regime.
For ratio KPIs, aggregate components:
- CTR = total clicks / total impressions
- CVR = total conversions / configured denominator
- CPI/CPA = total spend / total attributed outcomes
- ROAS = total attributed revenue / total spend
- retention = total retained eligible users / total eligible cohort
Use the user's confirmed formula when it differs.
## 2. Benchmark hierarchy
Use only confirmed business targets for pass/fail decisions. Apply the narrowest
confirmed target whose scope matches the entity, for example:
1. Ad- or Ad Group-specific target or hard guardrail;
2. Campaign-specific target or hard guardrail;
3. confirmed target for the matching product/objective/geo/account;
4. confirmed account-wide target.
Historical Campaign, peer, or account performance may explain direction and context,
but is not a business target. Without a confirmed target, label findings
`无业务 KPI,仅分析趋势` and do not place an entity in intervention or scale buckets
solely from relative performance.
## 3. Diagnose the hierarchy before choosing an action
Use this sequence when the report contains the dimensions:
1. **Portfolio/account:** quantify daily spend attainment, total risk/opportunity, and
which Campaigns concentrate it.
2. **Campaign:** identify weak, mixed, and strong Campaign aggregates, then measure how
performance is distributed across their Ad Groups and Ads.
3. **Ad Group:** decide whether the issue is shared across material child Ads or
concentrated in a few Ads.
4. **Ad:** classify concrete losers, uncertain Ads, and durable winners.
Campaign averages are routing evidence. They are not sufficient for a Campaign-pause
recommendation when material child Ads disagree. Quantify concentration with spend,
outcome, and retained/revenue shares where possible.
Prefer the lowest entity whose evidence supports the proposed lever. If the platform
cannot change budget or status at Ad level, keep the Ad diagnosis but express the
human action at the lowest supported parent level and explain the constraint.
## 4. Classify evidence at each relevant entity
Start with the primary KPI and then apply secondary KPI, delivery, and data-quality
modifiers. Apply these classifications independently to Campaign, Ad Group, and Ad;
do not inherit a parent's verdict without computing the child's evidence.
### Underperformance
- **Likely one-day noise:** `D` misses target, but recent 7D still meets it or remains
stable, same-direction misses are not a majority, and no material risk is worsening.
- **Mixed/emerging risk:** `D` and recent 7D disagree, multiple KPIs conflict, or the
recent window deteriorates without enough consistent evidence.
- **Persistent underperformance:** recent 7D misses target, at least 4 of 7 mature
days miss in the same direction, and previous 7D does not show credible recovery.
- **Sharp deterioration:** recent 7D materially worsens versus previous 7D while
spend is stable or rising, even when the 14D aggregate hides the turn.
`4 of 7` is an evidence-consistency default, not a KPI. Replace it when the user has
configured a different persistence rule.
### Good performance
- **One-day spike:** `D` looks strong but recent 7D, sample, or concentration does not
support it.
- **Improving candidate:** recent 7D improves and approaches or clears target, but
consistency, maturity, or delivery headroom is not proven.
- **Durable winner:** recent 7D meets target, at least 4 of 7 mature days support it,
previous 7D is not contradictory, sample is sufficient, no hard guardrail is
breached, and results are not dominated by one event or segment.
## 5. Handle daily spend and pacing
At the configured scope calculate:
- attainment = actual spend / target spend;
- shortfall = max(target spend - actual spend, 0);
- overage = max(actual spend - target spend, 0).
Label whether the spend target is a minimum, desired level, cap, or range. When spend
is below a minimum/desired target, identify how much credible capacity exists among
durable winners. Never solve a pacing gap by scaling entities that fail efficiency,
quality, compliance, or data guardrails. If verified winner capacity is smaller than
the gap, report the unfilled remainder and, when appropriate, propose bounded
exploration rather than forced scale.
When spend meets or exceeds target, scaling one winner may require reallocating from
losers rather than increasing portfolio spend. State that distinction.
## 6. Apply decision modifiers
Check every candidate for:
- spend exposure and estimated avoidable loss/opportunity;
- minimum sample and cohort maturity;
- launch age and recent budget, bid, creative, targeting, status, or tracking changes;
- pacing, budget fill, learning limits, and delivery headroom;
- funnel conflict, such as cheap acquisition with poor retention or weak ROAS;
- concentration in a day, creative, placement, geo, device, or large revenue event;
- possible creative fatigue supported by frequency/CPM/CTR/CVR evidence;
- portfolio effects, shared budgets, audience overlap, or cannibalization;
- data integrity and attribution lag;
- prior intervention outcomes when available.
Phrase unsupported causal explanations as hypotheses. A recent intervention normally
needs time to mature; extreme ongoing exposure can still justify urgent review.
When a recent change, scale event, restructuring, former winner, or rescue question is
material to the verdict, apply the phase-aware method in
[change-response.md](change-response.md) before choosing the action.
For the daily brief, do not mix new and mature entities in one leaderboard. Apply the
two-pool method in [learning-stable-ranking.md](learning-stable-ranking.md).
## 7. Choose the intervention state
Use the smallest state that matches the evidence:
- **Pause/reduce:** persistent mature failure with adequate exposure and no credible
recent-change or data-lag explanation.
- **Rollback/recovery:** deterioration follows a known intervention and a credible
prior control can be restored without sacrificing protected winners.
- **Hold/protect:** evidence is promising or mixed, but changing variables now would
destroy the ability to learn; keep the entity stable until the next mature check.
- **Clean-control retest:** structure or traffic variables are entangled; preserve a
control and isolate one hypothesis per test cell.
- **Scale candidate:** durable winner evidence survives maturity, concentration, and
downstream guardrail checks.
Rollback, hold, and retest are explicit actions, not vague observation. State which
variables stay fixed, the mature observation window, and the exit condition.
### Pause/reduction candidate
Require persistent mature Ad-level underperformance, meaningful spend exposure,
adequate confirmed sample, and no unresolved attribution, tracking, or recent-change
explanation. A single bad day or low-sample zero outcome belongs in observation.
When only some Ads fail, protect the good Ads and recommend intervention on the named
losers. Move to Ad Group only when most material Ads share the problem or the lever is
available only there.
### Campaign-wide pause candidate
Treat this as exceptional. Require one of:
- broad persistent failure across the Campaign's material Ad Groups and Ads;
- a Campaign-level structural constraint such as wrong objective/targeting, compliance
risk, broken tracking, or unsafe delivery;
- no durable child winner worth protecting and material ongoing loss.
Show the child-level evidence and spend coverage. Do not recommend Campaign pause from
the aggregate alone.
### Scale candidate
Require durable mature Ad-level KPI performance, adequate confirmed sample, at least
the configured consistency rule, no hard downstream guardrail breach, no severe result
concentration, and plausible delivery headroom. A good day or a good Campaign average
does not qualify an Ad.
Keep the recommendation platform-neutral. If direct Ad budget scaling is unsupported,
suggest the applicable controlled lever—parent allocation, bid/target adjustment,
duplication/test, or creative expansion—without claiming execution. Do not invent a
scale percentage when no growth rule is configured.
## 8. Prioritize the human queue
Use three explicit judgments instead of a fake precise score:
- **Impact:** avoidable loss/opportunity or share of spend affected.
- **Confidence:** target validity, maturity, sample, consistency, and data quality.
- **Urgency:** whether delay increases loss or blocks safe interpretation.
Suggested priorities:
- **P0:** tracking, data, or delivery fault affecting material live spend.
- **P1:** persistent/high-confidence Ad or Ad Group underperformance, rapid
deterioration with material exposure, or broad child-level failure supporting a
Campaign review.
- **P2:** mixed evidence, pending intervention, low sample, or a durable Ad winner
that needs an operator decision.
- **P3:** no material action; summarize only.
Do not recommend a generic pause because one KPI missed. Match the human review to the
entity and evidence: verify tracking, inspect a breakdown, protect a good Ad,
cap/reduce/pause a named loser, restore a control, refresh creative, adjust
targeting/bid, reallocate, or run a controlled scale test. Quantify how a proposed
pause or rollback changes the pacing gap, and do not nominate an unstable early winner
to compensate. These remain recommendations, not executed actions.
## 9. Recommended row shape
| Priority | Entity level | Campaign > Ad Group > Ad | State | D vs target | Recent 7D | Previous 7D | Change / pre-post | Spend/sample | Evidence and risk | Human action | Recheck/exit condition |
|---|---|---|---|---|---|---|---|---|---|---|---|
Each row should make clear what changed, against which confirmed target, over which
exact mature window, with how much spend/sample, why this entity—not merely its
parent—owns the action, and why the next review follows.
@@ -0,0 +1,58 @@
# Delivery profile
Use this portable default when the user has not supplied an installation-level
delivery preference. A user-supplied directory and filename convention always wins.
```yaml
output_format: docx
output_directory: ./output
filename_pattern: "{analysis_date}-campaign-triage.docx"
chat_behavior: completion_and_file_link_only
```
Rules:
- `analysis_date` is the delivery/report date analyzed in the brief. Do not substitute
the file-generation date when they differ.
- Create the output directory when absent. Do not overwrite a same-named report until
the new DOCX has passed structural and visual verification; build in a temporary
path and replace only at final delivery.
- Preserve the four-section format from `output-standard.md` inside the document.
- Use a compact professional business-brief design with readable Chinese fonts,
deliberate table widths, repeated headers for multi-page tables, and explicit
maturity dates.
- Do not place a `核心判断` summary between the title/date block and section 1.
- Keep the visual treatment close to a restrained business brief. Do not use
full-width section bands, large color blocks, forced section page breaks, or repeated
callout boxes; these create excessive pagination when text wraps.
- Use at most three visible design colors across the document. Prefer one dark blue
for headings, one light blue-gray for table headers or thin rules, and neutral gray
for secondary text. Do not assign a separate color to every status or section.
- Separate hierarchy mainly through typography and spacing: numbered section heading,
smaller subsection heading, evidence table, and concise labeled interpretation or
action. A thin rule beneath a major heading is acceptable; shaded paragraphs are not
the default.
- Keep the action-oriented expression from the accepted direct version. Use short,
plain labeled lines such as `待排查`, `趋势`, `结论`, `预算影响`,
`继续观察`, `去重原则`, `预算情景`, `3 / 5 / 7天`, `筛选结果`, and `次优参考`
where applicable. Bold only the label; do not turn each line into a colored card.
- Do not include `Campaign层级`, a Campaign ranking table, or `层级判断`. The daily
brief is Ad-focused; parent names appear only when needed to identify an Ad.
- Do not carry a previously discussed named-Ad专项判断 into later daily briefs. Use
named-Ad drilldowns only for an explicit follow-up request; retain the structure as
a template, never the old entity name or historical values.
- Do not mechanically insert a separate `结论` paragraph before every table. Let the
table present comparable data, then place the nearest decision immediately beside
it. In the action section, let the action table lead and follow it with quantified
budget impact and observation instructions.
- Use prose for short conclusions and tables only for genuinely comparable evidence.
Keep tables able to flow naturally across pages with repeated headers; do not force
a new page merely to keep an entire table or section together.
- Render every generated DOCX to page images and inspect all pages when a compatible
renderer is available. If the renderer is unavailable, perform structural OOXML
checks and disclose that visual QA could not run.
- Do not paste the complete brief into chat after successful file delivery unless the
user explicitly asks for an inline copy.
- Never commit user reports, generated briefs, absolute home-directory paths, account
names, or installation-specific KPI into the reusable Skill. Store local overrides
outside the repository or in an ignored `delivery-profile.local.md` file.
@@ -0,0 +1,139 @@
# Input and KPI contract
Use this reference when a report is new, its schema is unfamiliar, or KPI maturity
and targets have not already been confirmed.
## 1. Identify the reporting grain
Determine whether each row represents a date, account, Campaign, Ad Group, Ad,
creative, country, placement, or a combination. Preserve the full parent-child path.
Do not aggregate rows across accounts, currencies, timezones, attribution definitions,
or materially different objectives without explicit normalization.
Prefer stable IDs at every available level over display names. If only names exist,
disclose collision and rename risk. Detect whether blank Campaign or Ad Group cells
mean a visually merged/continued parent; forward-fill only inside the current date and
parent block, then reset at boundaries.
Identify parent totals, subtotals, and leaf rows. Aggregate from one consistent grain,
normally the finest trustworthy Ad rows. Never sum a Campaign/Ad Group total together
with its children. Reconcile leaf totals to exported parents when both are available.
## 2. Map fields by meaning, not spelling
Map the available fields to these roles:
- dimensions: date, account, Campaign ID/name, Ad Group ID/name, Ad ID/name, status,
objective, country, platform;
- delivery: budget, daily spend/pacing target, spend, impressions, reach, clicks,
installs, conversions, revenue;
- quality/outcomes: retained users, purchases, qualified leads, downstream events;
- ratios: CPM, CPC, CPI, CPA, CTR, CVR, retention, ROAS;
- context: currency, timezone, attribution window, cohort eligibility, data watermark,
launch date, intervention timestamps, and an intervention/change log.
Column aliases vary by platform and language. Never decide that a similarly named
field is equivalent when its denominator, attribution window, or cohort definition is
unclear. Show the proposed mapping for material fields.
When numerator and denominator are present, compute the aggregate ratio from them and
use the exported ratio only as a cross-check. If only a precomputed ratio is present,
state that weighted aggregation may be impossible.
## 3. Confirm the KPI contract
For each decision-relevant KPI, capture:
| Field | Meaning |
|---|---|
| Role | Primary KPI, secondary KPI, or hard guardrail |
| Formula | Numerator, denominator, and eligible population |
| Direction | Lower-is-better, higher-is-better, or bounded range |
| Target | Business threshold and optional tolerance |
| Scope | Global, account, objective, product, geo, or Campaign-specific |
| Attribution | Click/view window and event definition |
| Maturity lag | When a cohort is complete enough to judge |
| Minimum sample | Spend, clicks, installs, conversions, or eligible cohort |
KPI targets may differ across Campaigns, Ad Groups, or Ads. Apply the narrowest
confirmed target matching the row and display which target was used.
Treat spend/pacing as its own contract:
| Field | Meaning |
|---|---|
| Scope | Portfolio/account, platform, Campaign, or Ad Group |
| Period | Daily, weekly, flight-to-date, or another pacing window |
| Target type | Minimum delivery, desired level, maximum cap, or range |
| Target value | Amount and currency |
| Allocation rule | Whether and where incremental spend may be assigned |
Compute target attainment and gap/overage at the configured scope. A minimum daily
spend goal does not override efficiency or quality guardrails. Report the numerical
shortfall, but do not automatically distribute it to weak Ads.
If any field needed for a verdict is missing, ask a concise question. A useful form is:
> 请确认主要 KPI、目标值和方向;日消耗目标的作用范围;每个滞后指标最晚成熟到哪一天;是否有最低样本门槛或分层目标?
Do not infer a business target from industry benchmarks, historical averages, file
names, source code, or another user's configuration.
## 4. Capture intervention history
When the question involves scaling, sudden deterioration, rollback, a former winner,
or a restructured Campaign, capture each known manual or automated change:
| Field | Meaning |
|---|---|
| Timestamp | Effective time in the reporting timezone |
| Entity path | Campaign, Ad Group, or Ad and stable ID |
| Change type | Budget, bid, targeting, word pack, match type, creative, status, structure, tracking, or platform automation |
| Before / after | Previous and new value or configuration |
| Reason | Operator hypothesis or system trigger, if known |
| Expected maturity | First cohort/date on which the change can be judged |
Keep budget, configured caps, and observed spend distinct. A spend plateau or jump can
suggest a delivery phase, but it does not prove that the budget changed. When the file
contains spend but no budget history, label any inferred phase boundary as an
inference and ask for the change log only if it could materially alter the action.
If there is no formal log, accept dated operator statements as declared context and
mark their provenance. Do not silently turn an approximate recollection into an exact
timestamp.
## 5. Resolve metric-specific mature dates
Let each KPI have its own latest mature date `D_metric`. Examples:
- spend, impressions, and clicks may mature on the latest complete reporting day;
- installs or purchases may require an attribution delay;
- D1 retention for an August 23 cohort cannot be treated as mature until its defined
next-day observation is complete, so its latest usable cohort may be August 22;
- D7 retention and revenue windows may lag further.
Report `D_metric` beside the KPI. Build recent and prior windows relative to that
metric's mature date. Do not compare an immature value with a mature target.
If maturity cannot be established, present the value as provisional and exclude it
from strong intervention claims.
## 6. Data-quality checks
Before interpretation, check:
- missing or duplicated entity-date rows;
- inconsistent date formats, timezones, currencies, or units;
- blanks confused with zero;
- zero delivery versus absent ingestion;
- totals mixed with detail rows;
- hierarchy blanks incorrectly treated as missing parents or forwarded across blocks;
- parent totals double-counted with Ad Group/Ad detail;
- negative or impossible counts and rates;
- spend with missing outcomes after the expected maturity lag;
- abrupt schema, tracking, attribution, or target changes;
- known intervention dates missing from the export or conflicting with delivery;
- insufficient history for complete 7D/14D comparisons.
Do not silently discard questionable rows. Quantify affected spend and Campaigns when
possible, and decide whether the issue blocks only one KPI or the whole report.
@@ -0,0 +1,127 @@
# Learning-period and stable-period ranking
Apply this two-pool method in every daily brief. The purpose is to prevent fresh tests
from being judged by mature-entity standards while still pruning obvious failures
quickly.
## 1. Define age and comparable peers
Use confirmed launch/effective dates when available. Otherwise infer launch from the
first eligible delivery date and label it as inferred.
- Use calendar age since confirmed launch to detect zero-delivery problems.
- Use delivered days and KPI-mature cohort days to judge performance.
- A lagged KPI may have fewer mature cohorts than the entity's calendar age; never
count immature values as failures.
- A major reset to creative, targeting, bid, word pack, or structure may re-enter the
affected entity into a learning cohort when it materially changes traffic.
Rank peers only within a comparable scope: same entity level, objective/KPI contract,
currency, attribution regime, and sufficiently similar launch cohort. Rank sibling Ads
launched together within their parent before comparing them portfolio-wide.
## 2. Rule one — learning-period pool
Treat a new or materially reset Ad as learning-period through the day-7 checkpoint
unless the operator configures another window.
### Day 3: early selection
Rank simultaneous tests by this ordered evidence, without inventing a composite score:
1. no hard-guardrail breach;
2. primary KPI result on mature data;
3. number of confirmed KPIs meeting target;
4. distance from target and direction of trend;
5. mature sample and result concentration;
6. delivery/budget fill and spend impact.
Actions:
- Confirmed launch with zero spend through three calendar days: label `无投放学习失败`;
inspect review/status/bid/audience/data issues and recommend pause/rebuild if no
correctable delivery block exists.
- Spend exists, every mature KPI fails, and sample is adequate: rank last and recommend
pause or reduction at day 3. If sample is inadequate, state the exact day-5 sample or
spend stop condition rather than treating missing outcomes as proof.
- Some KPIs meet target and delivery/budget is filling: keep stable through day 5–7;
specify which KPI must mature and what failure would stop the test.
- A peer clearly leads on mature KPIs: label `学习期扩量素材候选`, but do not call it a
durable winner yet.
### Day 5: confirmation
- Eliminate persistent bottom-ranked peers when mature evidence and sample support it.
- Continue mixed but improving or partially compliant entities to day 7 with variables
fixed.
- Promote consistent leaders to a bounded scale-test candidate only when downstream
guardrails are mature enough; otherwise keep the candidate label.
### Day 7: graduate or stop
- Graduate to the stable pool when the entity has adequate mature sample and an
interpretable run.
- Mark as a scale candidate when multi-day KPI support, downstream guardrails, and
headroom are credible.
- Pause/reduce or redesign persistent failures. Do not extend `learning` indefinitely
to avoid a decision.
At each checkpoint, show rank movement where earlier checkpoints are available.
## 3. Rule two — stable-period pool
Classify entities that graduated from learning or have an established run into one of
four tiers:
### S-优质
Recent 7D exceeds the confirmed KPI targets with multi-day stability, adequate sample,
and no severe concentration or recent deterioration. Rank the strongest durable Ads
first.
### A-稳定
The confirmed KPIs meet target with adequate sample and acceptable multi-day
consistency. Protect or consider bounded scaling subject to creative age and headroom.
### B-波动待优化
Results fluctuate, only some KPIs meet target, or performance hovers near thresholds,
but there is still a specific optimization or controlled-retest path. Keep, cap,
reduce, or test one bounded improvement hypothesis with an exit condition.
### C-劣质
The primary KPI or multiple core KPIs seriously or persistently fail on mature data
with meaningful exposure, or performance sharply deteriorates without credible
recovery. Treat these as risk Ads and rank by avoidable loss and confidence for
pause/reduction review.
Within each tier use the same ordered evidence as the learning pool, then use spend
impact to prioritize operator attention. Every stable-pool row must include a concrete
`决定` column.
## 4. Historical winner deterioration and new-material alert
Flag a creative/material alert when an established Ad has a credible strong historical
control but recent mature performance deteriorates materially and persistently. Show:
- total run age and historically strong window;
- exact recent 7D versus prior 7D or pre-change evidence;
- which funnel stage worsened;
- whether spend, budget, targeting, placement, or audience mix changed;
- fatigue as a hypothesis only when frequency/CPM/CTR/CVR or creative-age evidence
supports it.
Recommend preparing or testing new material when the decline is not explained by data
lag or an easily reversible traffic change. Protect any still-performing sibling Ads.
## 5. Ranking output invariants
- Show an ordinal rank and the decisive reason; do not present a fake precise score.
- Rank Ads only; do not output Campaign or Ad Group leaderboards.
- Show KPI pass count only alongside the primary KPI and hard guardrails; KPI counts
alone do not determine quality.
- State whether budget fill uses configured budget or a delivery proxy.
- Recompute rank when mature cohorts arrive; do not retroactively treat immature zeros
as valid evidence.
@@ -0,0 +1,119 @@
# Standard daily brief format
Follow this four-section structure exactly. Keep the brief decision-led, but include
exact dates, mature windows, and enough evidence to audit each action.
## 1. 是否达标
Use compact lines:
```text
1. 是否达标
- KPI 1:实际值(达标/未达标;数据日期/成熟 cohort 日期;目标)
- KPI 2:实际值(达标/未达标;数据日期/成熟 cohort 日期;目标)
- 日消耗:实际值 / 目标值,差额 xx(达标/未达标;目标类型和范围)
```
Evaluate at the confirmed portfolio/account scope unless a narrower scope was given.
Never use the same date automatically for same-day acquisition and lagged retention or
revenue KPIs. Add at most two short caveats here for material maturity or data issues.
If KPI Gate B is unsatisfied, state `无法判定` and ask one consolidated question before
producing action labels.
## 2. 问题分析
Start directly with the Ad-level learning-period and stable-period pools. Do not add a
Campaign ranking table or `Campaign层级/层级判断` subsection. Campaign and Ad Group may
appear only in an Ad identity when duplicate names require disambiguation. Keep each
decision adjacent to its evidence, using a compact labeled line rather than a separate
visual card.
### 学习期池排名
| Rank | Ad | Age / checkpoint | Spend / budget fill | Mature KPI result | Peer position | Decision |
|---|---|---|---|---|---|---|---|
Rank comparable new Ads within the same objective and launch cohort. When multiple Ads
launch together, rank them within that test cohort. Explain day-3 pruning and
day-5/day-7 observation or promotion decisions.
### 稳定期池排名
| Rank | Tier | Ad | Latest mature day | Recent 7D / prior 7D | KPI pass pattern | Stability / change | Diagnosis | Decision |
|---|---|---|---|---|---|---|---|
Use exactly these tiers:
- `S-优质`:KPI超额完成且多日稳定。
- `A-稳定`:KPI达标,样本与持续性足够。
- `B-波动待优化`:存在波动或部分KPI未达成,但仍有明确优化空间。
- `C-劣质`:KPI严重或持续不达标,属于风险项。
Rank Ads within and across tiers by primary KPI, number of KPI passes, persistence,
sample confidence, and spend impact. Every row must include a concrete `决定` such as
protect, hold, reduce, optimize/retest, pause, or replace creative, together with its
checkpoint or exit condition when observation continues.
## 3. 建议动作
Group recommendations under only the headings that have qualifying rows:
```text
3. 建议动作
- 暂停:<完整层级名称>;原因:<证据>;释放配置预算 xx。
- 下调:<完整层级名称>;表现波动,建议预算从 xx 下调至 xx;保持至 <成熟日期/样本> 后复查;退出条件:xx。
- 保持观察:<完整层级名称>;当前通过 xx/总 KPI;保持哪些变量不变;在第 5/7 天或指定成熟日复查。
- 扩量:<Ad 名称>;近 xx 个成熟日表现很好;建议采用 <可用的受控扩量方式>;回退条件:xx。
- 补量:距离日消耗目标还差 xx;现有合格扩量容量 xx;仍缺 xx,需新增 Ads <数量或“待测试预算确认”>。
- 素材预警:<Ad 名称> 已连续跑 xx 天;历史表现好但近期严重下滑;建议验证疲劳/流量变化并准备新素材。
```
Budget rules:
- `配置预算` comes only from an explicit budget field or operator-provided history.
- If only spend is available, write `预计减少/释放日消耗约 xx` and label the estimate;
never call spend a budget.
- State an exact reduction only when a configured adjustment rule or credible prior
control exists. Otherwise state the direction and the missing value needed.
- After pause/reduction recommendations, recompute the expected pacing gap.
- Estimate the number of new Ads only when a standard test budget or a defensible
observed test-spend baseline exists; otherwise request that one value.
Every observation action must include a checkpoint and exit condition. A recommendation
never implies execution.
## 4. 近7天表现好的 Ads(3–5个)
| Rank | Ad(include Campaign > Group) | Stage | Spend / sample | KPI data | Consistency | Why good | Next use |
|---|---|---|---|---|---|---|---|
Choose three to five Ads only when they genuinely qualify. Rank durable stable Ads
before learning-period candidates. A learning Ad may appear only when clearly labeled
`学习期候选`, with its checkpoint and concentration risk. Return one or two, or state
`暂无合格 Ads`, rather than pad the list with weak traffic.
Explain why each is good using confirmed KPI performance, multi-day consistency,
sample, trend, and delivery—not adjectives alone. Keep immature outcomes out of the
negative evidence.
End with `以上均为人工决策建议,未执行任何平台变更。`
## Writing rules
- Use the user's four Chinese section names and order; do not add a separate executive
summary or duplicate action table.
- Do not add a title-page or pre-section `核心判断` paragraph. Begin the report body
directly with `1. 是否达标` after the title/date block.
- Use the exact subsection titles `学习期池排名` and `稳定期池排名`; never prefix them
with `规则一` or `规则二`.
- Do not add a named-Ad `近7日专项判断` section to the standard daily brief. Insights
from prior conversation about a specific Ad belong in the reusable follow-up
template, not as hardcoded daily-report content, unless the user explicitly asks to
include that Ad's drilldown in the report.
- Use exact calendar dates and exact mature-window ranges.
- Keep each conclusion or action close to its evidence and avoid repeated colored
callout boxes. Prefer direct labels such as `预算影响` and `继续观察` over generic
analysis prose.
- Separate observed facts, inference, and unverified causes.
- Show numerator/denominator for low-sample or concentration-sensitive ratios.
- If a subsection is empty, say `无` and why.
@@ -0,0 +1,58 @@
# Script usage
The scripts are deterministic helpers. They prepare evidence and render the final
artifact; the Skill instructions still own KPI confirmation, interpretation, and
action judgment.
## 1. Normalize a report
```bash
python3 scripts/analyze_report.py report.xlsx \
--analysis-date 2026-08-25 \
--retention-lag-days 1 \
--output /tmp/report.analysis.json
```
Supported inputs are `.xlsx`, `.csv`, and `.tsv`. The analyzer uses the first sheet
by default. Use `--sheet SHEET_NAME` to select another sheet. It recognizes common
English and Chinese names for date, Campaign, Ad Group, Ad, spend, installs, and D1
retained users. If the export differs, pass a JSON mapping:
```json
{
"date": "统计日期",
"campaign": "系列",
"ad_group": "广告组",
"ad": "广告",
"spend": "花费 USD",
"installs": "安装",
"retained": "D1 用户"
}
```
The evidence JSON deliberately contains no KPI verdict or action. Ratios are always
recomputed from summed components. Retention windows end on the explicit mature date;
acquisition windows end on the analysis date.
## 2. Apply the decision framework
Read the evidence JSON, confirm the user's KPI card and pacing card, then fill a brief
JSON matching `examples/brief.example.json`. Do not mechanically classify Ads from a
single ratio. Use sample, persistence, trend, maturity, learning stage, recent changes,
and budget impact as required by the Skill.
## 3. Build the Word brief
```bash
python3 scripts/build_brief.py /tmp/brief.json \
--output ./output/2026-08-25-campaign-triage.docx
```
The renderer enforces the stable tier names and rejects forbidden daily-brief sections
such as Campaign ranking, `核心判断`, and stale named-Ad drilldowns. It uses only three
visual colors and lets tables flow naturally without forced section page breaks.
## Output safety
Write temporary analysis JSON and generated briefs outside the repository. Never add
the user's source workbook or a real advertiser name to examples, fixtures, or assets.
@@ -0,0 +1,56 @@
# VK optimization guidance
Read this reference when the source is a VK Ads export or the user wants guidance
that can be applied inside the VK advertising ecosystem.
## Translate evidence into operator levers
Keep the primary diagnosis at Ad level. Use Campaign and Ad Group only to locate the
Ad, detect shared constraints, or describe the platform lever the operator must use.
Choose the smallest justified lever:
- **Creative control:** pause a persistently poor Ad, protect a mature winner, or copy
a proven creative direction into a clean single-variable test. Change one meaningful
variable at a time: opening hook, visual sequence, message, format, or CTA.
- **Learning protection:** do not scale an Ad from one or two good days. Rank new Ads
within the same learning pool and apply the 3 / 5 / 7-day checkpoints using mature
KPI and adequate sample.
- **Budget recovery:** after a sharp post-scale decline, recommend a rollback toward
the last credible control budget and define a mature recheck window. A rollback is
a recovery experiment, not proof that performance has returned.
- **Audience or word-pack isolation:** when a previously strong combination fails
after restructuring, separate creative fatigue from audience/keyword delivery.
Recommend a clean control using the prior proven combination when its dated history
exists; otherwise label the cause as a hypothesis.
- **Controlled scaling:** scale durable winners through bounded budget increases or
new isolated copies, subject to the user's platform practice and risk limit. Never
fill a spend gap by forcing volume into an immature or deteriorating Ad.
- **Delivery troubleshooting:** zero spend is not automatically poor performance.
Ask the operator to inspect VK review/status, bid competitiveness, audience size,
schedule, budget availability, and parent-level delivery constraints when the export
cannot explain delivery.
## Action wording
Write recommendations so an operator can act without translating vague strategy:
```text
动作:回退预算并观察
对象:Creative Alpha(Group One)
依据:预算提升后,连续3个成熟日的CPI和次留成本同时恶化
建议:回到最近稳定预算;保持素材、受众和出价不变
复查:3个成熟日后;若仍未恢复则停止恢复测试
```
Every action should identify the Ad, evidence window, proposed lever, controlled
variables, recheck date or sample, and exit condition. If the export has no budget,
bid, status, audience, or change-history fields, state that limitation instead of
inventing the exact VK setting.
## Safety boundary
This Skill provides VK-relevant decision support only. Do not claim to have changed
VK status, budget, bid, audience, word pack, or creative. If the user separately asks
for execution, require an execution-capable VK workflow and confirm the exact objects
and mutations immediately before applying them.
@@ -0,0 +1,261 @@
#!/usr/bin/env python3
"""Normalize CSV/TSV/XLSX ad reports into deterministic Ad-level evidence JSON."""
from __future__ import annotations
import argparse
import csv
import datetime as dt
import json
import math
import re
import sys
import zipfile
from collections import defaultdict
from pathlib import Path
from xml.etree import ElementTree as ET
NS = {"m": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"}
ALIASES = {
"date": ["date", "日期", "day", "report date"],
"campaign": ["campaign", "campaign name", "平台 campaign name", "external_campaign_name"],
"ad_group": ["ad group", "ad group name", "平台 ad group name", "external_ad_group_name", "adgroup"],
"ad": ["ad", "ad name", "平台 ad name", "external_ad_name", "广告名称", "素材名称"],
"spend": ["spend", "cost", "消耗", "花费", "广告花费"],
"installs": ["installs", "install", "归因安装量", "安装量", "转化数"],
"retained": ["d1 retained", "d1 retention users", "d1 留存人数", "次留人数", "次日留存人数"],
"impressions": ["impressions", "展示量", "曝光量"],
"clicks": ["clicks", "点击量", "归因点击量"],
}
def norm(value: object) -> str:
return re.sub(r"[\s_\-/]+", " ", str(value or "").strip().lower())
def col_index(reference: str) -> int:
letters = re.match(r"[A-Z]+", reference or "A").group(0)
n = 0
for char in letters:
n = n * 26 + ord(char) - 64
return n - 1
def xlsx_rows(path: Path, sheet_name: str | None = None):
with zipfile.ZipFile(path) as archive:
shared = []
if "xl/sharedStrings.xml" in archive.namelist():
root = ET.fromstring(archive.read("xl/sharedStrings.xml"))
for item in root.findall("m:si", NS):
shared.append("".join(node.text or "" for node in item.iter(f"{{{NS['m']}}}t")))
workbook = ET.fromstring(archive.read("xl/workbook.xml"))
rels_root = ET.fromstring(archive.read("xl/_rels/workbook.xml.rels"))
rels = {item.attrib["Id"]: item.attrib["Target"] for item in rels_root}
sheets = []
for sheet in workbook.find("m:sheets", NS):
target = rels[sheet.attrib[f"{{{NS['r']}}}id"]].lstrip("/")
if not target.startswith("xl/"):
target = "xl/" + target
sheets.append((sheet.attrib["name"], target))
if sheet_name:
matches = [entry for entry in sheets if entry[0] == sheet_name]
if not matches:
raise ValueError(f"Sheet not found: {sheet_name}; available: {', '.join(x[0] for x in sheets)}")
sheets = matches
name, target = sheets[0]
root = ET.fromstring(archive.read(target))
for row in root.findall(".//m:sheetData/m:row", NS):
values = {}
for cell in row.findall("m:c", NS):
idx = col_index(cell.attrib.get("r", "A1"))
kind = cell.attrib.get("t")
value_node = cell.find("m:v", NS)
inline = cell.find("m:is", NS)
raw = value_node.text if value_node is not None else ""
if kind == "s" and raw:
value = shared[int(raw)]
elif kind == "inlineStr" and inline is not None:
value = "".join(n.text or "" for n in inline.iter(f"{{{NS['m']}}}t"))
elif kind == "b":
value = raw == "1"
else:
value = raw
values[idx] = value
if values:
yield name, [values.get(i, "") for i in range(max(values) + 1)]
def tabular_rows(path: Path, sheet: str | None = None):
suffix = path.suffix.lower()
if suffix == ".xlsx":
iterator = xlsx_rows(path, sheet)
try:
sheet_name, headers = next(iterator)
except StopIteration:
raise ValueError("Workbook contains no rows")
for _, row in iterator:
yield sheet_name, headers, row
return
delimiter = "\t" if suffix == ".tsv" else ","
with path.open("r", encoding="utf-8-sig", newline="") as handle:
reader = csv.reader(handle, delimiter=delimiter)
try:
headers = next(reader)
except StopIteration:
raise ValueError("File contains no rows")
for row in reader:
yield path.name, headers, row
def resolve_columns(headers: list[str], mapping: dict[str, str] | None = None) -> dict[str, int]:
normalized = {norm(header): i for i, header in enumerate(headers)}
result = {}
for field, aliases in ALIASES.items():
requested = (mapping or {}).get(field)
candidates = [requested] if requested else aliases
for candidate in candidates:
if norm(candidate) in normalized:
result[field] = normalized[norm(candidate)]
break
missing = [field for field in ("date", "ad", "spend", "installs") if field not in result]
if missing:
raise ValueError(f"Missing required columns: {', '.join(missing)}. Headers: {headers}")
return result
def number(value: object) -> float:
text = str(value or "").strip().replace(",", "").replace("$", "").replace("¥", "")
if not text or text.lower() in {"nan", "none", "null", "-"}:
return 0.0
if text.endswith("%"):
return float(text[:-1]) / 100
return float(text)
def date_value(value: object) -> dt.date:
text = str(value or "").strip()
if re.fullmatch(r"\d+(\.\d+)?", text):
serial = int(float(text))
if serial > 1000:
return dt.date(1899, 12, 30) + dt.timedelta(days=serial)
for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%Y.%m.%d", "%m/%d/%Y", "%d/%m/%Y"):
try:
return dt.datetime.strptime(text[:10], fmt).date()
except ValueError:
pass
try:
return dt.datetime.fromisoformat(text.replace("Z", "+00:00")).date()
except ValueError as exc:
raise ValueError(f"Unrecognized date: {text!r}") from exc
def ratio(numerator: float, denominator: float):
return None if denominator == 0 else numerator / denominator
def rounded(value):
if value is None:
return None
return round(value, 6)
def metrics(rows):
spend = sum(r["spend"] for r in rows)
installs = sum(r["installs"] for r in rows)
retained = sum(r["retained"] for r in rows)
return {
"spend": rounded(spend), "installs": rounded(installs), "retained": rounded(retained),
"cpi": rounded(ratio(spend, installs)),
"retention_rate": rounded(ratio(retained, installs)),
"retention_cost": rounded(ratio(spend, retained)),
"delivery_days": len({r["date"] for r in rows if r["spend"] > 0}),
}
def in_window(rows, end: dt.date, days: int):
start = end - dt.timedelta(days=days - 1)
return [row for row in rows if start <= row["date"] <= end]
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input", type=Path)
parser.add_argument("--analysis-date", help="YYYY-MM-DD; defaults to latest source date")
parser.add_argument("--retention-lag-days", type=int, default=1)
parser.add_argument("--sheet")
parser.add_argument("--mapping", type=Path, help="JSON mapping canonical fields to source headers")
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args(argv)
if not args.input.exists():
parser.error(f"Input not found: {args.input}")
mapping = json.loads(args.mapping.read_text(encoding="utf-8")) if args.mapping else None
records, source_headers, source_sheet = [], None, None
for source_sheet, source_headers, row in tabular_rows(args.input, args.sheet):
columns = resolve_columns(source_headers, mapping)
def get(field):
idx = columns.get(field)
return row[idx] if idx is not None and idx < len(row) else ""
if not any(str(v).strip() for v in row):
continue
ad = str(get("ad")).strip()
if not ad:
continue
records.append({
"date": date_value(get("date")), "campaign": str(get("campaign")).strip(),
"ad_group": str(get("ad_group")).strip(), "ad": ad,
"spend": number(get("spend")), "installs": number(get("installs")),
"retained": number(get("retained")), "impressions": number(get("impressions")),
"clicks": number(get("clicks")),
})
if not records:
raise ValueError("No usable Ad-level records found")
latest = max(row["date"] for row in records)
analysis_date = dt.date.fromisoformat(args.analysis_date) if args.analysis_date else latest
mature_date = analysis_date - dt.timedelta(days=args.retention_lag_days)
by_ad = defaultdict(list)
for row in records:
by_ad[(row["campaign"], row["ad_group"], row["ad"])].append(row)
ads = []
for (campaign, ad_group, ad), rows in by_ad.items():
all_dates = [r["date"] for r in rows]
acquisition_7 = in_window(rows, analysis_date, 7)
retention_7 = in_window(rows, mature_date, 7)
previous_7 = in_window(rows, mature_date - dt.timedelta(days=7), 7)
ads.append({
"ad": ad, "ad_group_context": ad_group or None, "campaign_context": campaign or None,
"first_date": min(all_dates).isoformat(), "last_date": max(all_dates).isoformat(),
"age_days_at_analysis": (analysis_date - min(all_dates)).days + 1,
"analysis_day": metrics([r for r in rows if r["date"] == analysis_date]),
"recent_7_acquisition": metrics(acquisition_7),
"recent_7_mature_retention": metrics(retention_7),
"previous_7_mature_retention": metrics(previous_7),
"recent_14_mature_retention": metrics(in_window(rows, mature_date, 14)),
})
ads.sort(key=lambda x: (-x["recent_7_acquisition"]["spend"], x["ad"]))
result = {
"contract_version": "1.0", "source": {"file": args.input.name, "sheet": source_sheet,
"row_count": len(records), "columns": source_headers},
"dates": {"analysis_date": analysis_date.isoformat(), "latest_source_date": latest.isoformat(),
"retention_mature_date": mature_date.isoformat(), "retention_lag_days": args.retention_lag_days},
"portfolio": {"analysis_day": metrics([r for r in records if r["date"] == analysis_date]),
"recent_7_acquisition": metrics(in_window(records, analysis_date, 7)),
"recent_7_mature_retention": metrics(in_window(records, mature_date, 7))},
"ads": ads,
"notes": ["Ratios are recomputed from summed numerators and denominators.",
"Campaign and Ad Group are identity context only; no parent-level ranking is produced.",
"This file contains evidence only. KPI verdicts and actions require the Skill decision gates."],
}
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Wrote {args.output} ({len(ads)} Ads; analysis date {analysis_date})")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (ValueError, zipfile.BadZipFile, json.JSONDecodeError) as exc:
print(f"error: {exc}", file=sys.stderr)
raise SystemExit(2)
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""Build a restrained four-section DOCX from the Skill's brief JSON contract."""
from __future__ import annotations
import argparse
import datetime as dt
import json
import sys
from pathlib import Path
from xml.sax.saxutils import escape
from zipfile import ZIP_DEFLATED, ZipFile
W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
FORBIDDEN = ("核心判断", "规则一", "规则二", "Campaign层级", "层级判断")
def run(text, bold=False, color="1F2937", size=20):
props = (f'<w:rFonts w:ascii="Arial" w:hAnsi="Arial" w:eastAsia="Microsoft YaHei"/>'
f'<w:color w:val="{color}"/><w:sz w:val="{size}"/><w:szCs w:val="{size}"/>'
+ ("<w:b/><w:bCs/>" if bold else ""))
return f'<w:r><w:rPr>{props}</w:rPr><w:t xml:space="preserve">{escape(str(text))}</w:t></w:r>'
def para(text="", bold=False, color="1F2937", size=20, align=None, before=0,
after=90, keep_next=False, border=False):
ppr = [f'<w:spacing w:before="{before}" w:after="{after}" w:line="276" w:lineRule="auto"/>',
"<w:keepLines/>"]
if align:
ppr.append(f'<w:jc w:val="{align}"/>')
if keep_next:
ppr.append("<w:keepNext/>")
if border:
ppr.append('<w:pBdr><w:bottom w:val="single" w:sz="8" w:space="5" w:color="2E74B5"/></w:pBdr>')
return f'<w:p><w:pPr>{"".join(ppr)}</w:pPr>{run(text, bold, color, size)}</w:p>'
def rich(label, text):
props = '<w:spacing w:before="20" w:after="110" w:line="276" w:lineRule="auto"/>'
return f'<w:p><w:pPr>{props}</w:pPr>{run(label + ":", True, "0B2545", 20)}{run(text)}</w:p>'
def heading(text, major=True):
return para(text, True, "0B2545" if major else "2E74B5", 28 if major else 23,
before=240 if major else 150, after=110, keep_next=True, border=major)
def cell(value, width, header=False, align="left"):
fill = '<w:shd w:val="clear" w:fill="E8EEF5"/>' if header else ""
tcpr = (f'<w:tcW w:w="{width}" w:type="dxa"/>{fill}<w:vAlign w:val="center"/>'
'<w:tcMar><w:top w:w="85" w:type="dxa"/><w:left w:w="100" w:type="dxa"/>'
'<w:bottom w:w="85" w:type="dxa"/><w:right w:w="100" w:type="dxa"/></w:tcMar>')
return f'<w:tc><w:tcPr>{tcpr}</w:tcPr>{para(value, header, "1F3A5F" if header else "1F2937", 17, align, after=0)}</w:tc>'
def table(headers, rows, widths, aligns=None):
if sum(widths) != 9360 or len(headers) != len(widths):
raise ValueError("Every table width list must match headers and sum to 9360")
aligns = aligns or ["left"] * len(widths)
borders = "".join(f'<w:{edge} w:val="single" w:sz="4" w:color="CBD5E1"/>'
for edge in ("top", "left", "bottom", "right", "insideH", "insideV"))
props = f'<w:tblW w:w="9360" w:type="dxa"/><w:tblLayout w:type="fixed"/><w:tblBorders>{borders}</w:tblBorders>'
grid = "".join(f'<w:gridCol w:w="{width}"/>' for width in widths)
header = "".join(cell(value, width, True, aligns[i]) for i, (value, width) in enumerate(zip(headers, widths)))
xml_rows = [f'<w:tr><w:trPr><w:tblHeader/></w:trPr>{header}</w:tr>']
for row in rows:
normalized = list(row) + [""] * (len(headers) - len(row))
cells = "".join(cell(normalized[i], width, False, aligns[i]) for i, width in enumerate(widths))
xml_rows.append(f'<w:tr><w:trPr><w:cantSplit/></w:trPr>{cells}</w:tr>')
return f'<w:tbl><w:tblPr>{props}</w:tblPr><w:tblGrid>{grid}</w:tblGrid>{"".join(xml_rows)}</w:tbl>'
def rows(items, fields):
return [[str(item.get(field, "")) for field in fields] for item in items]
def validate(data):
required = ("analysis_date", "portfolio_kpis", "learning_pool", "stable_pool", "actions", "good_ads")
missing = [key for key in required if key not in data]
if missing:
raise ValueError("Missing brief keys: " + ", ".join(missing))
payload = json.dumps(data, ensure_ascii=False)
found = [term for term in FORBIDDEN if term in payload]
if found:
raise ValueError("Forbidden daily-brief content: " + ", ".join(found))
for item in data["stable_pool"]:
tier = str(item.get("tier", ""))
if tier and not any(tier.startswith(prefix) for prefix in ("S-优质", "A-稳定", "B-波动待优化", "C-劣质")):
raise ValueError(f"Invalid stable tier: {tier}")
def document_body(data):
title = data.get("title") or f'{data["analysis_date"]} Campaign Triage'
label = data.get("account_label", "广告投放日报")
mature = data.get("retention_mature_date")
date_line = f'投放数据日:{data["analysis_date"]}' + (f' | D1次留成熟日:{mature}' if mature else "")
body = [para(label, True, "64748B", 18, after=55), para(title, True, "0B2545", 34, after=60),
para(date_line, color="475569", size=19, after=160, border=True)]
body += [heading("1. 是否达标"), table(
["指标", "实际值", "目标", "结论", "口径"],
rows(data["portfolio_kpis"], ["metric", "actual", "target", "verdict", "basis"]),
[1450, 1300, 1300, 1200, 4110], ["left", "center", "center", "center", "left"])]
if data.get("kpi_note"):
body.append(rich("口径说明", data["kpi_note"]))
body += [heading("2. 问题分析"), heading("学习期池排名", False)]
learning = data["learning_pool"]
if learning:
body.append(table(["排名", "Ad", "阶段", "成熟表现", "决定"],
rows(learning, ["rank", "ad", "stage", "evidence", "decision"]),
[650, 2300, 950, 3400, 2060], ["center", "left", "center", "left", "left"]))
else:
body.append(para("无", color="475569"))
body.append(heading("稳定期池排名", False))
stable = data["stable_pool"]
if stable:
body.append(table(["排名", "分级", "Ad", "判断", "决定"],
rows(stable, ["rank", "tier", "ad", "evidence", "decision"]),
[650, 1550, 2050, 3000, 2110], ["center", "center", "left", "left", "left"]))
else:
body.append(para("无", color="475569"))
body.append(heading("3. 建议动作"))
actions = data["actions"]
if actions:
body.append(table(["动作", "对象", "证据", "预算影响"],
rows(actions, ["action", "ad", "evidence", "budget_impact"]),
[1100, 2500, 4260, 1500], ["center", "left", "left", "center"]))
else:
body.append(para("无", color="475569"))
for note in data.get("action_notes", []):
body.append(rich(note.get("label", "说明"), note.get("text", "")))
body.append(heading("4. 近7天表现好的 Ads(3–5个)"))
good = data["good_ads"]
if good:
body.append(table(["排名", "Ad", "近7日数据", "为什么好", "下一步"],
rows(good, ["rank", "ad", "data", "why_good", "next_step"]),
[650, 1900, 2900, 2400, 1510], ["center", "left", "left", "left", "left"]))
else:
body.append(para("无符合标准的成熟 Ad;不降低标准凑数。", color="475569"))
if data.get("data_note"):
body += [heading("数据与决策说明", False), para(data["data_note"], color="475569", size=18)]
body.append(para("以上均为人工决策建议,未执行任何平台变更。", True, "0B2545", 19, before=60, after=0, border=True))
return "".join(body)
def write_docx(data, output: Path):
validate(data)
body = document_body(data)
sect = '<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="1080" w:right="1440" w:bottom="1080" w:left="1440" w:header="720" w:footer="720"/></w:sectPr>'
document = f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="{W}"><w:body>{body}{sect}</w:body></w:document>'
styles = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="{W}">
<w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="Arial" w:hAnsi="Arial" w:eastAsia="Microsoft YaHei"/><w:sz w:val="20"/><w:szCs w:val="20"/><w:color w:val="1F2937"/></w:rPr></w:rPrDefault></w:docDefaults>
<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/></w:style></w:styles>'''
content_types = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/></Types>'''
root_rels = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/></Relationships>'''
doc_rels = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>'''
stamp = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
core = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title>{escape(data.get("title", "Daily Campaign Triage"))}</dc:title><dc:creator>daily-campaign-triage</dc:creator><dcterms:created xsi:type="dcterms:W3CDTF">{stamp}</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">{stamp}</dcterms:modified></cp:coreProperties>'''
output.parent.mkdir(parents=True, exist_ok=True)
with ZipFile(output, "w", ZIP_DEFLATED) as archive:
for name, payload in (("[Content_Types].xml", content_types), ("_rels/.rels", root_rels),
("word/document.xml", document), ("word/styles.xml", styles),
("word/_rels/document.xml.rels", doc_rels), ("docProps/core.xml", core)):
archive.writestr(name, payload)
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input", type=Path, help="Brief JSON")
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args(argv)
data = json.loads(args.input.read_text(encoding="utf-8"))
write_docx(data, args.output)
print(f"Wrote {args.output}")
if __name__ == "__main__":
try:
main()
except (ValueError, json.JSONDecodeError) as exc:
print(f"error: {exc}", file=sys.stderr)
raise SystemExit(2)
@@ -0,0 +1,12 @@
日期,平台 Campaign Name,平台 Ad Group Name,平台 Ad Name,消耗,归因安装量,D1 留存人数
2026-08-19,Campaign Demo,Group One,Creative Alpha,30,10,1
2026-08-20,Campaign Demo,Group One,Creative Alpha,32,10,1
2026-08-21,Campaign Demo,Group One,Creative Alpha,34,10,2
2026-08-22,Campaign Demo,Group One,Creative Alpha,36,10,1
2026-08-23,Campaign Demo,Group One,Creative Alpha,38,10,1
2026-08-24,Campaign Demo,Group One,Creative Alpha,40,10,2
2026-08-25,Campaign Demo,Group One,Creative Alpha,42,10,0
2026-08-22,Campaign Demo,Group Two,Creative Beta,15,3,0
2026-08-23,Campaign Demo,Group Two,Creative Beta,20,4,0
2026-08-24,Campaign Demo,Group Two,Creative Beta,25,5,1
2026-08-25,Campaign Demo,Group Two,Creative Beta,30,6,0
1 日期 平台 Campaign Name 平台 Ad Group Name 平台 Ad Name 消耗 归因安装量 D1 留存人数
2 2026-08-19 Campaign Demo Group One Creative Alpha 30 10 1
3 2026-08-20 Campaign Demo Group One Creative Alpha 32 10 1
4 2026-08-21 Campaign Demo Group One Creative Alpha 34 10 2
5 2026-08-22 Campaign Demo Group One Creative Alpha 36 10 1
6 2026-08-23 Campaign Demo Group One Creative Alpha 38 10 1
7 2026-08-24 Campaign Demo Group One Creative Alpha 40 10 2
8 2026-08-25 Campaign Demo Group One Creative Alpha 42 10 0
9 2026-08-22 Campaign Demo Group Two Creative Beta 15 3 0
10 2026-08-23 Campaign Demo Group Two Creative Beta 20 4 0
11 2026-08-24 Campaign Demo Group Two Creative Beta 25 5 1
12 2026-08-25 Campaign Demo Group Two Creative Beta 30 6 0
@@ -0,0 +1,49 @@
import json
import subprocess
import sys
import tempfile
import unittest
import zipfile
from pathlib import Path
from xml.etree import ElementTree as ET
ROOT = Path(__file__).resolve().parents[1]
class ScriptTests(unittest.TestCase):
def test_csv_analysis_separates_maturity_dates(self):
with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp) / "analysis.json"
subprocess.run([
sys.executable, str(ROOT / "scripts/analyze_report.py"),
str(ROOT / "tests/fixtures/sample-report.csv"),
"--analysis-date", "2026-08-25", "--retention-lag-days", "1",
"--output", str(output),
], check=True, capture_output=True, text=True)
data = json.loads(output.read_text(encoding="utf-8"))
self.assertEqual(data["dates"]["analysis_date"], "2026-08-25")
self.assertEqual(data["dates"]["retention_mature_date"], "2026-08-24")
self.assertEqual(len(data["ads"]), 2)
self.assertNotIn("tier", data["ads"][0])
self.assertAlmostEqual(data["portfolio"]["analysis_day"]["spend"], 72)
def test_docx_contract_and_forbidden_headings(self):
with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp) / "brief.docx"
subprocess.run([
sys.executable, str(ROOT / "scripts/build_brief.py"),
str(ROOT / "examples/brief.example.json"), "--output", str(output),
], check=True, capture_output=True, text=True)
self.assertTrue(zipfile.is_zipfile(output))
with zipfile.ZipFile(output) as archive:
root = ET.fromstring(archive.read("word/document.xml"))
text = "".join(root.itertext())
for heading in ("1. 是否达标", "2. 问题分析", "3. 建议动作", "4. 近7天表现好的 Ads"):
self.assertIn(heading, text)
for forbidden in ("核心判断", "规则一", "规则二", "Campaign层级", "层级判断"):
self.assertNotIn(forbidden, text)
self.assertIn("决定", text)
if __name__ == "__main__":
unittest.main()