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.
@@ -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, prepare_layout, COLUMNS, DETAIL_COLUMNS
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 = prepare_layout([{'bizLine': '样本', 'adAccounts': text}], [])
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
- items = list(ET.fromstring(xml))
163
- indices = {key: i for i, (key, _) in enumerate(COLUMNS)}
164
- strategy = items[rows[0][indices['budgetStrategy']]]
165
- bold = [''.join(run.itertext()) for run in strategy.findall('s:r', ns) if run.find('s:rPr/s:b', ns) is not None]
166
- self.assertEqual(bold, ['【预算】', 'Meta'])
167
- self.assertEqual(''.join(strategy.itertext()), text)
168
- self.assertIsNone(items[rows[0][indices['budgetLearning']]].find('s:r', ns))
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
- prepared_long, _ = prepare_layout(payload['rows'], payload['details'])
897
- self.assertEqual(''.join(r.get('budgetStrategy', '') for r in prepared_long), long_text)
898
- self.assertEqual(len(prepared_long), 1)
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
- _, details = prepare_layout(payload['rows'], payload['details'])
911
- self.assertEqual(''.join(r['extra'] for r in details if r['kind'] == '出价'), long_text)
912
- self.assertNotIn('[续', details[0]['action'])
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'])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sino-bc-cli",
3
- "version": "1.8.38",
3
+ "version": "1.8.39",
4
4
  "description": "BeyondClick CLI - 命令行工具集合,提供快捷访问 BeyondClick 平台的命令行接口",
5
5
  "preferGlobal": true,
6
6
  "bin": {