sino-bc-cli 1.8.24 → 1.8.26

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.
Files changed (34) hide show
  1. package/dist/SKILL.md +30 -22
  2. package/dist/affordance/biz-line.md +7 -13
  3. package/dist/affordance/common-file.md +23 -0
  4. package/dist/affordance/wallet.md +207 -0
  5. package/dist/api-doc/README.md +4 -2
  6. package/dist/api-doc/oapi/biz-line-ai-strategy.md +45 -0
  7. package/dist/api-doc/oapi/biz-line-query.md +10 -3
  8. package/dist/api-doc/oapi/biz-line-strategy.md +12 -44
  9. package/dist/api-doc/oapi/common-file.md +44 -0
  10. package/dist/api-doc/oapi/wallet-bill.md +118 -0
  11. package/dist/api-doc/oapi/wallet-invoice.md +117 -0
  12. package/dist/api-doc/oapi/wallet-prepaid.md +249 -0
  13. package/dist/api-doc/oapi/wallet-quota.md +17 -0
  14. package/dist/api-doc/oapi/wallet-reconciliation.md +88 -0
  15. package/dist/generated/api-catalog.json +4183 -91
  16. package/dist/index.js +1 -1
  17. package/dist/skills/custom/biz-line-strategy-generate/SKILL.md +2 -3
  18. package/dist/skills/custom/biz-line-strategy-generate/references/api-flow.md +27 -1
  19. package/dist/skills/custom/biz-line-strategy-generate/references/output-excel.md +1 -1
  20. package/dist/skills/custom/biz-line-strategy-generate/references/runtime.md +3 -3
  21. package/dist/skills/custom/biz-line-strategy-generate/references/spec.md +2 -9
  22. package/dist/skills/custom/biz-line-strategy-generate/scripts/build_report.py +79 -225
  23. package/dist/skills/custom/biz-line-strategy-generate/scripts/cli_json.py +25 -91
  24. package/dist/skills/custom/biz-line-strategy-generate/scripts/fetch_data.py +5 -8
  25. package/dist/skills/custom/biz-line-strategy-generate/scripts/summarize.py +400 -588
  26. package/dist/skills/custom/biz-line-strategy-generate/scripts/test_strategy_rules.py +6 -53
  27. package/dist/skills/custom/biz-line-strategy-generate/scripts/test_workflow.py +6 -168
  28. package/dist/skills/default/sino-bc-shared/SKILL.md +1 -1
  29. package/dist/skills/default/sino-bc-shared/references/output-contract.md +2 -2
  30. package/dist/skills/oapi/sino-bc-biz-line/SKILL.md +11 -4
  31. package/dist/skills/oapi/sino-bc-common-file/SKILL.md +42 -0
  32. package/dist/skills/oapi/sino-bc-file/SKILL.md +3 -2
  33. package/dist/skills/oapi/sino-bc-wallet/SKILL.md +72 -0
  34. package/package.json +1 -1
@@ -142,39 +142,10 @@ def parse_json_text(raw: str):
142
142
  return None
143
143
 
144
144
 
145
- def load_json_file(path: Path, drop_page_rows: bool = False):
146
- """从文件解析 JSON。优先 json.load,避免先整文件读成 str 再 loads。
147
-
148
- drop_page_rows:分页信封在 object_hook 里记下行数并丢掉行 list,调用方拿不到整页行。
149
- 解析失败只读文件头,不把后面埋着的 JSON 当成功回包。
150
- """
145
+ def load_json_file(path: Path):
151
146
  if not path.exists():
152
147
  return None
153
-
154
- def hook(obj):
155
- if isinstance(obj.get("data"), list) and obj.get("total") is not None:
156
- obj["_page_got"] = len(obj["data"])
157
- obj["data"] = []
158
- if isinstance(obj.get("result"), list) and obj.get("total") is not None:
159
- obj["_page_got"] = obj.get("_page_got") or len(obj["result"])
160
- obj["result"] = []
161
- return obj
162
-
163
- try:
164
- with path.open(encoding="utf-8") as handle:
165
- if drop_page_rows:
166
- return json.load(handle, object_hook=hook)
167
- return json.load(handle)
168
- except json.JSONDecodeError:
169
- return parse_json_text(read_text_head(path))
170
-
171
-
172
- def read_text_head(path: Path, limit: int = 2000) -> str:
173
- """失败摘要只读文件头部,不把整页 stdout 再读进内存。"""
174
- if not path.exists():
175
- return ""
176
- with path.open(encoding="utf-8", errors="replace") as handle:
177
- return handle.read(limit)
148
+ return parse_json_text(path.read_text(encoding="utf-8"))
178
149
 
179
150
 
180
151
  def unwrap_page(envelope: dict) -> tuple[list, int]:
@@ -200,23 +171,8 @@ def unwrap_page(envelope: dict) -> tuple[list, int]:
200
171
  return [], 0
201
172
 
202
173
 
203
- def take_page_stats(envelope: dict) -> tuple[int, int]:
204
- """取出本页行数与 total,并丢掉行 list,让整页数据可被回收。"""
205
- rows, total = unwrap_page(envelope)
206
- got = len(rows)
207
- data = envelope.get("data")
208
- if isinstance(data, dict):
209
- if isinstance(data.get("data"), list):
210
- data["data"] = []
211
- if isinstance(data.get("result"), list):
212
- data["result"] = []
213
- elif isinstance(data, list):
214
- envelope["data"] = []
215
- return got, total
216
-
217
-
218
- def exec_cli_to_file(cmd: list[str], out_path: Path) -> int:
219
- """两个输出流直写本次文件,只返回退出码,不把 stdout 读回内存。"""
174
+ def exec_cli_to_file(cmd: list[str], out_path: Path) -> tuple[int, str]:
175
+ """两个输出流直写本次文件;取消时等待子进程退出后才允许清理。"""
220
176
  out_path.parent.mkdir(parents=True, exist_ok=True)
221
177
  cancel = getattr(_local, "cancel", None)
222
178
  if cancel and cancel.is_set():
@@ -239,7 +195,8 @@ def exec_cli_to_file(cmd: list[str], out_path: Path) -> int:
239
195
  except subprocess.TimeoutExpired:
240
196
  proc.kill()
241
197
  proc.wait()
242
- return proc.returncode
198
+ raw = out_path.read_text(encoding="utf-8")
199
+ return proc.returncode, raw
243
200
 
244
201
 
245
202
  def list_query_body(*, name: str = "", biz_id: int = 0) -> dict:
@@ -258,7 +215,7 @@ def list_query_body(*, name: str = "", biz_id: int = 0) -> dict:
258
215
  return body
259
216
 
260
217
 
261
- def run_cli(args: list[str], body: dict | None, out_path: Path, *, drop_rows: bool = False) -> dict:
218
+ def run_cli(args: list[str], body: dict | None, out_path: Path) -> dict:
262
219
  out_path.parent.mkdir(parents=True, exist_ok=True)
263
220
  cmd = [*resolve_cli(), *args, "--format", "json"]
264
221
  if body is not None:
@@ -267,12 +224,11 @@ def run_cli(args: list[str], body: dict | None, out_path: Path, *, drop_rows: bo
267
224
  body_path = out_path.with_suffix(".body.json")
268
225
  body_path.write_text(json.dumps(body, ensure_ascii=False), encoding="utf-8")
269
226
  cmd.extend(["--data", json.dumps(body, ensure_ascii=False)])
270
- rc = exec_cli_to_file(cmd, out_path)
271
- parsed = load_json_file(out_path, drop_page_rows=drop_rows)
272
- stderr_path = out_path.with_suffix(".stderr.txt")
227
+ rc, raw = exec_cli_to_file(cmd, out_path)
228
+ parsed = parse_json_text(raw.strip())
229
+ stderr = out_path.with_suffix(".stderr.txt").read_text(encoding="utf-8", errors="replace")
273
230
  if not isinstance(parsed, dict) or rc != 0 or parsed.get("ok") is not True:
274
- stderr = read_text_head(stderr_path)
275
- error = parse_json_text(stderr) or stderr or parsed or read_text_head(out_path) or "无有效 CLI 回包"
231
+ error = parse_json_text(stderr) or stderr or parsed or raw or "无有效 CLI 回包"
276
232
  parsed = {"ok": False, "error": error, "rc": rc}
277
233
  return parsed
278
234
 
@@ -293,46 +249,24 @@ def optional_num(row, key):
293
249
  return None
294
250
 
295
251
 
296
- METRIC_FORMULAS = {
297
- "CPI": ("spend_usd", "installs", 1),
298
- "CPC": ("spend_usd", "clicks", 1),
299
- "CPM": ("spend_usd", "impressions", 1000),
300
- "ROAS": ("purchase_value", "spend_usd", 1),
301
- "CTR": ("clicks", "impressions", 100),
302
- }
303
-
304
-
305
- def new_metric_acc(metric="CPI"):
306
- a, b, scale = METRIC_FORMULAS[metric]
307
- return {"a": a, "b": b, "scale": scale, "num_a": 0.0, "num_b": 0.0, "ok": True, "seen": False}
308
-
309
-
310
- def add_metric_acc(acc, row) -> None:
311
- if not acc["ok"]:
312
- return
313
- acc["seen"] = True
314
- left, right = optional_num(row, acc["a"]), optional_num(row, acc["b"])
315
- if left is None or right is None:
316
- acc["ok"] = False
317
- return
318
- acc["num_a"] += left
319
- acc["num_b"] += right
320
-
321
-
322
- def finish_metric_acc(acc):
323
- if not acc["ok"] or not acc["seen"]:
324
- return None
325
- return acc["num_a"] / acc["num_b"] * acc["scale"] if acc["num_b"] > 0 else None
326
-
327
-
328
252
  def metric_value(rows, metric="CPI"):
329
253
  """同一粒度先合计基础指标再计算,任一缺失不当作零补齐。只扫一遍,可接迭代器。"""
330
- acc = new_metric_acc(metric)
254
+ formulas = {"CPI": ("spend_usd", "installs", 1), "CPC": ("spend_usd", "clicks", 1),
255
+ "CPM": ("spend_usd", "impressions", 1000), "ROAS": ("purchase_value", "spend_usd", 1),
256
+ "CTR": ("clicks", "impressions", 100)}
257
+ a, b, scale = formulas[metric]
258
+ seen = False
259
+ num_a = num_b = 0.0
331
260
  for row in rows or ():
332
- add_metric_acc(acc, row)
333
- if not acc["ok"]:
261
+ seen = True
262
+ left, right = optional_num(row, a), optional_num(row, b)
263
+ if left is None or right is None:
334
264
  return None
335
- return finish_metric_acc(acc)
265
+ num_a += left
266
+ num_b += right
267
+ if not seen:
268
+ return None
269
+ return num_a / num_b * scale if num_b > 0 else None
336
270
 
337
271
 
338
272
  def core_rule(text):
@@ -15,7 +15,7 @@ SCRIPTS = Path(__file__).resolve().parent
15
15
  if str(SCRIPTS) not in sys.path:
16
16
  sys.path.insert(0, str(SCRIPTS))
17
17
 
18
- from cli_json import list_query_body, run_cli, unwrap_page, take_page_stats, require_ok, report_progress
18
+ from cli_json import list_query_body, run_cli, unwrap_page, require_ok, report_progress
19
19
  from datetime import date
20
20
 
21
21
  # 维度/成效 size=1000 时,TikTok 出价按天明细可到 4~5 万行,旧上限 30 页会截断
@@ -57,19 +57,16 @@ def fetch_paged(shortcut: str, base_body: dict, stem: str, bl_dir: Path) -> int:
57
57
  while True:
58
58
  body = {**base_body, "page": page, "size": 1000}
59
59
  out = bl_dir / f"{stem}.p{page}.out.json"
60
- env = run_cli(["biz-line", shortcut], body, out, drop_rows=True)
60
+ env = run_cli(["biz-line", shortcut], body, out)
61
61
  require_ok(env, stem)
62
62
  data = env.get("data")
63
63
  if not isinstance(data, dict) or not isinstance(data.get("data"), list) or not isinstance(data.get("total"), int) or data["total"] < 0:
64
64
  raise RuntimeError(f"{stem}: 分页信封缺少有效 data/total")
65
- got = data.get("_page_got")
66
- if isinstance(got, int):
67
- current_total = data["total"]
68
- else:
69
- got, current_total = take_page_stats(env)
65
+ rows, current_total = unwrap_page(env)
70
66
  if page > 1 and current_total != total:
71
67
  raise RuntimeError(f"{stem}: 分页期间总数变化,请重新拉取")
72
68
  total = current_total
69
+ got = len(rows)
73
70
  accumulated += got
74
71
  counts.append(got)
75
72
  report_progress(f"{stem} page={page} got={got} acc={accumulated} total={total}")
@@ -77,7 +74,7 @@ def fetch_paged(shortcut: str, base_body: dict, stem: str, bl_dir: Path) -> int:
77
74
  raise RuntimeError(f"{stem}: 累计行数超过 total")
78
75
  if accumulated == total:
79
76
  break
80
- if got == 0 or page >= PAGE_CAP:
77
+ if not rows or page >= PAGE_CAP:
81
78
  raise RuntimeError(f"{stem}: 分页不完整 {accumulated}/{total}")
82
79
  page += 1
83
80
  (bl_dir / f"{stem}.pages.json").write_text(