From e1b0967cb3799645bf6e85597bf3923c42dbca3a Mon Sep 17 00:00:00 2001 From: lily Date: Thu, 24 Sep 2026 02:01:52 +0800 Subject: [PATCH] =?UTF-8?q?update=EF=BC=9A=E6=9B=B4=E6=96=B0skill=E8=84=9A?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.example.yaml | 3 + haoxian/skills/temp0923/SKILL.md | 119 ++++++ .../skills/temp0923/scripts/query_qa_raw.py | 348 ++++++++++++++++++ 3 files changed, 470 insertions(+) create mode 100644 haoxian/skills/temp0923/SKILL.md create mode 100644 haoxian/skills/temp0923/scripts/query_qa_raw.py diff --git a/config/config.example.yaml b/config/config.example.yaml index 3d9fa02..3ed3a4f 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -6,3 +6,6 @@ feishu: hisense_geo: base_url: "https://geo-api.hisense.com" token: "" # 生产环境 x-token,勿提交仓库 +haoxian: + base_url: "https://resource-api.cemeta.cn" + token: "" # 好现 x-token,勿提交仓库 diff --git a/haoxian/skills/temp0923/SKILL.md b/haoxian/skills/temp0923/SKILL.md new file mode 100644 index 0000000..fbdb0d3 --- /dev/null +++ b/haoxian/skills/temp0923/SKILL.md @@ -0,0 +1,119 @@ +--- +name: hisense-sept-qa-raw-query +description: >- + Query Hisense September agentic projects for qa_raw and qa_product_raw + (HISENSE-HIST). Before any API call, must confirm BOTH batch date and project + id together. Returns id, project_name, total, qa_raw_list, qa_product_raw_list. + Use when user asks for 9月项目、问答底表、商品卡、qa_raw、qa_product_raw、 + HISENSE-HIST、temp0923, or 批次日期 / 项目id. +--- + +# 海信 9 月 qa_raw / qa_product_raw 查询 + +从 `https://resource-api.cemeta.cn` 拉 9 月项目,再按批次查问答底表与商品卡表,组装返回 JSON。 + +配置:`config/config.dev.yaml` → `haoxian.base_url` / `haoxian.token`(Header `x-token`)。本机可用 `http://localhost:9999` 代替 base(路径相同)。 + +## 必选前置输入(缺一不可,须同时确认) + +**使用本 skill 时,执行任何接口调用之前,必须先同时拿到下面 1 和 2。缺任意一项都停下来,两项齐了再继续后续流程。不要猜、不要沿用上次对话默认值、不要只拿到一个就先跑一半。** + +| # | 必选项 | 示例 | +|---|--------|------| +| 1 | 查询的批次日期 | `20260919`(`YYYYMMDD`) | +| 2 | 项目 id | `1096` | + +**一次性原文提示用户(同一条消息里同时确认两项):** + +> 请同时确认后回复: +> 1. 查询的批次日期(示例:20260919) +> 2. 项目id(示例:1096) + +用户在同一轮或连续回复中给出 1 和 2 后,再执行下方「执行流程」。若只给了一项,用同样提示补要缺失项,仍不要开查。 + +## 执行流程 + +### 1. 拉 9 月项目列表 + +``` +GET {base_url}/api/agentic-project/project?page=1&page_size=999&listed=all +Header: x-token +``` + +过滤(必须同时满足): + +- `listed = 1` +- `status = 'running'` +- `end_month = '2026-09'` + +取出全部 `id`、`project_name`。用用户输入的项目 id 在列表中解析 `project_name`(找不到也继续查表,name 可为空并注明)。 + +### 2. 拼接 crawl_batch_id + +``` +HISENSE-HIST-{project_id}-{批次日期} +``` + +例:`HISENSE-HIST-1096-20260919` + +### 3. 查问答底表 qa_raw + +``` +GET {base_url}/api/geo_label/l1/qa_raw?project_id={id}&crawl_batch_id={crawl_batch_id}&page=1&page_size=50 +Header: x-token +``` + +翻页直到收齐 `data.total`。接口列表字段为 `data.items`。 + +### 4. 查商品卡表 qa_product_raw + +``` +GET {base_url}/api/geo_label/l1/qa_product_raw?project_id={id}&crawl_batch_id={crawl_batch_id}&page=1&page_size=50 +Header: x-token +``` + +同样翻页收齐;接口列表字段为 `data.items`。 + +### 5. 组装返回 JSON + +**对外不要输出 `items`。** `data.items` 分别映射为: + +- qa_raw → `qa_raw_list` +- qa_product_raw → `qa_product_raw_list` + +`total` 取 **qa_raw** 的 `data.total`。 + +```json +{ + "id": 1096, + "project_name": "500L左右冰箱推荐", + "total": 227, + "code": 0, + "ok": true, + "crawl_batch_id": "HISENSE-HIST-1096-20260919", + "qa_raw_list": [], + "qa_product_raw_list": [] +} +``` + +必须包含:`id`、`project_name`、`total`、`qa_raw_list`(全量明细)、`qa_product_raw_list`(全量明细)。体积大时落盘并告知路径,例如: + +`haoxian/skills/temp0923/data/qa_raw_{批次}/{id}_response.json` + +## 推荐脚本 + +```bash +# 可选:刷新 9 月项目缓存 +python3 haoxian/skills/temp0923/scripts/query_qa_raw.py list-projects + +# 必选参数:--batch、--id +python3 haoxian/skills/temp0923/scripts/query_qa_raw.py project --batch 20260919 --id 1096 +``` + +脚本 stdout 即为上述返回结构。本地缓存目录:`haoxian/skills/temp0923/data/`。 + +## 注意 + +- Token 只读配置,勿写入 skill、勿提交仓库。 +- `total=0` 或空列表均为有效结果。 +- 翻页建议 `page_size=50`,失败重试,避免一次过大导致断连。 diff --git a/haoxian/skills/temp0923/scripts/query_qa_raw.py b/haoxian/skills/temp0923/scripts/query_qa_raw.py new file mode 100644 index 0000000..208ff73 --- /dev/null +++ b/haoxian/skills/temp0923/scripts/query_qa_raw.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python3 +"""海信 9 月项目 + qa_raw / qa_product_raw 查询(resource-api / localhost)。""" + +from __future__ import annotations + +import argparse +import json +import sys +import time +import urllib.request +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] # ai_script +SKILL_DATA = Path(__file__).resolve().parents[1] / "data" +DEFAULT_BASE = "https://resource-api.cemeta.cn" +REQUIRED_END_MONTH = "2026-09" + + +def load_haoxian_config() -> dict: + cfg_path = ROOT / "config" / "config.dev.yaml" + if not cfg_path.exists(): + raise SystemExit(f"缺少配置: {cfg_path}") + section = None + hao: dict[str, str] = {} + for line in cfg_path.read_text().splitlines(): + if line.startswith("haoxian:"): + section = "hao" + continue + if line and not line.startswith(" ") and line.endswith(":"): + section = None + continue + if section == "hao" and ":" in line and line.startswith(" "): + k, v = line.split(":", 1) + hao[k.strip()] = v.strip().strip('"').strip("'") + if not hao.get("token"): + raise SystemExit("config.dev.yaml 缺少 haoxian.token") + if not hao.get("base_url"): + hao["base_url"] = DEFAULT_BASE + return hao + + +def http_get(url: str, token: str, retries: int = 4) -> dict: + last: Exception | None = None + for i in range(retries): + try: + req = urllib.request.Request(url, headers={"x-token": token}) + with urllib.request.urlopen(req, timeout=180) as resp: + return json.loads(resp.read()) + except Exception as e: # noqa: BLE001 + last = e + time.sleep(1.5 * (i + 1)) + raise RuntimeError(f"GET failed after retries: {url}: {last}") + + +def list_sept_projects(base: str, token: str) -> list[dict]: + url = f"{base.rstrip('/')}/api/agentic-project/project?page=1&page_size=999&listed=all" + data = http_get(url, token) + items = (data.get("data") or {}).get("list") or [] + out = [] + for p in items: + if ( + p.get("listed") == 1 + and str(p.get("status") or "").lower() == "running" + and p.get("end_month") == REQUIRED_END_MONTH + ): + out.append( + { + "id": p.get("id"), + "project_name": p.get("project_name"), + "brand": p.get("brand"), + "category": p.get("category"), + "end_month": p.get("end_month"), + "status": p.get("status"), + "listed": p.get("listed"), + } + ) + out.sort(key=lambda x: int(x["id"]) if str(x["id"]).isdigit() else 0, reverse=True) + return out + + +def fetch_l1_table( + base: str, + token: str, + table: str, + project_id: int | str, + batch: str, + page_size: int = 50, +) -> dict: + """分页拉取 /api/geo_label/l1/{table},返回 {total, code, ok, crawl_batch_id, items}。""" + crawl = f"HISENSE-HIST-{project_id}-{batch}" + all_items: list = [] + total = None + code = 0 + page = 1 + while True: + url = ( + f"{base.rstrip('/')}/api/geo_label/l1/{table}" + f"?project_id={project_id}&crawl_batch_id={crawl}" + f"&page={page}&page_size={page_size}" + ) + data = http_get(url, token) + code = data.get("code", 0) + if code != 0: + return { + "id": int(project_id) if str(project_id).isdigit() else project_id, + "total": 0, + "code": code, + "ok": False, + "crawl_batch_id": crawl, + "items": [], + "message": data.get("message"), + } + d = data.get("data") or {} + if total is None: + total = d.get("total", 0) or 0 + page_items = d.get("items") or [] + all_items.extend(page_items) + if len(all_items) >= total or not page_items: + break + page += 1 + if page > 100: + break + return { + "id": int(project_id) if str(project_id).isdigit() else project_id, + "total": total, + "code": code, + "ok": True, + "crawl_batch_id": crawl, + "fetched": len(all_items), + "items": all_items, + } + + +def fetch_qa_raw(base: str, token: str, project_id: int | str, batch: str, page_size: int = 50) -> dict: + return fetch_l1_table(base, token, "qa_raw", project_id, batch, page_size) + + +def fetch_qa_product_raw(base: str, token: str, project_id: int | str, batch: str, page_size: int = 50) -> dict: + return fetch_l1_table(base, token, "qa_product_raw", project_id, batch, page_size) + + +def cmd_list_projects(args: argparse.Namespace) -> None: + cfg = load_haoxian_config() + base = args.base or cfg["base_url"] + projects = list_sept_projects(base, cfg["token"]) + SKILL_DATA.mkdir(parents=True, exist_ok=True) + out = SKILL_DATA / "sept_projects_202609.json" + out.write_text(json.dumps(projects, ensure_ascii=False, indent=2)) + print(json.dumps({"count": len(projects), "path": str(out), "projects": projects}, ensure_ascii=False, indent=2)) + + +def cmd_batch_totals(args: argparse.Namespace) -> None: + cfg = load_haoxian_config() + base = args.base or cfg["base_url"] + batch = args.batch + projects = list_sept_projects(base, cfg["token"]) + cache_dir = SKILL_DATA / f"qa_raw_{batch}" + cache_dir.mkdir(parents=True, exist_ok=True) + + rows = [] + for i, p in enumerate(projects, 1): + pid = p["id"] + cache = cache_dir / f"{pid}.json" + if cache.exists() and not args.refresh: + payload = json.loads(cache.read_text()) + total = payload.get("total", 0) + items = payload.get("items") or [] + row = { + "id": pid, + "project_name": p["project_name"], + "total": total, + "code": 0, + "ok": True, + "crawl_batch_id": payload.get("crawl_batch_id") or f"HISENSE-HIST-{pid}-{batch}", + "fetched": len(items), + } + else: + try: + raw = fetch_qa_raw(base, cfg["token"], pid, batch, page_size=args.page_size) + except Exception as e: # noqa: BLE001 + row = { + "id": pid, + "project_name": p["project_name"], + "total": -1, + "code": None, + "ok": False, + "err": str(e), + } + rows.append(row) + print(f"[{i}/{len(projects)}] FAIL id={pid}: {e}", file=sys.stderr) + continue + items = raw.get("items") or [] + payload = { + "project_id": pid, + "project_name": p["project_name"], + "crawl_batch_id": raw["crawl_batch_id"], + "batch_date": batch, + "total": raw["total"], + "fetched": raw.get("fetched", len(items)), + "items": items, + } + cache.write_text(json.dumps(payload, ensure_ascii=False)) + row = { + "id": pid, + "project_name": p["project_name"], + "total": raw["total"], + "code": raw.get("code", 0), + "ok": raw.get("ok", True), + "crawl_batch_id": raw["crawl_batch_id"], + "fetched": payload["fetched"], + } + rows.append(row) + if i % 20 == 0 or i == len(projects): + print(f"[{i}/{len(projects)}] ...", file=sys.stderr) + + summary = { + "batch_date": batch, + "base_url": base, + "count": len(rows), + "ok": sum(1 for r in rows if r.get("ok")), + "total_sum": sum(r["total"] for r in rows if r.get("ok") and r["total"] >= 0), + "projects": rows, + } + (SKILL_DATA / f"qa_raw_{batch}_summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2)) + tsv = ["project_id\tproject_name\ttotal\tok\tcrawl_batch_id"] + for r in rows: + tsv.append( + f"{r.get('id')}\t{r.get('project_name')}\t{r.get('total')}\t{r.get('ok')}\t{r.get('crawl_batch_id','')}" + ) + (SKILL_DATA / f"qa_raw_{batch}_totals.tsv").write_text("\n".join(tsv) + "\n") + print(json.dumps({"batch_date": batch, "count": len(rows), "projects": rows}, ensure_ascii=False, indent=2)) + + +def cmd_project(args: argparse.Namespace) -> None: + cfg = load_haoxian_config() + base = args.base or cfg["base_url"] + batch = args.batch + pid = args.id + cache_dir = SKILL_DATA / f"qa_raw_{batch}" + cache = cache_dir / f"{pid}.json" + product_cache = cache_dir / f"{pid}_product_raw.json" + + name = None + projects_path = SKILL_DATA / "sept_projects_202609.json" + if projects_path.exists(): + for p in json.loads(projects_path.read_text()): + if str(p.get("id")) == str(pid): + name = p.get("project_name") + break + + if cache.exists() and not args.refresh: + payload = json.loads(cache.read_text()) + qa_raw_list = payload.get("items") or payload.get("qa_raw_list") or [] + crawl = payload.get("crawl_batch_id") or f"HISENSE-HIST-{pid}-{batch}" + total = payload.get("total") + code = 0 + ok = True + if not name: + name = payload.get("project_name") + else: + raw = fetch_qa_raw(base, cfg["token"], pid, batch, page_size=args.page_size) + qa_raw_list = raw.get("items") or [] + crawl = raw["crawl_batch_id"] + total = raw["total"] + code = raw.get("code", 0) + ok = raw.get("ok", True) + cache_dir.mkdir(parents=True, exist_ok=True) + cache.write_text( + json.dumps( + { + "project_id": raw["id"], + "project_name": name, + "crawl_batch_id": crawl, + "batch_date": batch, + "total": total, + "fetched": len(qa_raw_list), + "items": qa_raw_list, + }, + ensure_ascii=False, + ) + ) + + if product_cache.exists() and not args.refresh: + prod_payload = json.loads(product_cache.read_text()) + qa_product_raw_list = ( + prod_payload.get("items") or prod_payload.get("qa_product_raw_list") or [] + ) + else: + prod = fetch_qa_product_raw(base, cfg["token"], pid, batch, page_size=args.page_size) + qa_product_raw_list = prod.get("items") or [] + if not prod.get("ok", True): + ok = False + code = prod.get("code", code) + cache_dir.mkdir(parents=True, exist_ok=True) + product_cache.write_text( + json.dumps( + { + "project_id": int(pid) if str(pid).isdigit() else pid, + "project_name": name, + "crawl_batch_id": crawl, + "batch_date": batch, + "total": prod.get("total", len(qa_product_raw_list)), + "fetched": len(qa_product_raw_list), + "items": qa_product_raw_list, + }, + ensure_ascii=False, + ) + ) + + out = { + "id": int(pid) if str(pid).isdigit() else pid, + "project_name": name, + "total": total, + "code": code, + "ok": ok, + "crawl_batch_id": crawl, + "qa_raw_list": qa_raw_list, + "qa_product_raw_list": qa_product_raw_list, + } + print(json.dumps(out, ensure_ascii=False, indent=2)) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Hisense Sept qa_raw query") + parser.add_argument("--base", default=None, help="API base, default from config / resource-api") + parser.add_argument("--page-size", type=int, default=50) + sub = parser.add_subparsers(dest="cmd", required=True) + + p1 = sub.add_parser("list-projects", help="List Sept running projects") + p1.set_defaults(func=cmd_list_projects) + + p2 = sub.add_parser("batch-totals", help="Fetch/totals for all Sept projects in a batch") + p2.add_argument("--batch", required=True, help="Batch date YYYYMMDD, e.g. 20260919") + p2.add_argument("--refresh", action="store_true") + p2.set_defaults(func=cmd_batch_totals) + + p3 = sub.add_parser("project", help="Fetch qa_raw + qa_product_raw; stdout uses qa_raw_list") + p3.add_argument("--batch", required=True) + p3.add_argument("--id", required=True) + p3.add_argument("--refresh", action="store_true") + p3.set_defaults(func=cmd_project) + + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main()