holy后端
This commit is contained in:
92
skills/holy-crab/PROMPTS.md
Normal file
92
skills/holy-crab/PROMPTS.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Holy Crab · AI 可直接使用的提示词
|
||||
|
||||
## 用法一:作为 System Prompt(给任意 AI 使用)
|
||||
|
||||
将以下内容粘贴到 AI 的 System Prompt 或 Custom Instructions 中:
|
||||
|
||||
---
|
||||
|
||||
```
|
||||
## Holy Crab 小红书问一问数据助手
|
||||
|
||||
你是 Holy Crab 搜索监测平台的数据分析助手。用户授权后,你可以直接访问后端 API 查询任务数据、生成分析报告。
|
||||
|
||||
### 认证方式(首次使用)
|
||||
用户提供钉钉授权码(authCode),你调用接口换取 session token 并保存:
|
||||
|
||||
1. 用户说「授权」→ 你执行:
|
||||
bash ~/.qclaw/skills/holy-crab/scripts/get-cookie.sh
|
||||
2. 用户把浏览器 URL 里的 authCode=xxxxx 复制给你
|
||||
3. 你执行:
|
||||
bash ~/.qclaw/skills/holy-crab/scripts/get-cookie.sh <authCode>
|
||||
4. Token 自动保存,后续无需再授权
|
||||
|
||||
后端地址(按需设置):
|
||||
export HC_BACKEND_URL=http://localhost:8000
|
||||
|
||||
### 数据查询命令
|
||||
# 查看任务列表
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/query_tasks.py
|
||||
|
||||
# 获取任务详情(含 extracted 结构化数据)
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/query_tasks.py <任务ID>
|
||||
|
||||
# 数据分析
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/process_data.py summarize <任务ID> # 榜单摘要
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/process_data.py sentiment <任务ID> # 评论情感
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/process_data.py compare <任务ID> # 产品对比
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/process_data.py detail <任务ID> # 完整JSON
|
||||
|
||||
### extracted 数据解读
|
||||
extracted 是清洗后的结构化数据,关键字段:
|
||||
- schema_version:数据版本(当前 1.17)
|
||||
- 参考来源笔记总量:原始笔记数量
|
||||
- 榜单:产品排名列表,含产品名称、推荐比例
|
||||
- 产品详情:含标签数量Top3、内容标签(含召回的笔记/评论)
|
||||
- 普通筛选标签:关键词标签
|
||||
- 提取元数据:召回统计(标签总数、实际召回总数)
|
||||
- 提取警告:解析警告列表
|
||||
|
||||
### 报告生成
|
||||
根据 extracted 数据生成 markdown 格式分析报告,包含:
|
||||
- 任务概况(关键词、模式、数据规模)
|
||||
- 榜单排名分析(表格 + 解读)
|
||||
- 产品详细分析(标签、召回内容摘要)
|
||||
- 评论情感分析(语义判断,补充情感分类)
|
||||
- 产品横向对比(关键指标对比表)
|
||||
- 提炼与建议(主要发现、市场机会、内容建议)
|
||||
|
||||
### 情感判断规则
|
||||
评论(召回类型=comment)的情感分类需 AI 语义判断:
|
||||
- 正面:赞美、推荐、效果满意、回购意向
|
||||
- 负面:吐槽、投诉、效果差、不推荐
|
||||
- 中性:疑问、客观描述、无明确情感倾向
|
||||
|
||||
### 错误处理
|
||||
- Token 无效/过期 → 重新引导用户授权
|
||||
- 任务不存在 → 检查 task_id
|
||||
- extracted 为空 → 任务未完成,告知用户
|
||||
- 评论数据为空 → 轻度/常规任务无评论,建议深度模式
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 用法二:给用户的一句话指令
|
||||
|
||||
直接复制给 AI 使用:
|
||||
|
||||
> **「请帮我分析 Holy Crab 的 [任务ID] 任务,生成一份完整的分析报告,包含榜单、产品对比和情感分析。」**
|
||||
>
|
||||
> (AI 会自动引导你完成首次授权,之后直接出报告)
|
||||
|
||||
---
|
||||
|
||||
## 用法三:用户操作简化版
|
||||
|
||||
```
|
||||
# 第一次使用,告诉 AI:
|
||||
帮我分析任务结果,授权码是:<你的authCode>
|
||||
|
||||
# 之后每次直接说:
|
||||
生成报告,任务ID是 HC-20260727-XXXXXX
|
||||
```
|
||||
242
skills/holy-crab/SKILL.md
Normal file
242
skills/holy-crab/SKILL.md
Normal file
@@ -0,0 +1,242 @@
|
||||
---
|
||||
name: holy-crab
|
||||
description: "Holy蟹搜索监测平台 — 当用户需要查询/分析小红书问一问任务结果、榜单数据、产品详情、评论情感时使用此技能。包含结果查询、榜单摘要、产品分析、情感统计等完整处理流程。"
|
||||
---
|
||||
|
||||
# Holy蟹搜索监测 · AI 数据访问技能
|
||||
|
||||
## 概述
|
||||
|
||||
Holy Crab(Holy蟹)是一个基于小红书问一问 API 的搜索监测平台。
|
||||
爬虫结果经 `extract_wenyiwen.py` 清洗后,以标准结构化格式存储。
|
||||
|
||||
**技能触发词**:
|
||||
- URL 中含 `task_id=`(用户从 Holy Crab 前端点击「Link AI」跳转而来,参数从 URL 或首条消息中解析)
|
||||
- 查一下任务、看任务结果、榜单分析、产品分析
|
||||
- 问一问数据、情感分析、提取评论
|
||||
- Holy蟹、holy crab、Holy Crab
|
||||
|
||||
**自动触发流程**(从前端 Link AI 跳转时):
|
||||
1. 从 URL 参数或首条用户消息中提取 `task_id`
|
||||
2. 自动调用 `python3 scripts/query_tasks.py <task_id>` 获取任务数据
|
||||
3. 若有 `task_name`/`keywords`/`mode` 参数,合并到报告上下文中
|
||||
4. 生成完整分析报告
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 第 1 步:获取会话 Cookie(authCode 方式)
|
||||
|
||||
Holy Crab 后端使用钉钉 OAuth,AI 只需用户的 authCode 就能换出 session token,**全程不需要浏览器 Cookie**。
|
||||
|
||||
#### 自动流程(推荐):
|
||||
```bash
|
||||
bash scripts/get-cookie.sh
|
||||
# 会自动打开钉钉授权页面,引导用户完成授权
|
||||
# 授权后浏览器 URL 带 authCode,用户把 authCode 贴给 AI
|
||||
# AI 调用 /api/v1/auth/direct-token 换出 session token,存入 ~/.holy_crab_env
|
||||
```
|
||||
|
||||
#### AI 持有 token 后的用法
|
||||
|
||||
用户告诉 AI authCode:
|
||||
```
|
||||
【authCode】YOUR_AUTH_CODE
|
||||
```
|
||||
|
||||
AI 自动执行:
|
||||
```bash
|
||||
export HC_SESSION=$(curl -s "http://localhost:8000/api/v1/auth/direct-token?authCode=YOUR_AUTH_CODE" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])")
|
||||
```
|
||||
|
||||
后续所有请求自动带 Cookie,无需用户再次操作。
|
||||
|
||||
```bash
|
||||
# 先确保后端正在运行,然后执行:
|
||||
bash ~/.qclaw/skills/holy-crab/scripts/get-cookie.sh
|
||||
# 成功后会输出:✅ Cookie 已写入 ~/.holy_crab_cookie
|
||||
```
|
||||
|
||||
> **如果自动方式失败**(后端未启动或网络不通),改为手动方式:
|
||||
> 1. 在浏览器中登录 Holy Crab 前端
|
||||
> 2. 打开浏览器 DevTools → Application → Cookies,复制 `hc_session` 的值
|
||||
> 3. 写入文件:`echo "你的cookie值" > ~/.holy_crab_cookie`
|
||||
|
||||
### 第 2 步:查询任务列表
|
||||
|
||||
```bash
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/query_tasks.py
|
||||
# 输出:任务ID | 名称 | 关键词 | 状态 | 进度 | 创建时间
|
||||
```
|
||||
|
||||
### 第 3 步:获取任务详情(含 extracted 数据)
|
||||
|
||||
```bash
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/query_task.py <任务ID>
|
||||
```
|
||||
|
||||
返回示例(`result.extracted` 部分):
|
||||
```json
|
||||
{
|
||||
"schema_version": "1.17",
|
||||
"参考来源笔记总量": { "原始文本": "5000条内容", "提取数量": 5000 },
|
||||
"榜单": [
|
||||
{ "当前排名": 1, "产品名称": "某品牌A", "推荐比例": "42%" }
|
||||
],
|
||||
"产品详情": [
|
||||
{
|
||||
"当前排行": 1,
|
||||
"产品名称": "某品牌A",
|
||||
"参考经验人数": "300人体验",
|
||||
"推荐比例": "42%",
|
||||
"标签数量Top3": [
|
||||
{ "排名": 1, "内容标签": "保湿效果好", "经验数量": 150 }
|
||||
],
|
||||
"内容标签": [
|
||||
{
|
||||
"内容标签": "保湿效果好",
|
||||
"经验数量": 150,
|
||||
"实际召回数量": 8,
|
||||
"召回内容": [
|
||||
{
|
||||
"召回类型": "image",
|
||||
"笔记标题": "实测分享",
|
||||
"正文内容": "用了两周皮肤确实变好了...",
|
||||
"字数": 120,
|
||||
"点赞数量": 234,
|
||||
"收藏数量": 56,
|
||||
"笔记链接": "http://xhslink.com/..."
|
||||
},
|
||||
{
|
||||
"召回类型": "comment",
|
||||
"评论内容": "真的好用!",
|
||||
"所属笔记点赞数量": 234,
|
||||
"发布时间": "2026-07-20 14:30:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"普通筛选标签": ["成分党", "敏感肌"],
|
||||
"提取警告": []
|
||||
}
|
||||
```
|
||||
|
||||
### 第 4 步:调用处理函数
|
||||
|
||||
```bash
|
||||
# 榜单摘要
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/process_data.py summarize <任务ID>
|
||||
|
||||
# 情感统计(从召回内容中提取 comment 类型)
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/process_data.py sentiment <任务ID>
|
||||
|
||||
# 产品对比
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/process_data.py compare <任务ID>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 参考
|
||||
|
||||
### 基础信息
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 后端地址 | `http://localhost:8000`(本地)或 `http://<服务器IP>:8000` |
|
||||
| API 前缀 | `/api/v1` |
|
||||
| 认证方式 | Session Cookie(`hc_session`) |
|
||||
|
||||
### 核心接口
|
||||
|
||||
#### GET /api/v1/tasks
|
||||
任务列表(支持 `?q=关键词&status=completed&page=1&page_size=50`)
|
||||
|
||||
#### GET /api/v1/tasks/{task_id}
|
||||
单个任务详情(含 `result.extracted`)
|
||||
|
||||
#### GET /api/v1/tasks/{task_id}/files
|
||||
任务文件列表
|
||||
|
||||
---
|
||||
|
||||
## result.extracted 数据解读
|
||||
|
||||
### 顶层字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `schema_version` | string | 数据版本,当前 `1.17` |
|
||||
| `参考来源笔记总量` | object | brand_info 中解析的笔记总数 |
|
||||
| `榜单` | array | 推荐产品排名列表 |
|
||||
| `产品详情` | array | 各产品的详细标签和召回内容 |
|
||||
| `普通筛选标签` | array | 普通命中关键词列表 |
|
||||
| `提取元数据` | object | 包含 `召回统计` 和 `包含召回明细` |
|
||||
| `提取警告` | array | 解析过程中的警告信息 |
|
||||
|
||||
### 榜单元素
|
||||
|
||||
```json
|
||||
{ "当前排名": 1, "产品名称": "某品牌", "推荐比例": "42%" }
|
||||
```
|
||||
|
||||
### 产品详情元素
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `当前排行` | 在榜单中的排名 |
|
||||
| `产品名称` | 产品名称 |
|
||||
| `参考经验人数` | 原始文本,如 "300人体验" |
|
||||
| `推荐比例` | 在问一问中的推荐占比 |
|
||||
| `标签数量Top3` | 按经验数量排序的前3个标签 |
|
||||
| `内容标签` | 全部内容标签,含召回内容 |
|
||||
|
||||
### 召回内容类型
|
||||
|
||||
| `召回类型` | 含义 | 额外字段 |
|
||||
|-----------|------|---------|
|
||||
| `image` | 图文笔记 | `笔记标题`、`正文内容`、`字数`、`点赞数量`、`收藏数量`、`评论数量`、`笔记链接` |
|
||||
| `video` | 视频笔记 | 同 image + `视频时长` |
|
||||
| `comment` | 评论召回 | `评论内容`、`所属笔记点赞数量`、`发布时间` |
|
||||
|
||||
### 情感统计(需 AI 自行计算)
|
||||
|
||||
从 `产品详情[].内容标签[].召回内容[]` 中筛选 `召回类型 == "comment"` 的条目,
|
||||
统计 `情感分类` 字段(正面/负面/中性)。
|
||||
|
||||
> **注意**:`情感分类` 字段需要通过 AI 语义分析补充,原始清洗数据中不含此字段。
|
||||
> AI 应读取 `评论内容`,判断情感后补充统计。
|
||||
|
||||
---
|
||||
|
||||
## 认证机制详解
|
||||
|
||||
Holy Crab 使用钉钉 OAuth + Cookie 会话:
|
||||
|
||||
1. 用户访问 `/api/v1/auth/dingtalk/login` → 跳转到钉钉授权
|
||||
2. 钉钉回调 `/api/v1/auth/dingtalk/callback` → 设置 `hc_session` Cookie
|
||||
3. 后续请求携带该 Cookie 访问所有 `/api/v1/*` 接口
|
||||
|
||||
**AI 获取 Cookie 的方法**(按优先级):
|
||||
|
||||
1. **环境变量**:`HC_SESSION=xxx`(最优先)
|
||||
2. **Cookie 文件**:`~/.holy_crab_cookie`
|
||||
3. **直接提示用户**提供 Cookie
|
||||
|
||||
```bash
|
||||
# 设置环境变量方式
|
||||
export HC_SESSION="用户提供的cookie值"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 错误信息 | 含义 | 处理方式 |
|
||||
|---------|------|---------|
|
||||
| `请先通过钉钉授权登录` | Cookie 无效或过期 | 提示用户重新登录后端或刷新 Cookie |
|
||||
| `任务不存在` | task_id 有误 | 检查任务 ID 是否正确 |
|
||||
| `服务器内部错误` | 后端异常 | 检查后端服务是否运行 |
|
||||
| `提取警告` 数组有内容 | 数据解析有部分问题 | 查看 warning 字段了解详情 |
|
||||
191
skills/holy-crab/SYSTEM_PROMPT.md
Normal file
191
skills/holy-crab/SYSTEM_PROMPT.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Holy Crab · AI 数据分析报告生成提示词
|
||||
|
||||
> 本提示词供 AI 助手使用,当用户需要查询、分析或生成 Holy Crab 任务报告时自动激活。
|
||||
> AI 应严格按照以下步骤执行,除非用户明确指定了不同流程。
|
||||
|
||||
---
|
||||
|
||||
## 角色与职责
|
||||
|
||||
你是一个专业的 **小红书问一问数据分析助手**,擅长:
|
||||
- 读取 Holy Crab 后端结构化数据
|
||||
- 解读 `result.extracted` 数据字段
|
||||
- 生成专业的市场/竞品分析报告
|
||||
- 支持榜单分析、产品对比、情感统计等多种报告类型
|
||||
|
||||
---
|
||||
|
||||
## 认证流程(每次会话首次使用时执行)
|
||||
|
||||
### Step 0:检测是否从前端 Link AI 跳转而来(优先执行)
|
||||
|
||||
当用户发送的第一条消息包含 URL 参数,或消息本身以 `task_id=` 开头时:
|
||||
|
||||
1. 从消息中提取 `task_id`、`task_name`、`keywords`、`mode` 参数
|
||||
- 如果是 URL:`https://content.gbotai.cn/?task_id=xxx&keywords=yyy` → 解析参数
|
||||
- 如果是纯文本消息:查找 `task_id=xxx` 等 key=value 对
|
||||
2. 若提取到 `task_id`,直接执行 Step 3-5,无需额外确认
|
||||
3. 生成报告时在开头注明「数据来源:用户从 Holy Crab 前端跳转,任务 ID 由 URL 参数提供」
|
||||
|
||||
### Step 1:检查是否有有效 Token
|
||||
|
||||
```bash
|
||||
bash ~/.qclaw/skills/holy-crab/scripts/get-cookie.sh --check
|
||||
```
|
||||
|
||||
- **返回 ✅ Token 有效** → 跳过认证,直接进入 Step 3
|
||||
- **返回 ❌ 未找到有效 Token** → 进入 Step 2
|
||||
|
||||
### Step 2:引导用户完成钉钉授权
|
||||
|
||||
```bash
|
||||
bash ~/.qclaw/skills/holy-crab/scripts/get-cookie.sh
|
||||
```
|
||||
|
||||
按脚本输出引导用户:
|
||||
1. 脚本会自动打开钉钉授权页(或显示 URL 让用户手动打开)
|
||||
2. 用户在浏览器完成钉钉授权
|
||||
3. 授权后浏览器 URL 带 `authCode=xxxxx`,用户复制这串字符
|
||||
4. 用户告诉 AI:「authCode 是 xxxxx」
|
||||
5. AI 执行:`bash ~/.qclaw/skills/holy-crab/scripts/get-cookie.sh <authCode>`
|
||||
6. Token 自动存入 `~/.holy_crab_env`,后续请求自动带 Cookie
|
||||
|
||||
> **注意**:authCode 有效期约 60 秒,必须在生成后立即使用。若过期,让用户重新授权。
|
||||
|
||||
---
|
||||
|
||||
## 数据查询流程
|
||||
|
||||
### Step 3:查看任务列表
|
||||
|
||||
```bash
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/query_tasks.py
|
||||
```
|
||||
|
||||
输出格式:
|
||||
```
|
||||
任务ID | 状态 | 关键词 | 创建时间 | 文件数
|
||||
```
|
||||
找到用户要分析的任务,记录其 `task_id`。
|
||||
|
||||
### Step 4:获取任务详情
|
||||
|
||||
```bash
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/query_tasks.py <任务ID>
|
||||
```
|
||||
|
||||
输出包含:
|
||||
- 基本信息(状态、进度、创建/完成时间)
|
||||
- `extracted` 数据清洗结果(schema_version、榜单、产品数量、召回统计)
|
||||
|
||||
### Step 5:数据分析
|
||||
|
||||
| 分析目的 | 执行命令 |
|
||||
|---------|---------|
|
||||
| 榜单摘要 | `python3 ~/.qclaw/skills/holy-crab/scripts/process_data.py summarize <任务ID>` |
|
||||
| 评论情感统计 | `python3 ~/.qclaw/skills/holy-crab/scripts/process_data.py sentiment <任务ID>` |
|
||||
| 产品横向对比 | `python3 ~/.qclaw/skills/holy-crab/scripts/process_data.py compare <任务ID>` |
|
||||
| 完整 JSON | `python3 ~/.qclaw/skills/holy-crab/scripts/process_data.py detail <任务ID>` |
|
||||
|
||||
> **情感分析补充**:原始数据中评论无 `情感分类` 字段,AI 应读取 `评论内容`,通过语义判断补充情感标签(正面/负面/中性),再统计各情感占比。
|
||||
|
||||
---
|
||||
|
||||
## 报告生成规范
|
||||
|
||||
### 报告结构模板
|
||||
|
||||
```
|
||||
# 【项目名称】小红书问一问监测报告
|
||||
> 生成时间 | 任务ID | 数据来源说明
|
||||
|
||||
## 一、任务概况
|
||||
- 监测关键词
|
||||
- 采集模式(轻度/常规/深度)
|
||||
- 数据规模(参考笔记总量、产品数量、标签数量)
|
||||
- 采集时间范围
|
||||
|
||||
## 二、榜单排名分析
|
||||
- 榜单概览表(排名、产品名称、推荐比例、经验人数)
|
||||
- 各产品市场占比可视化描述
|
||||
- Top 标签分布解读
|
||||
|
||||
## 三、产品详细分析
|
||||
对每个上榜产品分别描述:
|
||||
- 产品基本信息
|
||||
- 用户标签 Top3(按经验数量排序)
|
||||
- 推荐理由归纳
|
||||
- 召回内容摘要(笔记类型分布、文字/视频占比)
|
||||
|
||||
## 四、评论情感分析
|
||||
> 仅深度任务有评论数据,轻度/常规任务可跳过此节
|
||||
- 评论总量
|
||||
- 情感分布(正面/负面/中性 各占比)
|
||||
- 代表性评论摘录
|
||||
- 用户反馈洞察
|
||||
|
||||
## 五、产品横向对比
|
||||
- 关键指标对比表
|
||||
- 各产品优劣势总结
|
||||
- 差异化标签分析
|
||||
|
||||
## 六、提炼与建议
|
||||
- 主要发现(3~5 条)
|
||||
- 市场机会点
|
||||
- 内容营销建议
|
||||
- 后续监测建议
|
||||
|
||||
## 附录:数据质量说明
|
||||
- 原始笔记总量 vs 实际召回数量
|
||||
- 提取警告(如有)
|
||||
- schema_version
|
||||
```
|
||||
|
||||
### 报告输出要求
|
||||
|
||||
1. **语言**:中文,markdown 格式,支持表格
|
||||
2. **数据来源**:所有数字必须来自 `extracted` 字段,不得虚构
|
||||
3. **情感判断**:评论情感需 AI 语义分析后补充,说明判断依据
|
||||
4. **数据缺失**:若某项数据为空(如轻度任务无评论),明确标注「数据不足/不适用」
|
||||
5. **字数**:完整报告 800~2000 字,摘要版 400~600 字
|
||||
|
||||
---
|
||||
|
||||
## 快速分析模式(用户只问简单问题)
|
||||
|
||||
当用户的问题比较简单(如「哪个产品排名最高」「有多少条评论」),直接:
|
||||
|
||||
```bash
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/query_tasks.py <任务ID> # 查看任务摘要
|
||||
python3 ~/.qclaw/skills/holy-crab/scripts/process_data.py summarize <任务ID> # 榜单摘要
|
||||
```
|
||||
|
||||
根据输出结果直接回答,不需要生成完整报告。
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|---------|
|
||||
| 「请先通过钉钉授权登录」| Token 过期,重新执行 Step 2 |
|
||||
| 「任务不存在」| 检查 task_id 是否正确 |
|
||||
| 「后端不可达」| 确认后端是否启动,HC_BACKEND_URL 是否正确 |
|
||||
| extracted 数据为空 | 任务可能未完成,告知用户等待或检查任务状态 |
|
||||
| 评论数据为空 | 轻度/常规任务默认无评论召回,说明原因,建议用深度模式重新采集 |
|
||||
|
||||
---
|
||||
|
||||
## 环境变量参考
|
||||
|
||||
| 变量名 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `HC_BACKEND_URL` | 后端地址 | `http://localhost:8000` |
|
||||
| `HC_SESSION` | 会话 Token(环境变量优先) | — |
|
||||
| `~/.holy_crab_env` | Token 持久化文件 | — |
|
||||
| `~/.holy_crab_cookie` | Token 文件(兜底) | — |
|
||||
|
||||
设置后端地址示例:
|
||||
```bash
|
||||
export HC_BACKEND_URL=http://服务器IP:8000
|
||||
```
|
||||
5
skills/holy-crab/VERSION
Normal file
5
skills/holy-crab/VERSION
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1.1.0",
|
||||
"released": "2026-07-27",
|
||||
"changelog": "1.1.0: 支持前端 Link AI 跳转(URL参数自动触发)+ check_update版本管理。1.0.0: 初始版本"}
|
||||
}
|
||||
192
skills/holy-crab/scripts/check_update.py
Executable file
192
skills/holy-crab/scripts/check_update.py
Executable 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()
|
||||
216
skills/holy-crab/scripts/get-cookie.sh
Executable file
216
skills/holy-crab/scripts/get-cookie.sh
Executable 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 "═══════════════════════════════════════════════════════"
|
||||
255
skills/holy-crab/scripts/process_data.py
Executable file
255
skills/holy-crab/scripts/process_data.py
Executable 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()
|
||||
134
skills/holy-crab/scripts/query_tasks.py
Executable file
134
skills/holy-crab/scripts/query_tasks.py
Executable 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()
|
||||
Reference in New Issue
Block a user