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.
- package/dist/SKILL.md +30 -22
- package/dist/affordance/biz-line.md +7 -13
- package/dist/affordance/common-file.md +23 -0
- package/dist/affordance/wallet.md +207 -0
- package/dist/api-doc/README.md +4 -2
- package/dist/api-doc/oapi/biz-line-ai-strategy.md +45 -0
- package/dist/api-doc/oapi/biz-line-query.md +10 -3
- package/dist/api-doc/oapi/biz-line-strategy.md +12 -44
- package/dist/api-doc/oapi/common-file.md +44 -0
- package/dist/api-doc/oapi/wallet-bill.md +118 -0
- package/dist/api-doc/oapi/wallet-invoice.md +117 -0
- package/dist/api-doc/oapi/wallet-prepaid.md +249 -0
- package/dist/api-doc/oapi/wallet-quota.md +17 -0
- package/dist/api-doc/oapi/wallet-reconciliation.md +88 -0
- package/dist/generated/api-catalog.json +4183 -91
- package/dist/index.js +1 -1
- package/dist/skills/custom/biz-line-strategy-generate/SKILL.md +2 -3
- package/dist/skills/custom/biz-line-strategy-generate/references/api-flow.md +27 -1
- package/dist/skills/custom/biz-line-strategy-generate/references/output-excel.md +1 -1
- package/dist/skills/custom/biz-line-strategy-generate/references/runtime.md +3 -3
- package/dist/skills/custom/biz-line-strategy-generate/references/spec.md +2 -9
- package/dist/skills/custom/biz-line-strategy-generate/scripts/build_report.py +79 -225
- package/dist/skills/custom/biz-line-strategy-generate/scripts/cli_json.py +25 -91
- package/dist/skills/custom/biz-line-strategy-generate/scripts/fetch_data.py +5 -8
- package/dist/skills/custom/biz-line-strategy-generate/scripts/summarize.py +400 -588
- package/dist/skills/custom/biz-line-strategy-generate/scripts/test_strategy_rules.py +6 -53
- package/dist/skills/custom/biz-line-strategy-generate/scripts/test_workflow.py +6 -168
- package/dist/skills/default/sino-bc-shared/SKILL.md +1 -1
- package/dist/skills/default/sino-bc-shared/references/output-contract.md +2 -2
- package/dist/skills/oapi/sino-bc-biz-line/SKILL.md +11 -4
- package/dist/skills/oapi/sino-bc-common-file/SKILL.md +42 -0
- package/dist/skills/oapi/sino-bc-file/SKILL.md +3 -2
- package/dist/skills/oapi/sino-bc-wallet/SKILL.md +72 -0
- 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
|
|
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
|
|
204
|
-
"""
|
|
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
|
-
|
|
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
|
|
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 =
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|