sino-bc-cli 1.8.26 → 1.8.27
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 +6 -0
- package/dist/SKILL.md +10 -10
- package/dist/affordance/project-analysis.md +18 -0
- package/dist/affordance/user.md +8 -2
- package/dist/api-doc/oapi/project-analysis.md +86 -0
- package/dist/api-doc/oapi/user.md +19 -2
- package/dist/generated/api-catalog.json +779 -4
- package/dist/index.js +1 -1
- package/dist/skills/custom/biz-line-strategy-generate/SKILL.md +36 -17
- package/dist/skills/custom/biz-line-strategy-generate/references/api-flow.md +44 -22
- package/dist/skills/custom/biz-line-strategy-generate/references/four-strategies.md +18 -74
- package/dist/skills/custom/biz-line-strategy-generate/references/metrics.md +20 -32
- package/dist/skills/custom/biz-line-strategy-generate/references/output-excel.md +12 -7
- package/dist/skills/custom/biz-line-strategy-generate/references/runtime.md +41 -4
- package/dist/skills/custom/biz-line-strategy-generate/references/spec.md +176 -6
- package/dist/skills/custom/biz-line-strategy-generate/scripts/build_report.py +17 -6
- package/dist/skills/custom/biz-line-strategy-generate/scripts/cli_json.py +36 -17
- package/dist/skills/custom/biz-line-strategy-generate/scripts/fetch_data.py +44 -17
- package/dist/skills/custom/biz-line-strategy-generate/scripts/run_workflow.py +134 -37
- package/dist/skills/custom/biz-line-strategy-generate/scripts/strategy_output.py +277 -0
- package/dist/skills/custom/biz-line-strategy-generate/scripts/summarize.py +84 -146
- package/dist/skills/custom/biz-line-strategy-generate/scripts/test_database_output.py +401 -0
- package/dist/skills/custom/biz-line-strategy-generate/scripts/test_strategy_rules.py +18 -8
- package/dist/skills/custom/biz-line-strategy-generate/scripts/test_workflow.py +138 -8
- package/dist/skills/oapi/sino-bc-project-analysis/SKILL.md +13 -1
- package/dist/skills/oapi/sino-bc-project-analysis/references/analysis-workflow.md +4 -0
- package/dist/skills/oapi/sino-bc-project-analysis/references/filters-and-metrics.md +2 -2
- package/dist/skills/oapi/sino-bc-user/SKILL.md +6 -4
- package/package.json +1 -1
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
|
|
38
38
|
### Requirement: 分页与失败回包必须完整校验
|
|
39
39
|
|
|
40
|
-
每页 CLI 回包 MUST 直写 `<stem>.pN.out.json` 并检查退出码、信封和分页总数。拉数成功后 MUST 写 `<stem>.pages.json`(含 `total`、`pages`、每页 `counts`)。拉数过程 MUST 只在内存保留当前页与行数累计,MUST NOT 将各页累积为全表 list,MUST NOT 再写一份全表 `merged.json`。汇总 MUST 按 `pages.json` 顺序逐页 yield 行;`pages.json` 缺失但存在 `p2` 及之后页文件时 MUST 失败,不得只读第一页。`share_map` / `metric_value` / 覆盖日期 / 出价快照 MUST 一次扫描完成,MUST NOT 为占比或比率把全表物化成 list。新增对象识别 MUST 只为新增行保留明细,持续对象 MUST NOT 再攒成全表。开始分析前的清单校验 MUST 只核对该索引与 manifest 的 `total`/`counts`,MUST NOT
|
|
40
|
+
每页 CLI 回包 MUST 直写 `<stem>.pN.out.json` 并检查退出码、信封和分页总数。拉数成功后 MUST 写 `<stem>.pages.json`(含 `total`、`pages`、每页 `counts`)。拉数过程 MUST 只在内存保留当前页与行数累计,MUST NOT 将各页累积为全表 list,MUST NOT 再写一份全表 `merged.json`。汇总 MUST 按 `pages.json` 顺序逐页 yield 行;`pages.json` 缺失但存在 `p2` 及之后页文件时 MUST 失败,不得只读第一页。`share_map` / `metric_value` / 覆盖日期 / 出价快照 MUST 一次扫描完成,MUST NOT 为占比或比率把全表物化成 list。新增对象识别 MUST 只为新增行保留明细,持续对象 MUST NOT 再攒成全表。开始分析前的清单校验 MUST 只核对该索引与 manifest 的 `total`/`counts`,MUST NOT 为校验整表加载。广告/广告组名称片段分布 MUST 只用原始展示名,MUST NOT 把空名回退成 ID 再拆词。异常、解析失败、未到总数即遇空页、达到分页上限仍未取全,MUST 阻止该业务线生成成功报告,且 MUST NOT 写下完整 `pages.json`。CLI stderr 错误 MUST 保留到本次任务结果摘要,而非当作无数据。其他业务线 MUST 可以继续执行。
|
|
41
41
|
|
|
42
42
|
#### Scenario: 后续页失败不导出部分结果
|
|
43
43
|
- **GIVEN** 首页成功且声明仍有下一页
|
|
@@ -54,7 +54,19 @@
|
|
|
54
54
|
|
|
55
55
|
### Requirement: 指标与周期口径固定
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
统一入口 MUST 在批次开始时按执行机器当地日期解析默认周期:本期为上周一至周日,上期为上上周一至周日。MUST NOT 使用业务线返回的周期。用户指定日期 MUST 按期成对覆盖,未指定的期保持其自然周默认值;不合法日期 MUST 报错而非静默回退。查询、Excel 及数据库本期字段 MUST 使用同一解析结果。
|
|
58
|
+
|
|
59
|
+
#### Scenario: 自然周默认与业务线周期不同
|
|
60
|
+
- **GIVEN** 执行日期为 2026-09-22,业务线返回本期 2026-09-07~2026-09-13
|
|
61
|
+
- **WHEN** 用户没有指定日期
|
|
62
|
+
- **THEN** 使用本期 2026-09-14~2026-09-20、上期 2026-09-07~2026-09-13,保存本期字段与此一致
|
|
63
|
+
|
|
64
|
+
#### Scenario: 用户只覆盖本期
|
|
65
|
+
- **GIVEN** 用户提供完整本期起止日期但未提供上期
|
|
66
|
+
- **WHEN** 解析周期
|
|
67
|
+
- **THEN** 本期使用用户日期,上期仍为执行日期的上上周一至周日
|
|
68
|
+
|
|
69
|
+
分析 MUST 从同粒度基础指标先求和再计算比率,分母为零或所需指标缺失时明确不可计算。安装 CPI MUST 标注为美元消耗除以平台安装量,MUST NOT 直接改称客户激活或留存成本。缺少同币种、同转化定义或留存数据时,客户 KPI MUST 标为不可判定,MUST NOT 使用固定 CPM 阈值替代。结论 MUST 联合达标状态与趋势判断,绝对变化不超过 10% 为稳定:达标且稳定/改善为有效,达标但下滑为待观察(预警),未达标但改善为待观察(潜力),未达标且稳定/下滑为无效。成本下降、收益指标上升为改善。无可比核心指标时 MUST 待观察并说明原因。
|
|
58
70
|
|
|
59
71
|
请求周期与实际返回的数据覆盖范围 MUST 分别展示。数据未覆盖完整请求周期时 MUST 提示不完整,MUST NOT 将较短周期消耗下降直接解释为主动减预算。新增部分成效 MUST 单独聚合,MUST NOT 将业务线整体环比宣称为单一动作的因果效果。
|
|
60
72
|
|
|
@@ -70,7 +82,7 @@
|
|
|
70
82
|
|
|
71
83
|
### Requirement: 对象与明细按稳定标识统计
|
|
72
84
|
|
|
73
|
-
广告和广告组 MUST 按渠道及 ID
|
|
85
|
+
广告和广告组 MUST 按渠道及 ID 判断新增与暂停,MUST NOT 仅按名称去重。完整明细 MUST 保留同名不同 ID 的记录。两期集合差异 MUST NOT 被描述为已确认的创建或暂停操作。出价变化 MUST 按广告组日快照计算,并区分上调、下调、期内变化但首末持平;无值 MUST NOT 默认为零。国家全量与过消耗门槛的新增成效 MUST 可在明细追溯。
|
|
74
86
|
|
|
75
87
|
#### Scenario: 同名广告不可合并
|
|
76
88
|
- **GIVEN** 两个不同广告 ID 使用同一名称
|
|
@@ -84,7 +96,7 @@
|
|
|
84
96
|
|
|
85
97
|
### Requirement: 每条业务线只交付一份校验通过的最终 Excel
|
|
86
98
|
|
|
87
|
-
|
|
99
|
+
每条可生成的业务线 MUST 独立产出一份包含固定 15 列策略摘要及完整明细的 Excel。预算、广告产品、受众正文前各增加实时字典匹配的名称列,素材无标签。分析与排版 MUST 由固定脚本完成;正常流程 MUST 一次生成最终样式,MUST NOT 依赖 Agent 临时生成修订脚本或二次导入重排。文件发布前 MUST 检查 Excel 容器、两张表、列名及记录完整性。本地模式最终路径 MUST 位于待清理工作目录之外,名称 MUST 能区分业务线与请求,MUST NOT 覆盖已存在的其他文件。
|
|
88
100
|
|
|
89
101
|
#### Scenario: 并发生成不覆盖结果
|
|
90
102
|
- **GIVEN** 两条业务线在同一秒完成导出
|
|
@@ -98,7 +110,7 @@
|
|
|
98
110
|
|
|
99
111
|
### Requirement: 任务结束后按归属清理全部临时产物
|
|
100
112
|
|
|
101
|
-
每条业务线任务完成后,MUST 清理本次任务创建的全部非最终产物,包括但不限于 JSON、请求/响应、日志副本、预览图片、临时脚本、临时 Excel
|
|
113
|
+
每条业务线任务完成后,MUST 清理本次任务创建的全部非最终产物,包括但不限于 JSON、请求/响应、日志副本、预览图片、临时脚本、临时 Excel、检查文件及运行时缓存。本地成功时 MUST 先确认最终文件已发布且验证通过,再清理;数据库模式 MUST 在临时 Excel 校验并上传成功后删除该文件,再携带 storageKey 保存策略;失败时 MUST 先保存可返回的错误摘要到内存/输出,再清理其临时和不完整产物。所有任务结束后 MUST 清理本次批次共享临时目录。MUST NOT 保留额外磁盘分析报告或将已删除目录作为交付物。
|
|
102
114
|
|
|
103
115
|
清理 MUST 以本次创建的独立目录及登记文件为边界,校验规范化绝对路径及所有权,MUST NOT 按扩展名或桌面 glob 广泛扫描。MUST NOT 删除历史请求、其他并行业务线、仓库源码、技能目录、平台凭证或 CLI 全局审计日志。符号链接、Windows junction/reparse point MUST NOT 被递归跟随到边界外。存在清理失败时 MUST 明确报告残留路径和原因,不宣称已完全清理。
|
|
104
116
|
|
|
@@ -124,9 +136,167 @@
|
|
|
124
136
|
|
|
125
137
|
### Requirement: 批次结果可观察且失败互不掩盖
|
|
126
138
|
|
|
127
|
-
工作流 MUST
|
|
139
|
+
工作流 MUST 按业务线返回阶段状态、本地最终 Excel 路径或数据库 aiStrategyId/storageKey,以及未生成或失败原因,并提供阶段耗时和批次成功/失败数量。结果 MUST 区分生成失败、导出失败、清理失败;重试结束后任一业务线最终失败时批次 MUST 非零退出,同时保留其他任务的成功文件。耗时结果 MUST 通过输出或最终 Excel 内已有适当区域提供,MUST NOT 为此保留额外临时报告。用户可处理的中断 MUST 停止启动新任务并等待/终止子进程后执行边界内清理;进程被强制杀死或断电的情形不得宣称已完成清理。
|
|
128
140
|
|
|
129
141
|
#### Scenario: 混合结果完整汇报
|
|
130
142
|
- **GIVEN** 三条业务线分别成功、拉数失败、清理失败
|
|
131
143
|
- **WHEN** 批次收尾
|
|
132
144
|
- **THEN** 输出三条各自结果与阶段耗时,保留成功的最终 Excel,批次非零退出
|
|
145
|
+
|
|
146
|
+
### Requirement: 用户显式选择数据库输出
|
|
147
|
+
|
|
148
|
+
技能 MUST 默认仅交付本地 Excel,沿用桌面及显式目录选择。仅用户明确要求数据库输出时 MUST 上传 Excel 并保存 AI 策略;此模式 MUST NOT 在桌面发布文件。所有业务请求 MUST 走已有 CLI。
|
|
149
|
+
|
|
150
|
+
#### Scenario: 默认本地交付
|
|
151
|
+
- **GIVEN** 用户没有要求入库
|
|
152
|
+
- **WHEN** 完成策略生成
|
|
153
|
+
- **THEN** 返回可读取的最终 Excel 路径,不调用上传或 AI 保存接口
|
|
154
|
+
|
|
155
|
+
#### Scenario: 数据库交付
|
|
156
|
+
- **GIVEN** 用户明确要求入库且可生成策略
|
|
157
|
+
- **WHEN** 上传与保存均返回成功
|
|
158
|
+
- **THEN** 返回 AI 策略 ID、storageKey 与清理结果,不返回已删除的本地 Excel 链接
|
|
159
|
+
|
|
160
|
+
### Requirement: 动态标签与 Excel 展示一致
|
|
161
|
+
|
|
162
|
+
两种输出 MUST 通过 `biz-line +categories` 分别查询 BUDGET_STRATEGY、MEDIA_STRATEGY、AUDIENCE_STRATEGY,MUST 先查询业务线并取 adCategoryIndustryIds 第一个行业 ID,以其字符串值作为 filterCondition;缺失或首项非法 MUST 失败,不改取后续项。三类标签 MUST 在项目预检和广告数据查询前加载。同批次 MUST 按行业 ID 缓存完整三类成功字典;错误仅在当前轮缓存,重试轮开始清除错误,同行业同轮并发只加载一次,不同行业分别查询,MUST NOT 跨批次缓存或硬编码行业 ID,并保留 ID/name 对应。MUST 由 Agent 逐类分析实际策略正文、结构化事实与本行业本场景全部候选标签,按含义选择最适合者;MUST NOT 仅因同名或固定动作词匹配失败而选其他,只有具体标签均不合适时选择本场景“其他”;MUST NOT 跨场景兜底或编造 ID。字典获取失败、空列表或缺少必要兜底 MUST 明确报告。Excel MUST 在上述三类正文前分别增加标签名称列,主表合计 15 列,素材策略不增加标签。入库 MUST 传对应 ID 数组,符合 schema 的大小限制。
|
|
163
|
+
|
|
164
|
+
#### Scenario: 未匹配使用其他
|
|
165
|
+
- **GIVEN** 预算策略没有可靠匹配且预算字典返回“其他”
|
|
166
|
+
- **WHEN** 导出或入库
|
|
167
|
+
- **THEN** Excel 展示该名称,入库 budgetStrategyTags 使用同一个字典 ID
|
|
168
|
+
|
|
169
|
+
#### Scenario: 字典不可用
|
|
170
|
+
- **GIVEN** 业务线首个行业 ID 无效、接口失败或返回空列表
|
|
171
|
+
- **WHEN** 处理标签
|
|
172
|
+
- **THEN** 报告具体原因,不把空结果当作已成功匹配,不保存 READY
|
|
173
|
+
|
|
174
|
+
#### Scenario: 多业务线行业重复
|
|
175
|
+
- **GIVEN** 三条业务线的 adCategoryIndustryIds 分别为 [11,22]、[11]、[22]
|
|
176
|
+
- **WHEN** 在同一批次并发生成
|
|
177
|
+
- **THEN** 第一条仅使用行业 11,前两条共享行业 11 的三类字典,第三条使用行业 22;合计六次字典查询,不串用标签
|
|
178
|
+
|
|
179
|
+
### Requirement: 四类效果独立保存
|
|
180
|
+
|
|
181
|
+
READY MUST 包含四类策略正文、四类 learning、四类 effect、三类标签 ID 数组及 fileStorageKey。effect MUST 使用整数 1=有效、2=无效、3=待观察,与各自 learning 结论一致;待观察预警及潜力均为 3,MUST 使用达标与趋势的联合判定规则。
|
|
182
|
+
|
|
183
|
+
#### Scenario: 待观察预警
|
|
184
|
+
- **GIVEN** 某类 learning 判定为待观察(预警)
|
|
185
|
+
- **WHEN** 保存该类效果
|
|
186
|
+
- **THEN** 对应 StrategyEffect 为 3,保留其独立正文和 learning
|
|
187
|
+
|
|
188
|
+
### Requirement: 工作流保存真实生成结果
|
|
189
|
+
|
|
190
|
+
数据库模式 MUST 按真实结果保存 READY、NO_PROJECT、UNSUPPORTED_CHANNEL、NO_AD_ACCOUNT、NO_DELIVERY_DATA 或 GENERATION_FAILED;非 READY MUST 提供 remark。MUST NOT 保存 PERIOD_NOT_FINISHED、BEFORE_SUPPORTED_PERIOD、GENERATING,MUST NOT 把查询异常当作空业务数据。MUST 先取得真实业务线及合法周期;无法确定时仅报告错误而不伪造写入。不支持或未知渠道 MUST NOT 被静默丢弃后宣称成功。
|
|
191
|
+
|
|
192
|
+
#### Scenario: 未关联项目
|
|
193
|
+
- **GIVEN** 项目查询成功返回空且业务线与可保存周期已确定
|
|
194
|
+
- **WHEN** 结束数据库模式生成
|
|
195
|
+
- **THEN** 保存 NO_PROJECT 和原因,不制造空策略或 Excel
|
|
196
|
+
|
|
197
|
+
#### Scenario: 请求异常
|
|
198
|
+
- **GIVEN** 已确定可保存的业务线和周期,但投放请求失败
|
|
199
|
+
- **WHEN** 收尾
|
|
200
|
+
- **THEN** 尝试保存 GENERATION_FAILED 及原因,并分别报告生成和状态保存结果
|
|
201
|
+
|
|
202
|
+
### Requirement: 用全部关联项目上周消耗预检
|
|
203
|
+
|
|
204
|
+
数据库模式 MUST 分页获取该业务线全部关联项目并与业务线 projectIds 核对覆盖;缺少项目时 MUST 失败,不按部分项目判断全零。使用项目列表 lastWeekSpend 判断上周消耗,MUST NOT 使用 isLast7DSpend 或近七天状态。无项目时保存 NO_PROJECT;存在项目且全部 lastWeekSpend 明确为 0 时 MUST 立即保存 NO_DELIVERY_DATA 与原因,不继续广告数据查询、策略生成或上传。至少一个项目大于 0 时才继续渠道及账号校验,再按目标周期取数。缺失、非法值和查询失败 MUST NOT 当成 0;无法证实全零或存在正消耗时 MUST 报告失败。
|
|
205
|
+
|
|
206
|
+
#### Scenario: 全部项目上周为零
|
|
207
|
+
- **GIVEN** 所有页读取成功且每个关联项目 lastWeekSpend 都为 0
|
|
208
|
+
- **WHEN** 执行生成前检查
|
|
209
|
+
- **THEN** 直接保存 NO_DELIVERY_DATA,不查询广告维度及成效,也不生成空 Excel
|
|
210
|
+
|
|
211
|
+
#### Scenario: 后续页存在正消耗
|
|
212
|
+
- **GIVEN** 第一页项目消耗为 0,后续页至少一个项目 lastWeekSpend 大于 0
|
|
213
|
+
- **WHEN** 完成分页预检
|
|
214
|
+
- **THEN** 继续检查全部项目渠道与账号,不提前保存 NO_DELIVERY_DATA
|
|
215
|
+
|
|
216
|
+
#### Scenario: 消耗缺失不能冒充零
|
|
217
|
+
- **GIVEN** 项目列表无正消耗但存在 lastWeekSpend 缺失
|
|
218
|
+
- **WHEN** 执行预检
|
|
219
|
+
- **THEN** 报告无法判断并保存 GENERATION_FAILED,不保存 NO_DELIVERY_DATA
|
|
220
|
+
|
|
221
|
+
#### Scenario: 服务默认筛选遗漏关联项目
|
|
222
|
+
- **GIVEN** 分页 total 已取全,但返回 ID 未覆盖业务线 projectIds
|
|
223
|
+
- **WHEN** 尝试判断上周消耗
|
|
224
|
+
- **THEN** 报告项目覆盖不完整,不保存 NO_PROJECT 或 NO_DELIVERY_DATA
|
|
225
|
+
|
|
226
|
+
### Requirement: 上传校验清理与保存顺序可追溯
|
|
227
|
+
|
|
228
|
+
数据库模式 MUST 先生成并完整校验 Excel,再调用 `common-file +upload`,serviceName MUST 为 sino-opti-hive。MUST 验证非空 storageKey,删除本次临时 Excel,并在 `biz-line +ai-strategy-save` 的 fileStorageKey 传原值。保存结果未知 MUST NOT 自动重试或追加失败状态覆盖;上传或保存失败 MUST 报告阶段与已有 key。清理 MUST 仅针对本次拥有的文件,失败 MUST 报告残留,MUST NOT 宣称完全成功。
|
|
229
|
+
|
|
230
|
+
#### Scenario: 上传成功后保存失败
|
|
231
|
+
- **GIVEN** 已取得 storageKey 并删除临时 Excel
|
|
232
|
+
- **WHEN** AI 策略保存失败或超时
|
|
233
|
+
- **THEN** 返回 key、保存失败或结果未知,不重新上传、不自动重试、不用 GENERATION_FAILED 覆盖潜在成功记录
|
|
234
|
+
|
|
235
|
+
#### Scenario: 清理失败
|
|
236
|
+
- **GIVEN** 本次文件被占用无法删除
|
|
237
|
+
- **WHEN** 任务收尾
|
|
238
|
+
- **THEN** 返回确切残留路径和清理错误,数据库保存结果独立展示
|
|
239
|
+
|
|
240
|
+
### Requirement: 批量执行保持隔离
|
|
241
|
+
|
|
242
|
+
工作流 MUST 保持单次最多三条业务线并发、同批次 ID 去重、每条内部串行请求与分页处理。各业务线结果、上传 key、状态写入及清理 MUST 独立,失败 MUST NOT 阻塞其他业务线或掩盖其结果。
|
|
243
|
+
|
|
244
|
+
#### Scenario: 混合生成结果
|
|
245
|
+
- **GIVEN** 同批业务线分别生成成功、无账号、保存失败
|
|
246
|
+
- **WHEN** 批次结束
|
|
247
|
+
- **THEN** 分别返回三条完整结果,保存失败令批次非成功且不影响其他记录
|
|
248
|
+
|
|
249
|
+
### Requirement: 显式跳过项目消耗预检
|
|
250
|
+
|
|
251
|
+
仅用户明确要求跳过项目上周消耗判断时传 `--skip-project-spend-check`,默认不启用。该参数只跳过 `lastWeekSpend` 的全零或无正消耗门槛;仍校验项目分页完整性、关联项目、渠道、账号、指定周期真实投放数据及标签。不得因用户指定了历史周期就自动跳过。
|
|
252
|
+
|
|
253
|
+
### Requirement: 主表汇总与简洁 learning
|
|
254
|
+
|
|
255
|
+
原始 KPI MUST NOT 进入任何 sheet 或入库正文。策略 MUST 仅显示合计与分类分布,逐个对象、调价值及成效移入明细。所有展示的消耗占比 MUST 有对应金额,金额及合计消耗 MUST 有环比,零基数明确不可计算。调价、广告组和广告 MUST 展示名称片段分布与数量,不猜代码含义、不用 ID 代替名称。对外文案 MUST 使用新增、暂停、分布在。learning MUST 只有结论与说明两行,四象限固定文案见 metrics.md,未知数据为待观察。策略汇总 MUST 完整展示,不能静默截断尾部类别。
|
|
256
|
+
|
|
257
|
+
#### Scenario: 未达标但改善
|
|
258
|
+
- **GIVEN** 同口径 KPI 未达标且趋势改善超过 10%,样本足够
|
|
259
|
+
- **WHEN** 输出 learning
|
|
260
|
+
- **THEN** 仅显示“结论:待观察(潜力)”和“说明:再给一周机会”,效果码为 3
|
|
261
|
+
|
|
262
|
+
#### Scenario: 汇总变长
|
|
263
|
+
- **GIVEN** 策略同时显示金额、占比、分布,超过原 18 行限制
|
|
264
|
+
- **WHEN** 导出 Excel
|
|
265
|
+
- **THEN** 自适应列宽完整展示各类别;超过 Excel 可显示范围则失败,不截掉末段或留下半句话
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
### Requirement: 批次失败按轮重跑
|
|
269
|
+
|
|
270
|
+
工作流 MUST 首轮执行全部去重业务线,整轮结束后才重跑可重试的失败条目,每条含首次最多 3 次;成功条目 MUST NOT 重跑。每轮并发上限 3,周期与用户参数保持不变,每次使用独立临时目录。行业字典成功缓存 MUST 跨轮复用,失败缓存 MUST 在下一轮开始前清除。
|
|
271
|
+
|
|
272
|
+
#### Scenario: 部分失败后恢复
|
|
273
|
+
- **WHEN** 首轮 A、B 失败、C 成功,第二轮 A 失败、B 成功
|
|
274
|
+
- **THEN** 顺序为全首轮结束后执行 A/B,再仅执行 A 第三次;最终仅返回三条业务线结果,C 执行一次、B 两次、A 三次,各自包含 attempts 与 attempt_history
|
|
275
|
+
|
|
276
|
+
#### Scenario: 保存未知及取消
|
|
277
|
+
- **WHEN** READY 或失败状态保存请求报错/结果未知,或条目有已交付文件/清理失败/输出残留,或 worker 外层异常使副作用无法确定
|
|
278
|
+
- **THEN** MUST 保留错误及已有 key,不自动整条重跑,避免重复保存或交付
|
|
279
|
+
- **WHEN** 用户取消
|
|
280
|
+
- **THEN** MUST 停止启动新任务与重试,等待在途清理;未启动条目也出现在最终结果中
|
|
281
|
+
|
|
282
|
+
#### Scenario: 业务结果与技术错误
|
|
283
|
+
- **WHEN** 无项目等不可生成业务状态已保存成功
|
|
284
|
+
- **THEN** 视为已处理结果,不重跑
|
|
285
|
+
- **WHEN** 技术错误且 GENERATION_FAILED 已保存成功
|
|
286
|
+
- **THEN** 可进入下一轮;第三次仍失败即停止,按最终结果统计失败,不将历次尝试累加成业务线数量
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
### Requirement: 核心指标配置失败说明
|
|
290
|
+
四类 learning MUST 区分核心指标为空与无法识别,结论均为待观察、效果均为 3。未填写或纯空白时说明 MUST 为“核心指标为空”;非空但不能按现有同币种同定义规则解析时说明 MUST 为“核心指标无法识别”。此原因优先于样本不足或缺数据。只有核心指标可识别但数据不足时使用“数据不足,暂无法判断”。MUST 保持两行格式,不回显 KPI 原文。
|
|
291
|
+
|
|
292
|
+
### Requirement: 标签内容分析交接
|
|
293
|
+
标签选择 MUST 在导出/上传/保存前通过 tag_review_required 交给当前 Agent,提供本业务线三类正文、事实与对应行业完整候选标签。Agent MUST 返回真实 ID 和内容对应理由;脚本 MUST 校验三类非空选择、不跨场景、不重复、有理由、具体标签与其他互斥,Excel 与入库使用同一选择。无响应或非法响应 MUST 明确失败而非自动填其他。
|
|
294
|
+
|
|
295
|
+
#### Scenario: 标签措辞与动作词不同
|
|
296
|
+
- **GIVEN** 策略正文与一个具体标签含义吻合,但名称不等于固定动作词
|
|
297
|
+
- **WHEN** Agent 逐一比较本场景所有标签
|
|
298
|
+
- **THEN** 选择该具体标签并说明正文依据,不因词表未命中而选择其他
|
|
299
|
+
|
|
300
|
+
#### Scenario: 无合适具体标签
|
|
301
|
+
- **WHEN** 本场景全部具体标签均不适合策略正文
|
|
302
|
+
- **THEN** 仅选择真实其他 ID 并说明不匹配原因;无有效其他标签则失败
|
|
@@ -18,7 +18,7 @@ payload.json 结构:
|
|
|
18
18
|
{
|
|
19
19
|
"bizLine": "业务线",
|
|
20
20
|
"kind": "国家|广告组|出价|兴趣词|关键词|广告",
|
|
21
|
-
"action": "
|
|
21
|
+
"action": "新增|暂停|上调|下调|当期",
|
|
22
22
|
"channel": "Meta|Google|TikTok",
|
|
23
23
|
"name": "名称",
|
|
24
24
|
"extra": "补充"
|
|
@@ -27,8 +27,8 @@ payload.json 结构:
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
列固定为:组别、业务线、本期/上期、项目名称、
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
预算策略标签、预算策略内容、预算策略learning、广告产品策略标签、广告产品策略内容、广告产品策略learning、
|
|
31
|
+
受众策略标签、受众策略内容、受众策略learning、素材策略内容、素材策略learning。
|
|
32
32
|
|
|
33
33
|
一个业务线一行;本期/上期与多个项目名称都放在同一单元格内用换行分隔。
|
|
34
34
|
数据行统一套用 wrapText 样式,单元格内换行会正常展示。
|
|
@@ -51,10 +51,13 @@ COLUMNS = [
|
|
|
51
51
|
("bizLine", "业务线"),
|
|
52
52
|
("period", "本期/上期"),
|
|
53
53
|
("project", "项目名称"),
|
|
54
|
+
("budgetTags", "预算策略标签"),
|
|
54
55
|
("budgetStrategy", "预算策略内容"),
|
|
55
56
|
("budgetLearning", "预算策略learning"),
|
|
57
|
+
("productTags", "广告产品策略标签"),
|
|
56
58
|
("productStrategy", "广告产品策略内容"),
|
|
57
59
|
("productLearning", "广告产品策略learning"),
|
|
60
|
+
("audienceTags", "受众策略标签"),
|
|
58
61
|
("audienceStrategy", "受众策略内容"),
|
|
59
62
|
("audienceLearning", "受众策略learning"),
|
|
60
63
|
("creativeStrategy", "素材策略内容"),
|
|
@@ -194,6 +197,7 @@ def build_sheet_xml(header_indices, row_indices, columns, content_keys=None, mul
|
|
|
194
197
|
dimension_ref = f"A1:{col_letter(col_count - 1)}{len(row_indices) + 1}"
|
|
195
198
|
|
|
196
199
|
cols_xml_parts = []
|
|
200
|
+
widths = {}
|
|
197
201
|
for i, (key, _label) in enumerate(columns):
|
|
198
202
|
if key in content_keys:
|
|
199
203
|
width = COL_WIDTH_CONTENT
|
|
@@ -201,6 +205,13 @@ def build_sheet_xml(header_indices, row_indices, columns, content_keys=None, mul
|
|
|
201
205
|
width = COL_WIDTH_MULTILINE
|
|
202
206
|
else:
|
|
203
207
|
width = COL_WIDTH_DEFAULT
|
|
208
|
+
# 策略汇总按完整文本适度扩列,最多 26 行可在 Excel 行高上限内展示。
|
|
209
|
+
if key.endswith('Strategy'):
|
|
210
|
+
while width < 255 and any(visual_lines(str(record.get(key) or ''), width) > 26 for record in (values or [])):
|
|
211
|
+
width = min(255, width + 4)
|
|
212
|
+
if any(visual_lines(str(record.get(key) or ''), width) > 26 for record in (values or [])):
|
|
213
|
+
raise ValueError('策略汇总超过可展示范围,请压缩汇总后重试,不能静默截断')
|
|
214
|
+
widths[key] = width
|
|
204
215
|
cols_xml_parts.append(
|
|
205
216
|
f'<col min="{i + 1}" max="{i + 1}" width="{width}" customWidth="1"/>'
|
|
206
217
|
)
|
|
@@ -216,7 +227,7 @@ def build_sheet_xml(header_indices, row_indices, columns, content_keys=None, mul
|
|
|
216
227
|
height = 32
|
|
217
228
|
else:
|
|
218
229
|
record = (values or [{}] * len(row_indices))[row_num - 2]
|
|
219
|
-
lines = max((visual_lines(str(record.get(key) or ""),
|
|
230
|
+
lines = max((visual_lines(str(record.get(key) or ""), widths[key]) for key, _ in columns), default=1)
|
|
220
231
|
height = min(409, max(30, lines * 15 + 10))
|
|
221
232
|
return f'<row r="{row_num}" ht="{height}" customHeight="1">{"".join(cells)}</row>'
|
|
222
233
|
|
|
@@ -284,9 +295,9 @@ def prepare_layout(rows, details):
|
|
|
284
295
|
text = str(row.get(key) or '')
|
|
285
296
|
width = COL_WIDTH_CONTENT if key in CONTENT_KEYS else (COL_WIDTH_MULTILINE if key in MULTILINE_KEYS else COL_WIDTH_DEFAULT)
|
|
286
297
|
parts = chunks(text, width, 18)
|
|
287
|
-
if len(parts) > 1:
|
|
298
|
+
if len(parts) > 1 and not key.endswith('Strategy'):
|
|
288
299
|
details.append({'bizLine': row.get('bizLine', ''), 'kind': '完整说明', 'action': label, 'name': label, 'extra': text})
|
|
289
|
-
row[key] = parts[0]
|
|
300
|
+
row[key] = parts[0]
|
|
290
301
|
expanded = []
|
|
291
302
|
for record in details:
|
|
292
303
|
split = {key: chunks(str(record.get(key) or ''), COL_WIDTH_CONTENT if key in ('name', 'extra') else COL_WIDTH_DEFAULT, 18) for key, _ in DETAIL_COLUMNS}
|
|
@@ -279,7 +279,8 @@ def core_rule(text):
|
|
|
279
279
|
currency, suffix = (currency or '').upper(), (suffix or '').upper()
|
|
280
280
|
if (metric in ("CPI", "CPC", "CPM") and (currency or suffix in ("USD", "美元", "$")) and suffix != "%") or (metric == "ROAS" and not currency and not suffix) or (metric == "CTR" and not currency and suffix == "%"):
|
|
281
281
|
return {"metric": metric, "op": op, "target": float(target), "usable": True, "reason": "同口径 KPI"}
|
|
282
|
-
return {"metric": "CPI", "usable": False,
|
|
282
|
+
return {"metric": "CPI", "usable": False,
|
|
283
|
+
"reason": "核心指标为空" if not text else "核心指标无法识别"}
|
|
283
284
|
|
|
284
285
|
|
|
285
286
|
def rule_hit(value, rule):
|
|
@@ -304,16 +305,27 @@ def period_verdict(
|
|
|
304
305
|
kpi_usable: bool = False,
|
|
305
306
|
kpi_hit=None,
|
|
306
307
|
) -> str:
|
|
307
|
-
"""
|
|
308
|
-
if first_period:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
308
|
+
"""用达标状态与同口径趋势联合判断;缺任一依据不强行归类。"""
|
|
309
|
+
if first_period or not kpi_usable or kpi_hit is None or qoq_label == '无法对比':
|
|
310
|
+
return '待观察'
|
|
311
|
+
return dual_verdict(kpi_hit, qoq_label)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def learning_text(verdict, rule=None):
|
|
315
|
+
"""所有 learning 统一仅输出结论和业务说明,不附 KPI 原文或证据明细。"""
|
|
316
|
+
notes = {'有效': '策略有效', '待观察(预警)': '警惕,再观察一周',
|
|
317
|
+
'待观察(潜力)': '再给一周机会', '无效': '需关停',
|
|
318
|
+
'待观察': '数据不足,暂无法判断'}
|
|
319
|
+
if rule is not None and not rule.get('usable'):
|
|
320
|
+
return f'结论:待观察\n说明:{rule["reason"]}'
|
|
321
|
+
return f'结论:{verdict}\n说明:{notes[verdict]}'
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def amount_change(curr, prev):
|
|
325
|
+
"""相对上期的金额环比;零基数不伪造百分比。"""
|
|
326
|
+
if prev == 0:
|
|
327
|
+
return '环比持平' if curr == 0 else '环比不可计算(上期为0)'
|
|
328
|
+
return f'环比 {(curr - prev) / prev * 100:+.2f}%'
|
|
317
329
|
|
|
318
330
|
|
|
319
331
|
def is_ghost_enum(curr_share: float, prev_share: float) -> bool:
|
|
@@ -384,7 +396,7 @@ def budget_abs_share(
|
|
|
384
396
|
flag = ";⚠️占比降但金额升"
|
|
385
397
|
return (
|
|
386
398
|
f"{channel}:${prev_abs:,.0f} → ${curr_abs:,.0f}({abs_delta:+,.0f});"
|
|
387
|
-
f"{prev_share:.1f}% → {curr_share:.1f}%({share_delta:+.1f}
|
|
399
|
+
f"占比 {prev_share:.1f}% → {curr_share:.1f}%({share_delta:+.1f}个百分点);{amount_change(curr_abs, prev_abs)}{flag}"
|
|
388
400
|
)
|
|
389
401
|
|
|
390
402
|
|
|
@@ -412,16 +424,23 @@ _NAMING_SKIP = {
|
|
|
412
424
|
}
|
|
413
425
|
|
|
414
426
|
|
|
415
|
-
def naming_patterns(names: list[str], limit: int =
|
|
416
|
-
|
|
427
|
+
def naming_patterns(names: list[str], limit: int = 3) -> str:
|
|
428
|
+
"""按名称中可见词段统计对象数,一对象同词只计一次,不臆测内部代码含义。"""
|
|
429
|
+
counts = {}
|
|
417
430
|
for name in names:
|
|
418
|
-
|
|
431
|
+
tokens = set()
|
|
432
|
+
clean = re.sub(r'\.(mp4|mov|jpg|jpeg|png)$', '', str(name or ''), flags=re.I)
|
|
433
|
+
for tok in re.split(r'[-_\s/|]+', clean):
|
|
419
434
|
tok = tok.strip()
|
|
420
|
-
if not tok or tok.isdigit() or len(tok) < 2 or tok
|
|
435
|
+
if not tok or tok.isdigit() or len(tok) < 2 or len(tok) > 16:
|
|
436
|
+
continue
|
|
437
|
+
if tok.lower() in {'copy', 'test', '测试', 'ad', 'adgroup'} or re.fullmatch(r'[0-9a-fA-F]{8,}', tok):
|
|
421
438
|
continue
|
|
439
|
+
tokens.add(tok)
|
|
440
|
+
for tok in tokens:
|
|
422
441
|
counts[tok] = counts.get(tok, 0) + 1
|
|
423
442
|
top = sorted(counts, key=lambda k: (-counts[k], k))[:limit]
|
|
424
|
-
return
|
|
443
|
+
return '、'.join(f'{tok}({counts[tok]}个)' for tok in top) if top else '未识别分类'
|
|
425
444
|
|
|
426
445
|
|
|
427
446
|
def summarize_countries(countries: list[str]) -> tuple[str, list[str]]:
|
|
@@ -16,7 +16,8 @@ if str(SCRIPTS) not in sys.path:
|
|
|
16
16
|
sys.path.insert(0, str(SCRIPTS))
|
|
17
17
|
|
|
18
18
|
from cli_json import list_query_body, run_cli, unwrap_page, require_ok, report_progress
|
|
19
|
-
from datetime import date
|
|
19
|
+
from datetime import date, timedelta
|
|
20
|
+
from strategy_output import GenerationOutcome, precheck_projects, validate_period
|
|
20
21
|
|
|
21
22
|
# 维度/成效 size=1000 时,TikTok 出价按天明细可到 4~5 万行,旧上限 30 页会截断
|
|
22
23
|
PAGE_CAP = 80
|
|
@@ -101,6 +102,8 @@ def load_projects(req: Path, biz_line_id: int) -> tuple[list[str], dict[str, lis
|
|
|
101
102
|
req / "biz-line-project-list.out.json",
|
|
102
103
|
)
|
|
103
104
|
require_ok(pl, "关联项目")
|
|
105
|
+
if not isinstance(pl.get('data'), list):
|
|
106
|
+
raise RuntimeError('关联项目回包格式无效')
|
|
104
107
|
rows, _ = unwrap_page(pl)
|
|
105
108
|
names = [
|
|
106
109
|
p.get("projectName")
|
|
@@ -254,12 +257,31 @@ def argument_parser(batch=False):
|
|
|
254
257
|
parser.add_argument("--curr-end", default="")
|
|
255
258
|
parser.add_argument("--prev-start", default="")
|
|
256
259
|
parser.add_argument("--prev-end", default="")
|
|
260
|
+
parser.add_argument("--skip-project-spend-check", action="store_true", help="仅用户明确要求时跳过项目上周消耗预检")
|
|
257
261
|
parser.add_argument("--account-fb", action="append", default=[])
|
|
258
262
|
parser.add_argument("--account-gg", action="append", default=[])
|
|
259
263
|
parser.add_argument("--account-tt", action="append", default=[])
|
|
260
264
|
return parser
|
|
261
265
|
|
|
262
266
|
|
|
267
|
+
def resolve_periods(args, today=None):
|
|
268
|
+
"""按执行机器当地日期取两个已结束自然周,各期用户起止日期成对覆盖。"""
|
|
269
|
+
anchor = today or date.today()
|
|
270
|
+
monday = anchor - timedelta(days=anchor.weekday())
|
|
271
|
+
result = {}
|
|
272
|
+
for key, weeks in (('curr', 1), ('prev', 2)):
|
|
273
|
+
start, end = getattr(args, key + '_start'), getattr(args, key + '_end')
|
|
274
|
+
if bool(start) != bool(end):
|
|
275
|
+
raise ValueError('本期和上期起止日期必须分别成对提供')
|
|
276
|
+
if not start:
|
|
277
|
+
first = monday - timedelta(weeks=weeks)
|
|
278
|
+
start, end = first.isoformat(), (first + timedelta(days=6)).isoformat()
|
|
279
|
+
if date.fromisoformat(start) > date.fromisoformat(end):
|
|
280
|
+
raise ValueError('周期开始日期晚于结束日期')
|
|
281
|
+
result[key] = (start, end)
|
|
282
|
+
return result
|
|
283
|
+
|
|
284
|
+
|
|
263
285
|
def fetch_business(args):
|
|
264
286
|
"""一条业务线内所有调用串行;阶段失败交由统一入口收尾。"""
|
|
265
287
|
|
|
@@ -285,20 +307,25 @@ def fetch_business(args):
|
|
|
285
307
|
raise RuntimeError("返回的业务线 ID 与请求不一致")
|
|
286
308
|
(req / "biz-line.json").write_text(json.dumps(biz, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
287
309
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
prev_end =
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
310
|
+
# 查询、报表和保存共享本次解析的周期,不读取业务线返回的周期字段。
|
|
311
|
+
periods = resolve_periods(args)
|
|
312
|
+
curr_start, curr_end = periods['curr']
|
|
313
|
+
prev_start, prev_end = periods['prev']
|
|
314
|
+
|
|
315
|
+
# 只有真实业务线及合法周期才建立入库上下文,后续失败可据此保存状态。
|
|
316
|
+
if getattr(args, 'output_mode', 'excel') == 'database':
|
|
317
|
+
validate_period(curr_start, curr_end)
|
|
318
|
+
(req / 'save-context.json').write_text(json.dumps({
|
|
319
|
+
'bizLineId': biz_id, 'periodStart': curr_start, 'periodEnd': curr_end,
|
|
320
|
+
}), encoding='utf-8')
|
|
321
|
+
# 业务线已唯一确定且保存上下文已准备:先按行业加载标签,再取项目和广告数据。
|
|
322
|
+
if getattr(args, 'tag_cache', None) is not None:
|
|
323
|
+
args.tag_cache.get(biz)
|
|
324
|
+
if getattr(args, 'output_mode', 'excel') == 'database':
|
|
325
|
+
expected_ids = biz.get('projectIds')
|
|
326
|
+
if not isinstance(expected_ids, list) or any(type(value) is not int or value <= 0 for value in expected_ids):
|
|
327
|
+
raise RuntimeError('业务线缺少有效 projectIds,无法校验项目预检的完整性')
|
|
328
|
+
precheck_projects(req, biz_id, expected_ids, skip_spend_check=getattr(args, 'skip_project_spend_check', False))
|
|
302
329
|
|
|
303
330
|
require_ok(run_cli(["biz-line", "+detail"], {"id": biz_id}, req / "biz-line-detail.out.json"), "业务线详情")
|
|
304
331
|
names, discovered = load_projects(req, biz_id)
|
|
@@ -327,9 +354,9 @@ def fetch_business(args):
|
|
|
327
354
|
encoding="utf-8",
|
|
328
355
|
)
|
|
329
356
|
if not names and not any(accounts.values()):
|
|
330
|
-
raise
|
|
357
|
+
raise GenerationOutcome('NO_AD_ACCOUNT' if getattr(args, 'output_mode', 'excel') == 'database' else 'NO_PROJECT', "未绑定项目/广告账号")
|
|
331
358
|
if not any(accounts.values()):
|
|
332
|
-
raise
|
|
359
|
+
raise GenerationOutcome('NO_AD_ACCOUNT', "项目下没有 Meta/Google/TikTok 广告账号")
|
|
333
360
|
|
|
334
361
|
bl_dir = req / f"bl-{biz_id}"
|
|
335
362
|
bl_dir.mkdir(parents=True, exist_ok=True)
|