sino-bc-cli 1.8.38 → 1.8.39
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/dist/index.js +1 -1
- package/dist/skill-manifest.json +11 -8
- package/dist/skills/biz-line-strategy-generate/SKILL.md +2 -0
- package/dist/skills/biz-line-strategy-generate/references/output-excel.md +6 -0
- package/dist/skills/biz-line-strategy-generate/references/runtime.md +8 -0
- package/dist/skills/biz-line-strategy-generate/scripts/build_report.py +299 -244
- package/dist/skills/biz-line-strategy-generate/scripts/detail_store.py +82 -0
- package/dist/skills/biz-line-strategy-generate/scripts/summarize.py +22 -22
- package/dist/skills/biz-line-strategy-generate/scripts/test_detail_store.py +122 -0
- package/dist/skills/biz-line-strategy-generate/scripts/test_excel_streaming.py +223 -0
- package/dist/skills/biz-line-strategy-generate/scripts/test_pipeline.py +4 -1
- package/dist/skills/biz-line-strategy-generate/scripts/test_workflow.py +40 -27
- package/package.json +1 -1
|
@@ -19,7 +19,7 @@ from fetch_data import fetch_paged
|
|
|
19
19
|
from summarize import bid_changes, dim_layer_text
|
|
20
20
|
from cli_json import core_rule, metric_value, qoq_axis, cli_context, exec_cli_to_file, shim_entry
|
|
21
21
|
from cleanup import OwnedDirectory
|
|
22
|
-
from build_report import write_xlsx, validate_xlsx,
|
|
22
|
+
from build_report import write_xlsx, validate_xlsx, COLUMNS, DETAIL_COLUMNS
|
|
23
23
|
from run_workflow import run_batch, execute_business, publish, workflow_parser
|
|
24
24
|
from argparse import Namespace
|
|
25
25
|
from datetime import date
|
|
@@ -100,7 +100,7 @@ class DataRules(unittest.TestCase):
|
|
|
100
100
|
{'country_code': 'CA', 'spend_usd': 6, 'clicks': 1}],
|
|
101
101
|
}
|
|
102
102
|
with patch('summarize.iter_stem_rows', side_effect=lambda _bl, stem: iter(fixture.get(stem, []))):
|
|
103
|
-
rows = build_dimension_details(Path('.'), ['fb'], {'fb': []})
|
|
103
|
+
rows = list(build_dimension_details(Path('.'), ['fb'], {'fb': []}))
|
|
104
104
|
country = {row['id']: row for row in rows if row['kind'] == '国家'}
|
|
105
105
|
self.assertEqual({ident: row['action'] for ident, row in country.items()},
|
|
106
106
|
{'0': '新增', 'CA': '暂停', 'US': '持续'})
|
|
@@ -111,24 +111,27 @@ class DataRules(unittest.TestCase):
|
|
|
111
111
|
|
|
112
112
|
def test_account_column_grouping_and_rich_text(self):
|
|
113
113
|
"""账号列紧跟项目,渠道原生加粗,长名单不截断且完整回读。"""
|
|
114
|
-
from build_report import build_shared_strings
|
|
115
114
|
from xml.etree import ElementTree as ET
|
|
116
115
|
text = 'Meta:\n' + '\n'.join(f'act_{i}' for i in range(30)) + '\nGoogle:\n123-456-7890'
|
|
117
|
-
records, details =
|
|
116
|
+
records, details = [{'bizLine': '样本', 'adAccounts': text}], []
|
|
118
117
|
self.assertEqual(records[0]['adAccounts'], text)
|
|
119
118
|
self.assertEqual(len(records), 1)
|
|
120
119
|
self.assertEqual(COLUMNS[3:5], [('project', '项目名称'), ('adAccounts', '广告账号')])
|
|
121
120
|
self.assertEqual(len(COLUMNS), 16)
|
|
122
|
-
xml, _, indices, _, _, _ = build_shared_strings(records)
|
|
123
121
|
ns = {'s': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
|
|
124
|
-
cell = list(ET.fromstring(xml))[indices[0][4]]
|
|
125
|
-
bold = [''.join(run.itertext()) for run in cell.findall('s:r', ns) if run.find('s:rPr/s:b', ns) is not None]
|
|
126
|
-
self.assertEqual(bold, ['Meta', 'Google'])
|
|
127
|
-
self.assertEqual(''.join(cell.itertext()), text)
|
|
128
122
|
with tempfile.TemporaryDirectory() as tmp:
|
|
129
123
|
output = str(Path(tmp) / 'accounts.xlsx')
|
|
130
124
|
write_xlsx(records, output, details)
|
|
131
125
|
validate_xlsx(output, records, details)
|
|
126
|
+
with zipfile.ZipFile(output) as archive:
|
|
127
|
+
items = list(ET.fromstring(archive.read('xl/sharedStrings.xml')))
|
|
128
|
+
sheet = ET.fromstring(archive.read('xl/worksheets/sheet1.xml'))
|
|
129
|
+
cell = sheet.find('.//s:c[@r="E2"]/s:v', ns)
|
|
130
|
+
value = items[int(cell.text)]
|
|
131
|
+
bold = [''.join(run.itertext()) for run in value.findall('s:r', ns)
|
|
132
|
+
if run.find('s:rPr/s:b', ns) is not None]
|
|
133
|
+
self.assertEqual(bold, ['Meta', 'Google'])
|
|
134
|
+
self.assertEqual(''.join(value.itertext()), text)
|
|
132
135
|
|
|
133
136
|
def test_excel_strips_invalid_xml_controls_before_validation(self):
|
|
134
137
|
"""广告名称含垂直制表符等控制字符时,导出与回读保持同一清洗口径。"""
|
|
@@ -150,22 +153,26 @@ class DataRules(unittest.TestCase):
|
|
|
150
153
|
|
|
151
154
|
def test_strategy_native_bold_and_zero_changes(self):
|
|
152
155
|
"""维度/渠道原生加粗,普通单元格不串格式;零动作和100%占比不制造噪声。"""
|
|
153
|
-
from build_report import build_shared_strings
|
|
154
156
|
from summarize import setting_line
|
|
155
157
|
from cli_json import budget_abs_share
|
|
156
158
|
from xml.etree import ElementTree as ET
|
|
157
159
|
self.assertEqual(setting_line('【国家】', {'US'}, {'US'}), '【国家】\n未出现变化')
|
|
158
160
|
self.assertEqual(setting_line('【国家】', {'US'}, set()), '【国家】\n新增 1 个:US')
|
|
159
161
|
text = '总结:消耗变化。\n【预算】\nMeta:$100 → $120;环比 +20.00%'
|
|
160
|
-
xml, _, rows, _, _, _ = build_shared_strings([{'budgetStrategy': text, 'budgetLearning': text}])
|
|
161
162
|
ns = {'s': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
163
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
164
|
+
output = Path(tmp) / 'rich.xlsx'
|
|
165
|
+
write_xlsx([{'budgetStrategy': text, 'budgetLearning': text}], output)
|
|
166
|
+
with zipfile.ZipFile(output) as archive:
|
|
167
|
+
items = list(ET.fromstring(archive.read('xl/sharedStrings.xml')))
|
|
168
|
+
sheet = ET.fromstring(archive.read('xl/worksheets/sheet1.xml'))
|
|
169
|
+
strategy = items[int(sheet.find('.//s:c[@r="G2"]/s:v', ns).text)]
|
|
170
|
+
learning = items[int(sheet.find('.//s:c[@r="H2"]/s:v', ns).text)]
|
|
171
|
+
bold = [''.join(run.itertext()) for run in strategy.findall('s:r', ns)
|
|
172
|
+
if run.find('s:rPr/s:b', ns) is not None]
|
|
173
|
+
self.assertEqual(bold, ['【预算】', 'Meta'])
|
|
174
|
+
self.assertEqual(''.join(strategy.itertext()), text)
|
|
175
|
+
self.assertIsNone(learning.find('s:r', ns))
|
|
169
176
|
value = budget_abs_share('Meta', prev_abs=100, curr_abs=120, prev_share=100, curr_share=100)
|
|
170
177
|
self.assertNotIn('占比', value)
|
|
171
178
|
self.assertIn('环比 +20.00%', value)
|
|
@@ -680,6 +687,12 @@ class DataRules(unittest.TestCase):
|
|
|
680
687
|
with patch('fetch_data.run_cli', api), cli_context(threading.Event(), lambda _: None):
|
|
681
688
|
fetch_business(args)
|
|
682
689
|
payload, summary = summarize_request(root)
|
|
690
|
+
# 正常汇总只返回可重读引用,等待 Agent 时不持有完整明细。
|
|
691
|
+
self.assertNotIsInstance(payload['details'], list)
|
|
692
|
+
saved_payload = json.loads((root / 'payload.json').read_text(encoding='utf-8'))
|
|
693
|
+
self.assertNotIn('details', saved_payload)
|
|
694
|
+
self.assertEqual(saved_payload['detailSource']['count'], summary['detail_count'])
|
|
695
|
+
self.assertEqual(list(payload['details']), list(payload['details']))
|
|
683
696
|
self.assertEqual(summary['overall_cpi'], [2, 4])
|
|
684
697
|
self.assertEqual(payload['rows'][0]['adAccounts'], 'Meta:\nfixture')
|
|
685
698
|
self.assertEqual(summary['spend_verdict'], '有效')
|
|
@@ -726,7 +739,7 @@ class DataRules(unittest.TestCase):
|
|
|
726
739
|
(root / 'analysis-settings.json').write_text(json.dumps({'bizLineId': 2, 'coreIndex': 'CPI <= 999'}), encoding='utf-8')
|
|
727
740
|
default_payload, _ = summarize_request(root)
|
|
728
741
|
self.assertIn('KPI目标 <= 3', default_payload['rows'][0]['budgetLearning'])
|
|
729
|
-
self.assertNotIn(biz['coreIndex'], json.dumps(payload, ensure_ascii=False))
|
|
742
|
+
self.assertNotIn(biz['coreIndex'], json.dumps({**payload, 'details': list(payload['details'])}, ensure_ascii=False))
|
|
730
743
|
for key in ('budget', 'product', 'audience', 'creative'):
|
|
731
744
|
expected = {'budget': '结论:有效\n说明:策略有效',
|
|
732
745
|
'product': '结论:待观察\n说明:数据不足,暂无法判断'}
|
|
@@ -893,23 +906,23 @@ class CleanupAndExport(unittest.TestCase):
|
|
|
893
906
|
payload['rows'][0]['budgetStrategy'] = long_text
|
|
894
907
|
path = Path(tmp) / 'out.xlsx'
|
|
895
908
|
# 汇总不能静默截断;超长逐条信息必须放入明细。
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
self.assertEqual(
|
|
909
|
+
long_path = Path(tmp) / 'long.xlsx'
|
|
910
|
+
write_xlsx(payload['rows'], long_path, payload['details'])
|
|
911
|
+
self.assertEqual(validate_xlsx(long_path, payload['rows'], payload['details'])['rows'], 1)
|
|
899
912
|
# 超过可见行高仍只有一条业务线,保存值完整;不生成策略续行。
|
|
900
913
|
payload['rows'][0]['budgetStrategy'] = '枚举及消耗环比\n' * 60
|
|
901
914
|
single_row_path = Path(tmp) / 'single-row.xlsx'
|
|
902
915
|
write_xlsx(payload['rows'], str(single_row_path), payload['details'])
|
|
903
916
|
self.assertEqual(validate_xlsx(single_row_path, payload['rows'], payload['details'])['rows'], 1)
|
|
904
917
|
payload['rows'][0]['budgetStrategy'] = '【预算】\n' + ('渠道汇总:消耗 $100 → $120,占比 50% → 60%;环比 +20%\n' * 18) + '【调整出价】\n分布在:英语(3个)'
|
|
905
|
-
prepared, _ = prepare_layout(payload['rows'], payload['details'])
|
|
906
|
-
self.assertEqual(prepared[0]['budgetStrategy'], payload['rows'][0]['budgetStrategy'])
|
|
907
918
|
write_xlsx(payload['rows'], str(path), payload['details'])
|
|
908
919
|
counts = validate_xlsx(path, payload['rows'], payload['details'])
|
|
909
920
|
self.assertEqual(counts['details'], 1)
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
921
|
+
from xml.etree import ElementTree as ET
|
|
922
|
+
with zipfile.ZipFile(path) as archive:
|
|
923
|
+
items = [''.join(item.itertext()) for item in ET.fromstring(archive.read('xl/sharedStrings.xml'))]
|
|
924
|
+
self.assertIn(long_text, items)
|
|
925
|
+
self.assertFalse(any('[续' in value for value in items))
|
|
913
926
|
payload['details'][0]['extra'] = '过长' * 17000
|
|
914
927
|
with self.assertRaises(ValueError):
|
|
915
928
|
write_xlsx(payload['rows'], str(Path(tmp) / 'too-long.xlsx'), payload['details'])
|