holy后端

This commit is contained in:
2026-08-04 14:02:45 +08:00
commit 175b38138e
25 changed files with 3752 additions and 0 deletions

View File

@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""
check_update.py — 检查 / 更新 Holy Crab Skill 版本
用法:
python3 check_update.py # 检查更新
python3 check_update.py --force # 强制更新到最新版本
python3 check_update.py --install # 从后端下载并安装
"""
import hashlib
import json
import os
import shutil
import sys
import zipfile
from datetime import datetime
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
SKILL_DIR = SCRIPT_DIR.parent
VERSION_FILE = SKILL_DIR / "VERSION"
BACKEND = os.environ.get("HC_BACKEND_URL", "http://localhost:8000").rstrip("/")
VERSION_API = f"{BACKEND}/api/v1/skill/holy-crab/version"
DOWNLOAD_API = f"{BACKEND}/api/v1/skill/holy-crab/download"
def local_version() -> dict | None:
if not VERSION_FILE.exists():
return None
return json.loads(VERSION_FILE.read_text())
def compute_hash(path: Path) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()[:16]
def update_version_file(remote: dict) -> None:
# 更新本地 VERSION
v = remote.copy()
for fname, finfo in v.get("files", {}).items():
fpath = SKILL_DIR / fname
if fpath.exists():
finfo["hash"] = compute_hash(fpath)
VERSION_FILE.write_text(json.dumps(v, ensure_ascii=False, indent=2))
def check_update() -> tuple[bool, dict | None, dict | None]:
"""检查是否有更新。返回 (有更新, 本地版本, 远程版本)"""
local = local_version()
remote_v = None
import urllib.request
try:
req = urllib.request.Request(VERSION_API, headers={"Accept": "application/json"})
with urllib.request.urlopen(req, timeout=10) as resp:
raw = json.loads(resp.read().decode("utf-8"))
# 后端返回格式:{"data": {"version": {...}}}
remote_v = raw.get("data", {}).get("version") or raw
except Exception:
return False, local, None
if not local:
return True, None, remote_v
def parse_ver(v: str) -> tuple:
return tuple(int(x) for x in v.split("."))
local_ver = local.get("version", "0.0.0")
remote_ver = remote_v.get("version", "0.0.0")
has_update = parse_ver(remote_ver) > parse_ver(local_ver)
return has_update, local, remote_v
def download_and_install() -> None:
import urllib.request
print(" 📥 正在从后端下载...")
try:
req = urllib.request.Request(DOWNLOAD_API, headers={"Accept": "application/zip"})
with urllib.request.urlopen(req, timeout=60) as resp:
zip_data = resp.read()
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
print(f" ❌ 下载失败 HTTP {e.code}: {body[:200]}")
sys.exit(1)
# 备份当前版本
backup_dir = SKILL_DIR.parent / f"holy-crab.backup.{datetime.now().strftime('%Y%m%d%H%M%S')}"
shutil.copytree(SKILL_DIR, backup_dir)
print(f" 📦 当前版本已备份至: {backup_dir}")
# 解压到 skill 目录
import io
with zipfile.ZipFile(io.BytesIO(zip_data), "r") as zf:
members = [m for m in zf.namelist() if not m.endswith("/")]
for member in members:
# 去掉顶层目录名skill 安装包通常带 holy-crab/v1.0.0/ 前缀)
dst_name = member
for prefix in [f"holy-crab-{VERSION_FILE.parent.name}/", "holy-crab/"]:
if member.startswith(prefix):
dst_name = member[len(prefix):]
break
if not dst_name or dst_name == member:
# 直接从根目录解压的文件
dst_name = member.split("/", 1)[1] if "/" in member else member
dst = SKILL_DIR / dst_name
dst.parent.mkdir(parents=True, exist_ok=True)
if dst_name:
dst.write_bytes(zf.read(member))
print(" ✅ 安装完成!")
def main() -> None:
force = "--force" in sys.argv
install = "--install" in sys.argv or "-u" in sys.argv
print()
print("" * 55)
print(" 🔍 Holy Crab Skill 版本检查")
print("" * 55)
print()
print(f" 后端地址: {BACKEND}")
print(f" 本地路径: {SKILL_DIR}")
print()
local = local_version()
if local:
print(f" 本地版本: {local.get('version')} ({local.get('released', '未知日期')})")
else:
print(" 本地版本: 未安装")
has_update, _, remote = check_update()
if not remote:
print()
print(" ⚠️ 无法连接到后端获取远程版本信息")
print(" 请确认:")
print(" 1. 后端是否已启动")
print(" 2. HC_BACKEND_URL 是否正确")
print()
sys.exit(1)
print(f" 远程版本: {remote.get('version')} ({remote.get('released', '未知日期')})")
if not has_update and not force:
print()
print(" ✅ 当前已是最新版本,无需更新")
print()
if local:
print(" 文件状态:")
for fname, finfo in local.get("files", {}).items():
fpath = SKILL_DIR / fname
status = "" if fpath.exists() else "❌ 缺失"
print(f" {status} {fname}")
print()
return
print()
print(f" 🆕 发现新版本: {remote.get('version')}")
changelog = remote.get("changelog", "")
if changelog:
print(f" 更新内容: {changelog}")
print()
if install or force or "--yes" in sys.argv:
confirm = True
else:
answer = input(" 是否立即更新?(y/n): ").strip().lower()
confirm = answer in ("y", "yes", "")
if confirm:
download_and_install()
# 更新本地 VERSION 记录
if local_version():
update_version_file(remote)
print()
print(f" ✅ Holy Crab Skill 已更新到 {remote.get('version')}")
else:
print(" 已取消更新")
print()
print("" * 55)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,216 @@
#!/usr/bin/env bash
# get-cookie.sh — Holy Crab AI 认证流程
#
# 流程:
# 1. 从后端获取钉钉授权 URL
# 2. 让用户在浏览器打开,授权后 URL 里会带 authCode
# 3. AI 把 authCode 贴过来 → 调用 /api/v1/auth/direct-token 换出 session token
# 4. 存 token 到 ~/.holy_crab_env后续所有请求自动带 Cookie
#
# 用法:
# bash get-cookie.sh # 交互式
# bash get-cookie.sh --check # 仅检查 token 是否有效
# bash get-cookie.sh <authCode> # 直接用 authCode 换 token
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
COOKIE_FILE="$HOME/.holy_crab_cookie"
ENV_FILE="$HOME/.holy_crab_env"
BACKEND="${HC_BACKEND_URL:-http://localhost:8000}"
# ---------------------------------------------------------------------------
# 工具函数
# ---------------------------------------------------------------------------
save_token() {
local token="$1"
echo "HC_SESSION=${token}" > "$ENV_FILE"
chmod 600 "$ENV_FILE"
echo "✅ Token 已写入: $ENV_FILE"
echo ""
echo "📋 后续 AI 将自动从此文件读取 Cookie无需任何额外操作。"
}
save_token_raw() {
local token="$1"
echo "$token" > "$COOKIE_FILE"
chmod 600 "$COOKIE_FILE"
}
check_token() {
local token="$1"
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
--max-time 10 \
-H "Cookie: hc_session=${token}" \
"$BACKEND/api/v1/auth/me")
if [ "$http_code" = "200" ]; then
echo "✅ Token 有效"
return 0
else
echo "❌ Token 无效或已过期 (HTTP $http_code)"
return 1
fi
}
# 从现有文件读 token
load_existing_token() {
# 优先 ~/.holy_crab_env
if [ -f "$ENV_FILE" ]; then
local val
val=$(grep -E "^HC_SESSION=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" | xargs)
[ -n "$val" ] && echo "$val" && return 0
fi
# 兜底 ~/.holy_crab_cookie
if [ -f "$COOKIE_FILE" ]; then
local val
val=$(cat "$COOKIE_FILE" | xargs)
[ -n "$val" ] && echo "$val" && return 0
fi
return 1
}
# ---------------------------------------------------------------------------
# Step 1检查现有 token
# ---------------------------------------------------------------------------
if [ "$1" = "--check" ]; then
echo ""
echo "🔍 检查现有 Token..."
if EXISTING=$(load_existing_token 2>/dev/null); then
if check_token "$EXISTING"; then
echo ""
echo " Token 来自: ${ENV_FILE:-$COOKIE_FILE}"
exit 0
fi
fi
echo " 未找到有效 Token需要重新授权。"
exit 1
fi
# ---------------------------------------------------------------------------
# Step 2直接用 authCode 换 token
# ---------------------------------------------------------------------------
if [ -n "$1" ] && [ "$1" != "--check" ]; then
AUTH_CODE="$1"
echo ""
echo "🔄 正在用 authCode 换取 session token..."
RESPONSE=$(curl -s --max-time 20 \
"$BACKEND/api/v1/auth/direct-token?authCode=${AUTH_CODE}")
ERROR=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('code',''))" 2>/dev/null)
if [ -n "$ERROR" ]; then
MSG=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('msg',''))" 2>/dev/null)
echo "❌ 换取失败: [$ERROR] $MSG"
echo " 原始响应: $RESPONSE"
exit 1
fi
TOKEN=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null)
USER=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['user'].get('name','?'))" 2>/dev/null)
EXPIRES=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['expires_at'])" 2>/dev/null)
if [ -z "$TOKEN" ]; then
echo "❌ 响应解析失败,原始响应:"
echo "$RESPONSE"
exit 1
fi
echo "✅ 换取成功!"
echo " 用户:$USER"
echo " 有效期至:$EXPIRES"
save_token "$TOKEN"
echo ""
echo "🎉 认证完成AI 现在可以访问 Holy Crab 后端了!"
exit 0
fi
# ---------------------------------------------------------------------------
# Step 3交互式引导 — 获取 authCode
# ---------------------------------------------------------------------------
echo ""
echo "═══════════════════════════════════════════════════════"
echo " 🔐 Holy Crab · AI 授权流程"
echo "═══════════════════════════════════════════════════════"
echo ""
echo " 本流程分两步:① 获取授权码 ② 换取 Token"
echo ""
# 检查后端是否可达
if ! curl -s --max-time 5 "$BACKEND/health" > /dev/null 2>&1; then
echo " ❌ 后端不可达: $BACKEND"
echo ""
echo " 请确认:"
echo " 1. 后端是否已启动python3 app.py"
echo " 2. HC_BACKEND_URL 是否正确?"
echo ""
echo " 当前 BACKEND=$BACKEND"
echo " 修改方式export HC_BACKEND_URL=http://服务器IP:8000"
exit 1
fi
echo " ✅ 后端在线: $BACKEND"
# 检查现有 token
if EXISTING=$(load_existing_token 2>/dev/null); then
if check_token "$EXISTING"; then
echo ""
echo " ✅ 已有有效 Token无需重新授权。"
echo " Token 来源: ${ENV_FILE:-$COOKIE_FILE}"
exit 0
else
echo " ⚠️ 已有 Token 但已过期,需要重新授权..."
fi
fi
echo ""
echo "───────────────────────────────────────────────────────"
echo " Step 1在浏览器中完成钉钉授权"
echo "───────────────────────────────────────────────────────"
echo ""
# 获取钉钉授权 URL
AUTH_URL_RESP=$(curl -s --max-time 10 "$BACKEND/api/v1/auth/dingtalk/login?redirect=/")
AUTH_URL=$(echo "$AUTH_URL_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['url'])" 2>/dev/null)
if [ -z "$AUTH_URL" ]; then
echo " ❌ 无法获取钉钉授权 URL后端可能未配置 DINGTALK_CLIENT_ID"
echo " 原始响应: $AUTH_URL_RESP"
exit 1
fi
echo " 请在浏览器中打开以下链接:"
echo ""
echo " $AUTH_URL"
echo ""
echo " 操作步骤:"
echo " 1. 浏览器会自动打开钉钉授权页面"
echo " 2. 点击「授权登录」"
echo " 3. 授权成功后,浏览器会跳转到后端页面"
echo " 4. 此时地址栏 URL 中会有 ?authCode=xxxxx"
echo " 把 authCode= 后面的那串字符复制下来"
echo ""
# 自动打开浏览器
if command -v open >/dev/null 2>&1; then
echo " 正在打开授权页面..."
open "$AUTH_URL" 2>/dev/null || true
fi
echo ""
echo "───────────────────────────────────────────────────────"
echo " Step 2把 authCode 贴过来"
echo "───────────────────────────────────────────────────────"
echo ""
echo " 复制 authCode 后,运行以下命令(把 YOUR_AUTH_CODE 换成你的值):"
echo ""
echo " bash $0 YOUR_AUTH_CODE"
echo ""
echo " 或者直接告诉 AI"
echo " 【authCode】YOUR_AUTH_CODE"
echo " AI 会自动帮你完成后续操作。"
echo ""
echo "═══════════════════════════════════════════════════════"

View File

@@ -0,0 +1,255 @@
#!/usr/bin/env python3
"""
process_data.py — 对 Holy Crab 任务结果进行数据分析
用法:
python3 process_data.py summarize <任务ID> 榜单摘要
python3 process_data.py sentiment <任务ID> 评论情感统计
python3 process_data.py compare <任务ID> 产品横向对比
python3 process_data.py detail <任务ID> 完整数据(含召回内容)
Examples:
python3 process_data.py summarize HC-20260727-ABC12
python3 process_data.py sentiment HC-20260727-ABC12
"""
import json
import os
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_BACKEND = "http://localhost:8000"
def load_cookie() -> str | None:
env = os.environ.get("HC_SESSION")
if env:
return env
cookie_file = Path.home() / ".holy_crab_cookie"
if cookie_file.exists():
return cookie_file.read_text().strip()
return None
def fetch_task(task_id: str) -> dict:
backend = os.environ.get("HC_BACKEND_URL", DEFAULT_BACKEND).rstrip("/")
url = f"{backend}/api/v1/tasks/{task_id}"
cookie = load_cookie()
headers = {}
if cookie:
headers["Cookie"] = f"hc_session={cookie}"
import urllib.request
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
try:
err = json.loads(body)
print(f"❌ API错误 {exc.code}: {err}", file=sys.stderr)
except Exception:
print(f"❌ HTTP {exc.code}: {body[:300]}", file=sys.stderr)
sys.exit(1)
result = data.get("data", {}).get("result", {})
extracted = result.get("extracted")
if not extracted:
print(f"❌ 任务 {task_id} 暂无 extracted 数据(任务未完成或格式不兼容)",
file=sys.stderr)
sys.exit(1)
return extracted
# ---------------------------------------------------------------------------
# 命令处理器
# ---------------------------------------------------------------------------
def cmd_summarize(extracted: dict) -> None:
"""榜单摘要"""
ranking = extracted.get("榜单", [])
products = extracted.get("产品详情", [])
src = extracted.get("参考来源笔记总量", {})
meta = extracted.get("提取元数据", {})
stats = meta.get("召回统计", {})
warns = extracted.get("提取警告", [])
print(f"\n{'='*60}")
print(f" 📊 榜单摘要")
print(f"{'='*60}")
print(f" 参考来源笔记总量 : {src.get('原始文本', 'N/A')} (提取: {src.get('提取数量', 'N/A')})")
print(f" 包含召回明细 : {'' if meta.get('包含召回明细') else ''}")
print(f" 标签总数 : {stats.get('标签总数', 0)}")
print(f" 实际召回总数 : {stats.get('实际召回内容总数', 0)}")
print(f"\n 【榜单排名】")
print(f" {'排名':<4} {'产品名称':<20} {'推荐比例':<8} {'产品详情'}")
print(f" {''*60}")
for item in ranking:
rank = item.get("当前排名", "")
name = item.get("产品名称", "")[:20]
ratio = item.get("推荐比例", "")
# 找对应的产品详情
prod = next((p for p in products if p.get("产品名称") == name), None)
if prod:
top3 = [t["内容标签"] for t in prod.get("标签数量Top3", [])]
detail = f"Top标签: {', '.join(top3)}"
else:
detail = ""
print(f" {str(rank):<4} {name:<20} {ratio:<8} {detail}")
if products:
print(f"\n 【各产品经验标签统计】")
for prod in products:
tags = prod.get("内容标签", [])
top3 = prod.get("标签数量Top3", [])
print(f"\n{prod.get('产品名称')} (排行{prod.get('当前排行')})")
print(f" 参考经验: {prod.get('参考经验人数')} | 推荐比例: {prod.get('推荐比例')}")
print(f" 标签 Top3: ", end="")
print(", ".join(f"{t['内容标签']}({t['经验数量']})" for t in top3) if top3 else "")
recall_total = sum(t.get("实际召回数量", 0) for t in tags)
print(f" 实际召回总数: {recall_total}")
if warns:
print(f"\n ⚠️ 提取警告 ({len(warns)} 条):")
for w in warns[:5]:
print(f" - {w}")
if len(warns) > 5:
print(f" ... 共 {len(warns)}")
print(f"\n{'='*60}\n")
def cmd_sentiment(extracted: dict) -> None:
"""评论情感统计(从 comment 类型召回内容中提取)"""
all_comments = []
for prod in extracted.get("产品详情", []):
for tag in prod.get("内容标签", []):
for note in tag.get("召回内容", []):
if note.get("召回类型") == "comment":
all_comments.append({
"评论内容": note.get("评论内容"),
"所属笔记点赞": note.get("所属笔记点赞数量"),
"发布时间": note.get("发布时间"),
"产品": prod.get("产品名称"),
"标签": tag.get("内容标签"),
})
if not all_comments:
print(f"\n ⚠️ 当前任务无评论召回数据comment 类型召回内容为空)")
print(f" 轻度/常规任务默认不包含评论召回深度任务deep模式才有。\n")
return
print(f"\n{'='*60}")
print(f" 💬 评论召回统计 (共 {len(all_comments)} 条)")
print(f"{'='*60}")
# 按产品分组
by_product: dict[str, list] = {}
for c in all_comments:
p = c["产品"]
by_product.setdefault(p, []).append(c)
for product, comments in by_product.items():
print(f"\n{product}】({len(comments)} 条评论)")
for c in comments[:10]:
content = c["评论内容"] or ""
print(f"{content[:60]}{'...' if len(content)>60 else ''}")
print(f" 所属笔记 👍{c['所属笔记点赞']} | {c['发布时间']}")
if len(comments) > 10:
print(f" ... 还有 {len(comments)-10}")
print(f"\n 📌 AI 情感分析建议:")
print(f" 请 AI 读取上述评论内容,判断每条评论的情感(正面/负面/中性),")
print(f" 并统计各情感类别的数量和占比。")
print(f"{'='*60}\n")
def cmd_compare(extracted: dict) -> None:
"""产品横向对比"""
products = extracted.get("产品详情", [])
if not products:
print(f"❌ 无产品详情数据", file=sys.stderr)
return
print(f"\n{'='*60}")
print(f" 🔍 产品横向对比")
print(f"{'='*60}")
headers = ["指标"] + [p.get("产品名称", f"产品{i+1}")[:12] for i, p in enumerate(products)]
col_w = 14
print(f" {'指标':<14} " + " ".join(f"{h:<{col_w}}" for h in headers[1:]))
print(f" {''*60}")
def col(val: str) -> str:
return f"{str(val):<{col_w}}"
print(f" {'当前排行':<14} " + " ".join(col(p.get("当前排行", "")) for p in products))
print(f" {'参考经验人数':<14} " + " ".join(col(p.get("参考经验人数", "")) for p in products))
print(f" {'推荐比例':<14} " + " ".join(col(p.get("推荐比例", "")) for p in products))
tag_counts = [len(p.get("内容标签", [])) for p in products]
print(f" {'标签数量':<14} " + " ".join(col(c) for c in tag_counts))
recall_totals = []
for p in products:
total = sum(t.get("实际召回数量", 0) for t in p.get("内容标签", []))
recall_totals.append(total)
print(f" {'实际召回总数':<14} " + " ".join(col(r) for r in recall_totals))
top_tags = []
for p in products:
top3 = p.get("标签数量Top3", [])
tags_str = ", ".join(t["内容标签"] for t in top3[:2]) if top3 else ""
top_tags.append(tags_str)
print(f" {'Top1-2标签':<14} " + " ".join(f"{t[:col_w*2-2]:<{col_w*2}}" for t in top_tags))
print(f"\n 【各产品 Top3 标签详情】")
for i, prod in enumerate(products):
print(f"\n {i+1}. {prod.get('产品名称')}")
for t in prod.get("标签数量Top3", []):
recall = next(
(x.get("实际召回数量", 0)
for x in prod.get("内容标签", [])
if x.get("内容标签") == t["内容标签"]),
0
)
print(f" {t['排名']}. {t['内容标签']} {t['经验数量']}人 召回:{recall}")
print(f"\n{'='*60}\n")
def cmd_detail(extracted: dict) -> None:
"""完整数据(含所有召回内容)"""
print(json.dumps(extracted, ensure_ascii=False, indent=2))
# ---------------------------------------------------------------------------
# 主入口
# ---------------------------------------------------------------------------
COMMANDS = {
"summarize": ("榜单摘要", cmd_summarize),
"sentiment": ("评论情感统计", cmd_sentiment),
"compare": ("产品横向对比", cmd_compare),
"detail": ("完整数据", cmd_detail),
}
def main() -> None:
if len(sys.argv) < 3 or sys.argv[1] not in COMMANDS:
print(f"用法: {sys.argv[0]} <{'|'.join(COMMANDS)}> <任务ID>")
print(f"示例: {sys.argv[0]} summarize HC-20260727-ABC12")
sys.exit(1)
cmd_key = sys.argv[1]
task_id = sys.argv[2]
_, handler = COMMANDS[cmd_key]
extracted = fetch_task(task_id)
handler(extracted)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""
query_tasks.py — 列出 Holy Crab 所有任务
用法: python3 query_tasks.py [任务ID 关键词]
Examples:
python3 query_tasks.py
python3 query_tasks.py HC-20260727-ABC12
"""
import json
import os
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_BACKEND = "http://localhost:8000"
def load_cookie() -> str | None:
# 优先级:环境变量 > ~/.holy_crab_env > ~/.holy_crab_cookie
env = os.environ.get("HC_SESSION")
if env:
return env
env_file = Path.home() / ".holy_crab_env"
if env_file.exists():
for line in env_file.read_text().strip().splitlines():
line = line.strip()
if line.startswith("HC_SESSION=") or line.startswith("hc_session="):
val = line.split("=", 1)[1].strip().strip('"').strip("'")
if val:
return val
cookie_file = Path.home() / ".holy_crab_cookie"
if cookie_file.exists():
return cookie_file.read_text().strip()
return None
def api_get(path: str, params: dict | None = None) -> dict:
backend = os.environ.get("HC_BACKEND_URL", DEFAULT_BACKEND).rstrip("/")
url = f"{backend}/api/v1{path}"
cookie = load_cookie()
headers = {}
if cookie:
headers["Cookie"] = f"hc_session={cookie}"
import urllib.request
req = urllib.request.Request(url, headers=headers)
if params:
import urllib.parse
req.full_url = f"{url}?{urllib.parse.urlencode(params)}"
try:
with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
try:
err = json.loads(body)
print(f"❌ API错误 {exc.code}: {err}", file=sys.stderr)
except Exception:
print(f"❌ HTTP {exc.code}: {body[:300]}", file=sys.stderr)
sys.exit(1)
def format_status(status: str) -> str:
mapping = {
"completed": "✅ 完成",
"failed": "❌ 失败",
"cancelled": "🚫 取消",
"partial": "⚠️ 部分",
"running": "⏳ 进行中",
"waiting": "⏳ 等待中",
}
return mapping.get(status, status)
def main() -> None:
task_id_filter = sys.argv[1] if len(sys.argv) > 1 else None
if task_id_filter:
# 精确查单个任务
data = api_get(f"/tasks/{task_id_filter}")
task = data.get("data", {})
print(f"\n{''*60}")
print(f" 任务ID : {task.get('task_id')}")
print(f" 名称 : {task.get('name')}")
print(f" 关键词 : {', '.join(task.get('keywords', []))}")
print(f" 状态 : {format_status(task.get('status'))}")
print(f" 进度 : {task.get('progress_percent')}%")
print(f" 模式 : {task.get('mode')}")
print(f" 创建时间 : {task.get('created_at')}")
print(f" 完成时间 : {task.get('completed_at') or ''}")
if task.get("error"):
print(f" 错误 : [{task['error'].get('code')}] {task['error'].get('message')}")
# 检查 extracted
result = task.get("result", {})
extracted = result.get("extracted")
if extracted:
meta = extracted.get("提取元数据", {})
stats = meta.get("召回统计", {})
print(f"\n 📊 数据清洗结果:")
print(f" schema : {extracted.get('schema_version')}")
print(f" 标签总数 : {stats.get('标签总数', 'N/A')}")
print(f" 召回总数 : {stats.get('实际召回内容总数', 'N/A')}")
print(f" 榜单数量 : {len(extracted.get('榜单', []))}")
print(f" 产品数量 : {len(extracted.get('产品详情', []))}")
src = extracted.get("参考来源笔记总量", {})
print(f" 笔记总量 : {src.get('原始文本', 'N/A')}")
else:
print(f"\n ⏳ 任务尚未完成或无 extracted 数据")
print(f"{''*60}\n")
else:
# 列表
data = api_get("/tasks", {"page_size": 50})
items = data.get("data", {}).get("items", [])
total = data.get("data", {}).get("total", 0)
print(f"\n{''*60}")
print(f" Holy Crab 任务列表 (共 {total} 个)")
print(f"{''*60}")
print(f" {'任务ID':<22} {'状态':<14} {'关键词':<16} {'创建时间':<12} {'文件'}")
print(f" {''*60}")
for t in items:
keywords = ",".join(t.get("keywords", [])[:2])
if len(",".join(t.get("keywords", []))) > 16:
keywords += "..."
status = format_status(t.get("status", ""))
files = len(t.get("files", []))
ts = (t.get("created_at") or "")[:10]
print(f" {t.get('task_id'):<22} {status:<14} {keywords:<16} {ts:<12} {files}f")
print(f"{''*60}\n")
if __name__ == "__main__":
main()