Spaces:
Running
Running
Fin-DataPilot Deploy Bot commited on
Commit ·
f992c25
0
Parent(s):
ci: 5d4fb19
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env.example +38 -0
- .gitignore +7 -0
- Dockerfile +30 -0
- README.md +26 -0
- Skills/announcement-search/README.md +252 -0
- Skills/announcement-search/SKILL.md +210 -0
- Skills/announcement-search/references/api.md +175 -0
- Skills/announcement-search/scripts/__main__.py +234 -0
- Skills/announcement-search/scripts/announcement_search.py +323 -0
- Skills/announcement-search/scripts/config.example.json +39 -0
- Skills/announcement-search/scripts/config.py +138 -0
- Skills/announcement-search/scripts/example_usage.py +253 -0
- Skills/announcement-search/scripts/requirements.txt +10 -0
- Skills/announcement-search/scripts/setup.py +69 -0
- Skills/announcement-search/scripts/test_basic.py +335 -0
- Skills/announcement-search/scripts/test_queries.txt +52 -0
- Skills/announcement-search/scripts/utils.py +206 -0
- Skills/financial-query/LICENSE.txt +21 -0
- Skills/financial-query/SKILL.md +224 -0
- Skills/financial-query/scripts/cli.py +300 -0
- Skills/news-search/README.md +277 -0
- Skills/news-search/SKILL.md +316 -0
- Skills/news-search/references/api.md +111 -0
- Skills/news-search/scripts/__main__.py +16 -0
- Skills/news-search/scripts/config.example.json +24 -0
- Skills/news-search/scripts/config.py +271 -0
- Skills/news-search/scripts/example_usage.py +269 -0
- Skills/news-search/scripts/news_search.py +792 -0
- Skills/news-search/scripts/requirements.txt +2 -0
- Skills/news-search/scripts/setup.py +41 -0
- Skills/news-search/scripts/test_basic.py +223 -0
- Skills/news-search/scripts/test_queries.txt +549 -0
- Skills/report-search/README.md +280 -0
- Skills/report-search/SKILL.md +321 -0
- Skills/report-search/references/api.md +235 -0
- Skills/report-search/scripts/__main__.py +16 -0
- Skills/report-search/scripts/api_client.py +261 -0
- Skills/report-search/scripts/cli.py +633 -0
- Skills/report-search/scripts/config.example.json +26 -0
- Skills/report-search/scripts/config.py +206 -0
- Skills/report-search/scripts/data_processor.py +600 -0
- Skills/report-search/scripts/example_usage.py +343 -0
- Skills/report-search/scripts/requirements.txt +3 -0
- Skills/report-search/scripts/research_report_search.py +16 -0
- Skills/report-search/scripts/setup.py +138 -0
- Skills/report-search/scripts/test_basic.py +418 -0
- Skills/report-search/scripts/test_queries.txt +43 -0
- app/__init__.py +2 -0
- app/agent/__init__.py +0 -0
- app/agent/graph.py +180 -0
.env.example
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ===== LLM =====
|
| 2 |
+
# LLM provider: minimax | openai | anthropic | custom (OpenAI-compatible)
|
| 3 |
+
LLM_PROVIDER=minimax
|
| 4 |
+
LLM_BASE_URL=https://api.minimaxi.com/v1
|
| 5 |
+
LLM_API_KEY=your-api-key-here
|
| 6 |
+
LLM_MODEL=MiniMax-M3
|
| 7 |
+
LLM_TEMPERATURE=0.2
|
| 8 |
+
LLM_MAX_TOKENS=4096
|
| 9 |
+
|
| 10 |
+
# ===== iWencai / 问财 API (used by 4 skills) =====
|
| 11 |
+
IWENCAI_API_KEY=your-iwencai-api-key-here
|
| 12 |
+
# IWENCAI_SKILL_ID_OVERRIDES=financial-query=hithink-financial-query,news-search=news-search
|
| 13 |
+
|
| 14 |
+
# ===== Server =====
|
| 15 |
+
DATA_PILOT_HOST=0.0.0.0
|
| 16 |
+
DATA_PILOT_PORT=7860
|
| 17 |
+
DATA_PILOT_ENV=development
|
| 18 |
+
API_KEY= # optional; if set, clients must send X-API-Key
|
| 19 |
+
|
| 20 |
+
# ===== CORS =====
|
| 21 |
+
CORS_ALLOW_ORIGINS=http://localhost:5173,http://localhost:3000,http://127.0.0.1:5173
|
| 22 |
+
|
| 23 |
+
# ===== Storage =====
|
| 24 |
+
# Set to a Turso URL to use libSQL in production; leave empty to use local SQLite
|
| 25 |
+
TURSO_DATABASE_URL=
|
| 26 |
+
TURSO_AUTH_TOKEN=
|
| 27 |
+
LOCAL_SQLITE_PATH=./data/findatapilot.db
|
| 28 |
+
|
| 29 |
+
# ===== Agent =====
|
| 30 |
+
AGENT_MAX_REFLECT_ROUNDS=5
|
| 31 |
+
AGENT_MAX_PARALLEL_SKILLS=3
|
| 32 |
+
AGENT_ENABLE_REFLECTION=true
|
| 33 |
+
|
| 34 |
+
# ===== Observability =====
|
| 35 |
+
LANGFUSE_PUBLIC_KEY=
|
| 36 |
+
LANGFUSE_SECRET_KEY=
|
| 37 |
+
LANGFUSE_HOST=
|
| 38 |
+
LOG_LEVEL=INFO
|
.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
.env
|
| 4 |
+
data/
|
| 5 |
+
logs/
|
| 6 |
+
.venv/
|
| 7 |
+
.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# System dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
+
curl ca-certificates git \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Python dependencies
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# Application code
|
| 15 |
+
COPY app ./app
|
| 16 |
+
|
| 17 |
+
# Data + log dirs (HF Spaces can persist /data across rebuilds)
|
| 18 |
+
RUN mkdir -p /data/outputs /data/logs
|
| 19 |
+
|
| 20 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 21 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 22 |
+
DATA_PILOT_PORT=7860 \
|
| 23 |
+
DATA_PILOT_HOST=0.0.0.0
|
| 24 |
+
|
| 25 |
+
EXPOSE 7860
|
| 26 |
+
|
| 27 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
| 28 |
+
CMD curl -fsS http://localhost:7860/api/health || exit 1
|
| 29 |
+
|
| 30 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
|
README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Fin-DataPilot
|
| 3 |
+
emoji: 📊
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: true
|
| 9 |
+
license: mit
|
| 10 |
+
short_description: NL financial data agent (LangGraph + 4 iWencai skills)
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# Fin-DataPilot Backend
|
| 14 |
+
|
| 15 |
+
Agent-based natural-language financial data platform. See root `README.md` for the full design.
|
| 16 |
+
|
| 17 |
+
- Health: `GET /api/health`
|
| 18 |
+
- Skills: `GET /api/skills` (and `PATCH /api/skills/{name}` to enable/disable)
|
| 19 |
+
- Sessions: `POST /api/sessions`, `GET /api/sessions`, `GET /api/sessions/{id}`
|
| 20 |
+
- Chat (SSE): `POST /api/agent/chat/stream`
|
| 21 |
+
|
| 22 |
+
Configure secrets in the Space's "Variables and secrets" tab:
|
| 23 |
+
- `LLM_BASE_URL` (default `https://api.minimaxi.com/v1`)
|
| 24 |
+
- `LLM_API_KEY`
|
| 25 |
+
- `IWENCAI_API_KEY`
|
| 26 |
+
- `CORS_ALLOW_ORIGINS` (your GitHub Pages / Vercel frontend origin)
|
Skills/announcement-search/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 公告搜索技能
|
| 2 |
+
|
| 3 |
+
## 简介
|
| 4 |
+
公告搜索技能是一个金融公告搜索引擎,通过调用同花顺问财的公告搜索接口,帮助用户查询A股、港股、基金、ETF等金融标的的最新公告信息。
|
| 5 |
+
|
| 6 |
+
## 功能特性
|
| 7 |
+
- 支持A股、港股、基金、ETF等金融标的公告查询
|
| 8 |
+
- 覆盖定期财务报告、分红派息、回购增持、资产重组等多种公告类型
|
| 9 |
+
- 智能查询处理,自动拆解复杂查询
|
| 10 |
+
- 友好的命令行接口(CLI)
|
| 11 |
+
- 支持批量搜索和数据导出
|
| 12 |
+
- 完善的数据处理和错误处理机制
|
| 13 |
+
|
| 14 |
+
## 快速开始
|
| 15 |
+
|
| 16 |
+
### 环境要求
|
| 17 |
+
- Python 3.7+
|
| 18 |
+
- 有效的同花顺问财API Key
|
| 19 |
+
|
| 20 |
+
### 安装步骤
|
| 21 |
+
1. 克隆或下载本技能到本地
|
| 22 |
+
2. 安装依赖:
|
| 23 |
+
```bash
|
| 24 |
+
pip install -r scripts/requirements.txt
|
| 25 |
+
```
|
| 26 |
+
3. 设置环境变量:
|
| 27 |
+
|
| 28 |
+
**macOS / Linux (bash/zsh):**
|
| 29 |
+
```bash
|
| 30 |
+
export IWENCAI_API_KEY="your_api_key_here"
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
**Windows (PowerShell):**
|
| 34 |
+
```powershell
|
| 35 |
+
$env:IWENCAI_API_KEY="your_api_key_here"
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
**Windows (CMD):**
|
| 39 |
+
```cmd
|
| 40 |
+
set IWENCAI_API_KEY=your_api_key_here
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
### 基本使用
|
| 44 |
+
```bash
|
| 45 |
+
# 使用CLI搜索公告
|
| 46 |
+
python scripts/__main__.py "贵州茅台 公告"
|
| 47 |
+
|
| 48 |
+
# 或者直接运行
|
| 49 |
+
announcement-search "上市公司业绩预告"
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## 使用示例
|
| 53 |
+
|
| 54 |
+
### 搜索特定公司公告
|
| 55 |
+
```bash
|
| 56 |
+
announcement-search "贵州茅台最近一个月的公告"
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
### 查询业绩预告
|
| 60 |
+
```bash
|
| 61 |
+
announcement-search "上市公司业绩预告"
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
### 搜索分红公告
|
| 65 |
+
```bash
|
| 66 |
+
announcement-search "分红派息公告"
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
### 批量搜索
|
| 70 |
+
```bash
|
| 71 |
+
announcement-search --input queries.txt --output results.csv
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
## 配置说明
|
| 75 |
+
|
| 76 |
+
### 环境变量配置
|
| 77 |
+
```bash
|
| 78 |
+
# 设置API Key
|
| 79 |
+
export IWENCAI_API_KEY="your_api_key"
|
| 80 |
+
|
| 81 |
+
# 可选:设置日志级别
|
| 82 |
+
export LOG_LEVEL="INFO"
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
### 配置文件
|
| 86 |
+
技能支持JSON配置文件,示例配置见`scripts/config.example.json`。
|
| 87 |
+
|
| 88 |
+
## 直接API调用(curl示例)
|
| 89 |
+
|
| 90 |
+
### curl调用示例
|
| 91 |
+
```bash
|
| 92 |
+
# 使用环境变量中的API Key
|
| 93 |
+
curl -X POST "https://openapi.iwencai.com/v1/comprehensive/search" \
|
| 94 |
+
-H "Content-Type: application/json" \
|
| 95 |
+
-H "Authorization: Bearer $IWENCAI_API_KEY" \
|
| 96 |
+
-H "X-Claw-Call-Type: normal" \
|
| 97 |
+
-H "X-Claw-Skill-Id: announcement-search" \
|
| 98 |
+
-H "X-Claw-Skill-Version: 1.0.0" \
|
| 99 |
+
-H "X-Claw-Plugin-Id: none" \
|
| 100 |
+
-H "X-Claw-Plugin-Version: none" \
|
| 101 |
+
-H "X-Claw-Trace-Id: $(python3 -c 'import secrets; print(secrets.token_hex(32))')" \
|
| 102 |
+
-d '{
|
| 103 |
+
"channels": ["announcement"],
|
| 104 |
+
"app_id": "AIME_SKILL",
|
| 105 |
+
"query": "贵州茅台 公告"
|
| 106 |
+
}'
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
### Windows PowerShell curl示例
|
| 110 |
+
```powershell
|
| 111 |
+
# 在PowerShell中设置环境变量后调用
|
| 112 |
+
$traceId = python -c "import secrets; print(secrets.token_hex(32))"
|
| 113 |
+
curl.exe -X POST "https://openapi.iwencai.com/v1/comprehensive/search" `
|
| 114 |
+
-H "Content-Type: application/json" `
|
| 115 |
+
-H "Authorization: Bearer $env:IWENCAI_API_KEY" `
|
| 116 |
+
-H "X-Claw-Call-Type: normal" `
|
| 117 |
+
-H "X-Claw-Skill-Id: announcement-search" `
|
| 118 |
+
-H "X-Claw-Skill-Version: 1.0.0" `
|
| 119 |
+
-H "X-Claw-Plugin-Id: none" `
|
| 120 |
+
-H "X-Claw-Plugin-Version: none" `
|
| 121 |
+
-H "X-Claw-Trace-Id: $traceId" `
|
| 122 |
+
-d '{
|
| 123 |
+
"channels": ["announcement"],
|
| 124 |
+
"app_id": "AIME_SKILL",
|
| 125 |
+
"query": "贵州茅台 公告"
|
| 126 |
+
}'
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
## 命令行参数
|
| 130 |
+
|
| 131 |
+
### 基本参数
|
| 132 |
+
- `query`: 搜索关键词(字符串)
|
| 133 |
+
- `--input`, `-i`: 输入文件路径(用于批量搜索)
|
| 134 |
+
- `--output`, `-o`: 输出文件路径
|
| 135 |
+
- `--format`, `-f`: 输出格式(csv, json, txt)
|
| 136 |
+
- `--limit`, `-l`: 结果数量限制
|
| 137 |
+
- `--verbose`, `-v`: 详细输出模式
|
| 138 |
+
|
| 139 |
+
### 使用示例
|
| 140 |
+
```bash
|
| 141 |
+
# 基本搜索,输出到控制台
|
| 142 |
+
announcement-search "回购增持"
|
| 143 |
+
|
| 144 |
+
# 搜索并保存为CSV文件
|
| 145 |
+
announcement-search "资产重组" --output results.csv --format csv
|
| 146 |
+
|
| 147 |
+
# 批量搜索
|
| 148 |
+
announcement-search --input queries.txt --output results.json --format json
|
| 149 |
+
|
| 150 |
+
# 限制结果数量
|
| 151 |
+
announcement-search "定期报告" --limit 10
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
## 数据格式
|
| 155 |
+
|
| 156 |
+
### 输入格式
|
| 157 |
+
- 单次搜索:直接提供查询字符串
|
| 158 |
+
- 批量搜索:文本文件,每行一个查询
|
| 159 |
+
|
| 160 |
+
### 输出格式
|
| 161 |
+
技能支持多种输出格式:
|
| 162 |
+
|
| 163 |
+
#### CSV格式
|
| 164 |
+
```csv
|
| 165 |
+
title,summary,url,publish_date
|
| 166 |
+
"某某公司2023年度业绩预告","公司预计2023年度净利润同比增长50%-70%","https://example.com/announcement/12345","2024-01-15 09:30:00"
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
#### JSON格式
|
| 170 |
+
```json
|
| 171 |
+
[
|
| 172 |
+
{
|
| 173 |
+
"title": "某某公司2023年度业绩预告",
|
| 174 |
+
"summary": "公司预计2023年度净利润同比增长50%-70%",
|
| 175 |
+
"url": "https://example.com/announcement/12345",
|
| 176 |
+
"publish_date": "2024-01-15 09:30:00"
|
| 177 |
+
}
|
| 178 |
+
]
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
#### 文本格式
|
| 182 |
+
```
|
| 183 |
+
标题:某某公司2023年度业绩预告
|
| 184 |
+
摘要:公司预计2023年度净利润同比增长50%-70%
|
| 185 |
+
链接:https://example.com/announcement/12345
|
| 186 |
+
发布时间:2024-01-15 09:30:00
|
| 187 |
+
---
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
## 错误处理
|
| 191 |
+
|
| 192 |
+
技能包含完善的错误处理机制:
|
| 193 |
+
|
| 194 |
+
### 常见错误
|
| 195 |
+
1. **API认证失败**: 检查API Key是否正确设置
|
| 196 |
+
2. **网络连接错误**: 检查网络��接
|
| 197 |
+
3. **参数错误**: 检查输入参数格式
|
| 198 |
+
4. **API限制**: 避免频繁请求
|
| 199 |
+
|
| 200 |
+
### 错误信息示例
|
| 201 |
+
```bash
|
| 202 |
+
# API认证失败
|
| 203 |
+
错误:API认证失败,请检查IWENCAI_API_KEY环境变量
|
| 204 |
+
|
| 205 |
+
# 网络错误
|
| 206 |
+
错误:网络连接失败,请检查网络连接
|
| 207 |
+
|
| 208 |
+
# 参数错误
|
| 209 |
+
错误:查询参数不能为空
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
## 开发指南
|
| 213 |
+
|
| 214 |
+
### 项目结构
|
| 215 |
+
```
|
| 216 |
+
announcement-search/
|
| 217 |
+
├── README.md # 本文件
|
| 218 |
+
├── SKILL.md # 技能详细文档
|
| 219 |
+
├── references/
|
| 220 |
+
│ └── api.md # API接口文档
|
| 221 |
+
└── scripts/
|
| 222 |
+
├── __main__.py # CLI入口点
|
| 223 |
+
├── config.py # 配置文件
|
| 224 |
+
├── config.example.json # 配置示例文件
|
| 225 |
+
├── requirements.txt # Python依赖
|
| 226 |
+
├── setup.py # 安装脚本
|
| 227 |
+
├── announcement_search.py # 核心搜索逻辑
|
| 228 |
+
└── utils.py # 工具函数
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
### 添加新功能
|
| 232 |
+
1. 在`announcement_search.py`中添加新的搜索逻辑
|
| 233 |
+
2. 在`__main__.py`中添加对应的命令行参数
|
| 234 |
+
3. 更新文档说明
|
| 235 |
+
|
| 236 |
+
## 许可证
|
| 237 |
+
|
| 238 |
+
本技能遵循MIT许可证。
|
| 239 |
+
|
| 240 |
+
## 支持与反馈
|
| 241 |
+
|
| 242 |
+
如有问题或建议,请通过以下方式联系:
|
| 243 |
+
- 提交Issue
|
| 244 |
+
- 发送邮件至维护者
|
| 245 |
+
|
| 246 |
+
## 更新日志
|
| 247 |
+
|
| 248 |
+
### v1.0.0
|
| 249 |
+
- 初始版本发布
|
| 250 |
+
- 支持基本公告搜索功能
|
| 251 |
+
- 提供CLI接口
|
| 252 |
+
- 支持多种输出格式
|
Skills/announcement-search/SKILL.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: announcement-search
|
| 3 |
+
description: 支持A股、港股、基金、ETF等金融标的公告的查询,同时公告类型包括不限于定期财务报告、分红派息、回购增持、资产重组等等。
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# 公告搜索技能
|
| 7 |
+
|
| 8 |
+
## 版本
|
| 9 |
+
当前技能版本:1.0.0
|
| 10 |
+
|
| 11 |
+
## 技能概述
|
| 12 |
+
|
| 13 |
+
本技能是一个金融公告搜索引擎,通过调用同花顺问财的公告搜索接口,帮助用户查询A股、港股、基金、ETF等金融标的的最新公告信息。支持查询的公告类型包括但不限于:定期财务报告、分红派息、回购增持、资产重组、重大合同、业绩预告等。
|
| 14 |
+
|
| 15 |
+
## API Key 获取与配置(首次使用必读)
|
| 16 |
+
|
| 17 |
+
> 首次使用 - 获取 API Key
|
| 18 |
+
> 所有技能都需要 IWENCAI_API_KEY 环境变量才能使用。 如果用户尚未配置,按以下步骤引导:
|
| 19 |
+
>
|
| 20 |
+
> 步骤 1:获取 API Key
|
| 21 |
+
> 在浏览器内打同花顺i问财SkillHub页面:https://www.iwencai.com/skillhub
|
| 22 |
+
>
|
| 23 |
+
> 步骤 2:登录
|
| 24 |
+
>
|
| 25 |
+
> 步骤 3:点击具体的Skill,打开弹窗查看详情,在安装方式-Agent用户-找到您的IWENCAI_API_KEY这一段,复制
|
| 26 |
+
>
|
| 27 |
+
> 步骤 4:配置环境变量
|
| 28 |
+
> 获取到 API Key 后,直接复制指引文字发送给AI助手,或手动设置环境变量:
|
| 29 |
+
|
| 30 |
+
### 跨平台环境变量设置
|
| 31 |
+
|
| 32 |
+
**macOS / Linux (bash/zsh):**
|
| 33 |
+
```bash
|
| 34 |
+
export IWENCAI_API_KEY="your_api_key_here"
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
**Windows (PowerShell):**
|
| 38 |
+
```powershell
|
| 39 |
+
$env:IWENCAI_API_KEY="your_api_key_here"
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
**Windows (CMD):**
|
| 43 |
+
```cmd
|
| 44 |
+
set IWENCAI_API_KEY=your_api_key_here
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
## 技能功能
|
| 48 |
+
|
| 49 |
+
### 1. 金融公告搜索
|
| 50 |
+
- 搜索各类金融公告信息
|
| 51 |
+
- 支持A股、港股、基金、ETF等金融标的
|
| 52 |
+
- 覆盖定期财务报告、分红派息、回购增持、资产重组等多种公告类型
|
| 53 |
+
- 支持中文关键词搜索
|
| 54 |
+
|
| 55 |
+
### 2. 智能查询处理
|
| 56 |
+
- 自动分析用户查询意图
|
| 57 |
+
- 根据需求决定是否拆解复杂查询为多个简单查询
|
| 58 |
+
- 示例:用户问"最近贵州茅台和五粮液有什么公告?"可以拆分为"贵州茅台 公告"和"五粮液 公告"两个查询
|
| 59 |
+
- 根据查询复杂度决定调用接口的次数
|
| 60 |
+
|
| 61 |
+
### 3. 数据评估与扩展
|
| 62 |
+
- 自动评估搜索结果是否能回答用户问题
|
| 63 |
+
- 如有必要,可调用其他技能或工具扩展数据
|
| 64 |
+
- 对搜索结果进行质量评估和相关性排序
|
| 65 |
+
|
| 66 |
+
### 4. 数据处理与返回
|
| 67 |
+
- 对搜索结果进行排序、过滤和摘要处理
|
| 68 |
+
- 生成结构化的数据结果
|
| 69 |
+
- 将处理后的数据返回给大模型,帮助回答用户问题
|
| 70 |
+
|
| 71 |
+
### 5. CLI支持
|
| 72 |
+
- 提供友好的命令行接口
|
| 73 |
+
- 支持基本搜索、批量搜索、数据导出等功能
|
| 74 |
+
- 详细的命令行文档和使用示例
|
| 75 |
+
|
| 76 |
+
## 使用场景
|
| 77 |
+
|
| 78 |
+
### 何时调用本技能
|
| 79 |
+
1. 当用户需要搜索上市公司最新公告时
|
| 80 |
+
2. 当用户查询特定金融标的的公告信息时
|
| 81 |
+
3. 当用户需要了解分红派息、回购增持等公告时
|
| 82 |
+
4. 当用户查询定期财务报告(年报、季报)时
|
| 83 |
+
5. 当用户需要获取资产重组、重大合同等公告信息时
|
| 84 |
+
|
| 85 |
+
### 使用示例
|
| 86 |
+
- "搜索贵州茅台最近一个月的公告"
|
| 87 |
+
- "查询宁德时代的业绩预告"
|
| 88 |
+
- "查看中国平安的分红公告"
|
| 89 |
+
- "搜索最近有哪些公司发布了回购计划"
|
| 90 |
+
- "查询创业板公司的年报公告"
|
| 91 |
+
|
| 92 |
+
## 技术实现
|
| 93 |
+
|
| 94 |
+
### API接口规范(遵循iwencai-skill-creator要求)
|
| 95 |
+
|
| 96 |
+
#### 基础接口信息
|
| 97 |
+
- **Base URL**: `https://openapi.iwencai.com`
|
| 98 |
+
- **接口路径**: `/v1/comprehensive/search`
|
| 99 |
+
- **请求方式**: POST(优先使用POST,符合规范要求)
|
| 100 |
+
- **认证方式**: API Key (Bearer Token),从环境变量 `IWENCAI_API_KEY` 读取
|
| 101 |
+
|
| 102 |
+
#### 必需请求头(Claw Headers)
|
| 103 |
+
所有发往问财OpenAPI网关的请求必须包含以下Header:
|
| 104 |
+
|
| 105 |
+
| Header | 取值说明 | 本技能取值 |
|
| 106 |
+
|--------|----------|------------|
|
| 107 |
+
| `X-Claw-Call-Type` | `normal`:正常请求;`retry`:失败后的重试 | `normal` |
|
| 108 |
+
| `X-Claw-Skill-Id` | 技能标识 | `announcement-search` |
|
| 109 |
+
| `X-Claw-Skill-Version` | 当前技能版本号 | `1.0.0`(与技能版本一致) |
|
| 110 |
+
| `X-Claw-Plugin-Id` | 插件ID(扩展字段) | `none` |
|
| 111 |
+
| `X-Claw-Plugin-Version` | 插件版本(扩展字段) | `none` |
|
| 112 |
+
| `X-Claw-Trace-Id` | **每次请求新生成**的64字符唯一追踪ID | 使用 `secrets.token_hex(32)` 生成 |
|
| 113 |
+
|
| 114 |
+
#### 请求参数
|
| 115 |
+
- **固定参数**: `channels: ["announcement"]`, `app_id: "AIME_SKILL"`
|
| 116 |
+
- **可变参数**: `query` (用户问句)
|
| 117 |
+
|
| 118 |
+
### 响应格式
|
| 119 |
+
API返回的`data`字段包含以下信息:
|
| 120 |
+
- `title`: 文章标题
|
| 121 |
+
- `summary`: 文章摘要
|
| 122 |
+
- `url`: 文章网址
|
| 123 |
+
- `publish_date`: 文章发布时间 (格式: YYYY-MM-DD HH:MM:SS)
|
| 124 |
+
|
| 125 |
+
### curl示例(脱敏)
|
| 126 |
+
|
| 127 |
+
**Unix/Linux/macOS (bash/zsh):**
|
| 128 |
+
```bash
|
| 129 |
+
# 使用环境变量中的API Key
|
| 130 |
+
curl -X POST "https://openapi.iwencai.com/v1/comprehensive/search" \
|
| 131 |
+
-H "Content-Type: application/json" \
|
| 132 |
+
-H "Authorization: Bearer $IWENCAI_API_KEY" \
|
| 133 |
+
-H "X-Claw-Call-Type: normal" \
|
| 134 |
+
-H "X-Claw-Skill-Id: announcement-search" \
|
| 135 |
+
-H "X-Claw-Skill-Version: 1.0.0" \
|
| 136 |
+
-H "X-Claw-Plugin-Id: none" \
|
| 137 |
+
-H "X-Claw-Plugin-Version: none" \
|
| 138 |
+
-H "X-Claw-Trace-Id: $(python3 -c 'import secrets; print(secrets.token_hex(32))' 2>/dev/null || openssl rand -hex 32)" \
|
| 139 |
+
-d '{
|
| 140 |
+
"channels": ["announcement"],
|
| 141 |
+
"app_id": "AIME_SKILL",
|
| 142 |
+
"query": "贵州茅台 公告"
|
| 143 |
+
}'
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
**Windows (PowerShell):**
|
| 147 |
+
```powershell
|
| 148 |
+
# 首先设置环境变量(如果尚未设置)
|
| 149 |
+
$env:IWENCAI_API_KEY="your_api_key_here"
|
| 150 |
+
|
| 151 |
+
# 生成64字符Trace ID
|
| 152 |
+
$traceId = -join ((48..57) + (97..102) | Get-Random -Count 64 | % { [char]$_ })
|
| 153 |
+
|
| 154 |
+
# 发送请求
|
| 155 |
+
$headers = @{
|
| 156 |
+
"Content-Type" = "application/json"
|
| 157 |
+
"Authorization" = "Bearer $env:IWENCAI_API_KEY"
|
| 158 |
+
"X-Claw-Call-Type" = "normal"
|
| 159 |
+
"X-Claw-Skill-Id" = "announcement-search"
|
| 160 |
+
"X-Claw-Skill-Version" = "1.0.0"
|
| 161 |
+
"X-Claw-Plugin-Id" = "none"
|
| 162 |
+
"X-Claw-Plugin-Version" = "none"
|
| 163 |
+
"X-Claw-Trace-Id" = $traceId
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
$body = @{
|
| 167 |
+
channels = @("announcement")
|
| 168 |
+
app_id = "AIME_SKILL"
|
| 169 |
+
query = "贵州茅台 公告"
|
| 170 |
+
} | ConvertTo-Json
|
| 171 |
+
|
| 172 |
+
Invoke-RestMethod -Uri "https://openapi.iwencai.com/v1/comprehensive/search" -Method Post -Headers $headers -Body $body
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
**通用说明:**
|
| 176 |
+
- Trace ID必须是64字符十六进制字符串,每次请求新生成
|
| 177 |
+
- API Key必须从环境变量 `IWENCAI_API_KEY` 读取,禁止硬编码
|
| 178 |
+
- 所有Claw headers必须正确设置
|
| 179 |
+
|
| 180 |
+
## 安装与使用
|
| 181 |
+
|
| 182 |
+
### 快速开始
|
| 183 |
+
1. 按照前面的"API Key 获取与配置"章节设置环境变量
|
| 184 |
+
2. 使用技能搜索公告:`announcement-search "贵州茅台 公告"`
|
| 185 |
+
|
| 186 |
+
### 配置文件(可选)
|
| 187 |
+
技能支持通过配置文件管理API Key等敏感信息,配置文件示例见`scripts/config.example.json`。
|
| 188 |
+
|
| 189 |
+
### CLI使用示例
|
| 190 |
+
```bash
|
| 191 |
+
# 基本搜索
|
| 192 |
+
announcement-search "上市公司业绩预告"
|
| 193 |
+
|
| 194 |
+
# 批量搜索(从文件读取查询)
|
| 195 |
+
announcement-search --input queries.txt --output results.csv
|
| 196 |
+
|
| 197 |
+
# 指定输出格式
|
| 198 |
+
announcement-search "分红派息" --format json
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
## 注意事项
|
| 202 |
+
|
| 203 |
+
1. **API Key安全**: 请妥善保管API Key,不要将其暴露在客户端代码中
|
| 204 |
+
2. **请求频率**: 请遵守API使用限制,避免频繁请求
|
| 205 |
+
3. **数据准确性**: 请注意数据时效性
|
| 206 |
+
4. **错误处理**: 技能包含完善的错误处理机制,会提示用户相关错误信息
|
| 207 |
+
|
| 208 |
+
## 支持与反馈
|
| 209 |
+
|
| 210 |
+
如有问题或需要技术支持,请联系技能维护者。技能会持续更新以提供更好的公告搜索体验。
|
Skills/announcement-search/references/api.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 问财公告搜索 API 文档
|
| 2 |
+
|
| 3 |
+
## 概述
|
| 4 |
+
问财公告搜索 API 提供公告信息的搜索功能,通过用户问句查询相关公告信息。
|
| 5 |
+
|
| 6 |
+
## 基础信息
|
| 7 |
+
- **Base URL**: `https://openapi.iwencai.com`
|
| 8 |
+
- **接口路径**: `/v1/comprehensive/search`
|
| 9 |
+
- **请求方式**: POST
|
| 10 |
+
- **Content-Type**: `application/json`
|
| 11 |
+
|
| 12 |
+
## 认证
|
| 13 |
+
API 使用 API Key 进行认证,需要在请求头中设置:
|
| 14 |
+
- **Header**: `Authorization: Bearer {IWENCAI_API_KEY}`
|
| 15 |
+
- **环境变量**: `IWENCAI_API_KEY` (用户侧申请的 API Key)
|
| 16 |
+
|
| 17 |
+
## 请求参数
|
| 18 |
+
|
| 19 |
+
### 请求头 (Headers)
|
| 20 |
+
| 参数名 | 类型 | 必填 | 说明 |
|
| 21 |
+
|--------|------|------|------|
|
| 22 |
+
| Content-Type | string | 是 | 必须设置为 `application/json` |
|
| 23 |
+
| Authorization | string | 是 | Bearer token 认证,格式: `Bearer {IWENCAI_API_KEY}` |
|
| 24 |
+
|
| 25 |
+
### 请求体 (Body)
|
| 26 |
+
请求体为 JSON 格式,包含以下参数:
|
| 27 |
+
|
| 28 |
+
#### 固定参数
|
| 29 |
+
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|
| 30 |
+
|--------|------|------|------|--------|
|
| 31 |
+
| channels | array | 是 | 搜索渠道类型 | `["announcement"]` |
|
| 32 |
+
| app_id | string | 是 | 应用 ID | `"AIME_SKILL"` |
|
| 33 |
+
|
| 34 |
+
#### 可变参数
|
| 35 |
+
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|
| 36 |
+
|--------|------|------|------|--------|
|
| 37 |
+
| query | string | 是 | 用户问句,搜索关键词 | `"上市公司业绩预告"` |
|
| 38 |
+
|
| 39 |
+
### 请求示例
|
| 40 |
+
```json
|
| 41 |
+
{
|
| 42 |
+
"channels": ["announcement"],
|
| 43 |
+
"app_id": "AIME_SKILL",
|
| 44 |
+
"query": "上市公司业绩预告"
|
| 45 |
+
}
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
## 响应
|
| 49 |
+
|
| 50 |
+
### 响应格式
|
| 51 |
+
响应为 JSON 格式,包含以下字段:
|
| 52 |
+
|
| 53 |
+
| 字段名 | 类型 | 说明 |
|
| 54 |
+
|--------|------|------|
|
| 55 |
+
| data | array | 返回的文章信息列表 |
|
| 56 |
+
|
| 57 |
+
### data 字段结构
|
| 58 |
+
`data` 数组中的每个元素包含以下字段:
|
| 59 |
+
|
| 60 |
+
| 字段名 | 类型 | 说明 | 格式 |
|
| 61 |
+
|--------|------|------|------|
|
| 62 |
+
| title | string | 文章标题 | - |
|
| 63 |
+
| summary | string | 文章摘要 | - |
|
| 64 |
+
| url | string | 文章网址 | URL 格式 |
|
| 65 |
+
| publish_date | string | 文章发布时间 | `YYYY-MM-DD HH:MM:SS` |
|
| 66 |
+
|
| 67 |
+
### 响应示例
|
| 68 |
+
```json
|
| 69 |
+
{
|
| 70 |
+
"data": [
|
| 71 |
+
{
|
| 72 |
+
"title": "某某公司2023年度业绩预告",
|
| 73 |
+
"summary": "公司预计2023年度净利润同比增长50%-70%",
|
| 74 |
+
"url": "https://example.com/announcement/12345",
|
| 75 |
+
"publish_date": "2024-01-15 09:30:00"
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"title": "另一家公司重大合同公告",
|
| 79 |
+
"summary": "公司与客户签订重大销售合同,金额约10亿元",
|
| 80 |
+
"url": "https://example.com/announcement/12346",
|
| 81 |
+
"publish_date": "2024-01-14 16:45:00"
|
| 82 |
+
}
|
| 83 |
+
]
|
| 84 |
+
}
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
## 错误处理
|
| 88 |
+
|
| 89 |
+
### HTTP 状态码
|
| 90 |
+
| 状态码 | 说明 |
|
| 91 |
+
|--------|------|
|
| 92 |
+
| 200 | 请求成功 |
|
| 93 |
+
| 400 | 请求参数错误 |
|
| 94 |
+
| 401 | 认证失败 |
|
| 95 |
+
| 500 | 服务器内部错误 |
|
| 96 |
+
|
| 97 |
+
### 错误响应示例
|
| 98 |
+
```json
|
| 99 |
+
{
|
| 100 |
+
"error": {
|
| 101 |
+
"code": "AUTH_FAILED",
|
| 102 |
+
"message": "API Key 无效或已过期"
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
## 使用示例
|
| 108 |
+
|
| 109 |
+
### Python 示例
|
| 110 |
+
```python
|
| 111 |
+
import requests
|
| 112 |
+
import os
|
| 113 |
+
|
| 114 |
+
# 从环境变量获取 API Key
|
| 115 |
+
api_key = os.getenv("IWENCAI_API_KEY")
|
| 116 |
+
|
| 117 |
+
# 请求头
|
| 118 |
+
headers = {
|
| 119 |
+
"Content-Type": "application/json",
|
| 120 |
+
"Authorization": f"Bearer {api_key}"
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
# 请求体
|
| 124 |
+
payload = {
|
| 125 |
+
"channels": ["announcement"],
|
| 126 |
+
"app_id": "AIME_SKILL",
|
| 127 |
+
"query": "上市公司业绩预告"
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
# 发送请求
|
| 131 |
+
response = requests.post(
|
| 132 |
+
"https://openapi.iwencai.com/v1/comprehensive/search",
|
| 133 |
+
headers=headers,
|
| 134 |
+
json=payload
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# 处理响应
|
| 138 |
+
if response.status_code == 200:
|
| 139 |
+
data = response.json()
|
| 140 |
+
for article in data["data"]:
|
| 141 |
+
print(f"标题: {article['title']}")
|
| 142 |
+
print(f"摘要: {article['summary']}")
|
| 143 |
+
print(f"发布时间: {article['publish_date']}")
|
| 144 |
+
print(f"链接: {article['url']}")
|
| 145 |
+
print("---")
|
| 146 |
+
else:
|
| 147 |
+
print(f"请求失败: {response.status_code}")
|
| 148 |
+
print(response.text)
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
### cURL 示例
|
| 152 |
+
```bash
|
| 153 |
+
curl -X POST \
|
| 154 |
+
https://openapi.iwencai.com/v1/comprehensive/search \
|
| 155 |
+
-H "Content-Type: application/json" \
|
| 156 |
+
-H "Authorization: Bearer $IWENCAI_API_KEY" \
|
| 157 |
+
-d '{
|
| 158 |
+
"channels": ["announcement"],
|
| 159 |
+
"app_id": "AIME_SKILL",
|
| 160 |
+
"query": "上市公司业绩预告"
|
| 161 |
+
}'
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
## 注意事项
|
| 165 |
+
1. **API Key 安全**: 请妥善保管 API Key,不要将其暴露在客户端代码中
|
| 166 |
+
2. **请求频率**: 请遵守 API 使用限制,避免频繁请求
|
| 167 |
+
3. **参数格式**: `channels` 参数必须为数组格式,且包含 `"announcement"`
|
| 168 |
+
4. **时间格式**: `publish_date` 字段使用 24 小时制时间格式
|
| 169 |
+
5. **错误处理**: 建议实现完善的错误处理机制
|
| 170 |
+
|
| 171 |
+
## 更新日志
|
| 172 |
+
- **v1.0.0** (初始版本): 基础公告搜索功能
|
| 173 |
+
|
| 174 |
+
## 支持
|
| 175 |
+
如有问题或需要技术支持,请联系 API 提供商。
|
Skills/announcement-search/scripts/__main__.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import argparse
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
import time
|
| 6 |
+
from typing import List, Dict, Any
|
| 7 |
+
|
| 8 |
+
from config import config
|
| 9 |
+
from utils import Utils
|
| 10 |
+
from announcement_search import AnnouncementSearch
|
| 11 |
+
|
| 12 |
+
def parse_arguments():
|
| 13 |
+
parser = argparse.ArgumentParser(
|
| 14 |
+
description="公告搜索工具 - 搜索A股、港股、基金、ETF等金融标的公告",
|
| 15 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 16 |
+
epilog="""
|
| 17 |
+
使用示例:
|
| 18 |
+
%(prog)s "贵州茅台 公告" # 基本搜索
|
| 19 |
+
%(prog)s "上市公司业绩预告" --limit 5 # 限制结果数量
|
| 20 |
+
%(prog)s "分红派息" --format json # 指定输出格式
|
| 21 |
+
%(prog)s --input queries.txt --output results.csv # 批量搜索
|
| 22 |
+
%(prog)s --help # 显示帮助信息
|
| 23 |
+
"""
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
parser.add_argument(
|
| 27 |
+
"query",
|
| 28 |
+
nargs="?",
|
| 29 |
+
help="搜索关键词,例如:'贵州茅台 公告'、'上市公司业绩预告'"
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
parser.add_argument(
|
| 33 |
+
"-i", "--input",
|
| 34 |
+
help="输入文件路径,包含多个查询(每行一个)"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
parser.add_argument(
|
| 38 |
+
"-o", "--output",
|
| 39 |
+
help="输出文件路径,保存搜索结果"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
parser.add_argument(
|
| 43 |
+
"-f", "--format",
|
| 44 |
+
choices=["csv", "json", "txt"],
|
| 45 |
+
default=config.get_output_config()["default_format"],
|
| 46 |
+
help="输出格式(默认: %(default)s)"
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
parser.add_argument(
|
| 50 |
+
"-l", "--limit",
|
| 51 |
+
type=int,
|
| 52 |
+
default=config.get_search_config()["default_limit"],
|
| 53 |
+
help="每个查询的结果数量限制(默认: %(default)d)"
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
parser.add_argument(
|
| 57 |
+
"-v", "--verbose",
|
| 58 |
+
action="store_true",
|
| 59 |
+
help="详细输出模式"
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
parser.add_argument(
|
| 63 |
+
"--version",
|
| 64 |
+
action="version",
|
| 65 |
+
version=f"公告搜索工具 v{config.get_skill_config()['version']}"
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
parser.add_argument(
|
| 69 |
+
"--config",
|
| 70 |
+
help="配置文件路径"
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
return parser.parse_args()
|
| 74 |
+
|
| 75 |
+
def show_platform_help():
|
| 76 |
+
print("\n跨平台环境变量设置说明:")
|
| 77 |
+
print("=" * 50)
|
| 78 |
+
print("macOS / Linux (bash/zsh):")
|
| 79 |
+
print(" export IWENCAI_API_KEY=\"your_api_key_here\"")
|
| 80 |
+
print()
|
| 81 |
+
print("Windows (PowerShell):")
|
| 82 |
+
print(" $env:IWENCAI_API_KEY=\"your_api_key_here\"")
|
| 83 |
+
print()
|
| 84 |
+
print("Windows (CMD):")
|
| 85 |
+
print(" set IWENCAI_API_KEY=your_api_key_here")
|
| 86 |
+
print()
|
| 87 |
+
print("获取API Key:")
|
| 88 |
+
print("1. 访问 https://www.iwencai.com/skillhub")
|
| 89 |
+
print("2. 登录您的账户")
|
| 90 |
+
print("3. 选择公告搜索技能,复制API Key")
|
| 91 |
+
print("=" * 50)
|
| 92 |
+
|
| 93 |
+
def validate_arguments(args):
|
| 94 |
+
if not args.query and not args.input:
|
| 95 |
+
print("错误: 必须提供查询关键词或输入文件")
|
| 96 |
+
return False
|
| 97 |
+
|
| 98 |
+
if args.limit > config.get_search_config()["max_limit"]:
|
| 99 |
+
print(f"警告: 结果数量限制超过最大值 {config.get_search_config()['max_limit']},已自动调整")
|
| 100 |
+
args.limit = config.get_search_config()["max_limit"]
|
| 101 |
+
|
| 102 |
+
if args.output and args.format not in config.get_output_config()["supported_formats"]:
|
| 103 |
+
print(f"错误: 不支持的输出格式 '{args.format}'")
|
| 104 |
+
print(f"支持的格式: {', '.join(config.get_output_config()['supported_formats'])}")
|
| 105 |
+
return False
|
| 106 |
+
|
| 107 |
+
return True
|
| 108 |
+
|
| 109 |
+
def save_results(results: List[Dict[str, Any]], args):
|
| 110 |
+
if not args.output:
|
| 111 |
+
return True
|
| 112 |
+
|
| 113 |
+
file_ext = Utils.get_file_extension(args.output)
|
| 114 |
+
|
| 115 |
+
if file_ext != args.format:
|
| 116 |
+
new_output = f"{os.path.splitext(args.output)[0]}.{args.format}"
|
| 117 |
+
print(f"警告: 输出文件扩展名与格式不匹配,已自动调整为: {new_output}")
|
| 118 |
+
args.output = new_output
|
| 119 |
+
|
| 120 |
+
if args.format == "csv":
|
| 121 |
+
return Utils.save_to_csv(results, args.output)
|
| 122 |
+
elif args.format == "json":
|
| 123 |
+
return Utils.save_to_json(results, args.output)
|
| 124 |
+
elif args.format == "txt":
|
| 125 |
+
return Utils.save_to_txt(results, args.output)
|
| 126 |
+
else:
|
| 127 |
+
print(f"错误: 不支持的输出格式 '{args.format}'")
|
| 128 |
+
return False
|
| 129 |
+
|
| 130 |
+
def display_results(results: List[Dict[str, Any]], query: str, execution_time: float):
|
| 131 |
+
if not results:
|
| 132 |
+
print(f"\n对于查询 '{query}',未找到相关公告信息。")
|
| 133 |
+
return
|
| 134 |
+
|
| 135 |
+
print(f"\n搜索完成!")
|
| 136 |
+
print(f"查询: {query}")
|
| 137 |
+
print(f"找到 {len(results)} 条相关公告")
|
| 138 |
+
print(f"执行时间: {Utils.format_execution_time(execution_time)}")
|
| 139 |
+
print("=" * 60)
|
| 140 |
+
|
| 141 |
+
for i, result in enumerate(results, 1):
|
| 142 |
+
print(f"{i}. {result.get('title', '')}")
|
| 143 |
+
|
| 144 |
+
if args.verbose:
|
| 145 |
+
print(f" 摘要: {result.get('summary', '')}")
|
| 146 |
+
print(f" 发布时间: {result.get('publish_date', '')}")
|
| 147 |
+
print(f" 链接: {result.get('url', '')}")
|
| 148 |
+
else:
|
| 149 |
+
summary = result.get('summary', '')
|
| 150 |
+
if len(summary) > 100:
|
| 151 |
+
summary = summary[:100] + "..."
|
| 152 |
+
print(f" 摘要: {summary}")
|
| 153 |
+
print(f" 发布时间: {result.get('publish_date', '')}")
|
| 154 |
+
|
| 155 |
+
print()
|
| 156 |
+
|
| 157 |
+
def main():
|
| 158 |
+
global args
|
| 159 |
+
args = parse_arguments()
|
| 160 |
+
|
| 161 |
+
if not validate_arguments(args):
|
| 162 |
+
sys.exit(1)
|
| 163 |
+
|
| 164 |
+
log_level = "DEBUG" if args.verbose else "INFO"
|
| 165 |
+
Utils.setup_logging(log_level)
|
| 166 |
+
|
| 167 |
+
if not config.validate():
|
| 168 |
+
print("错误: 配置验证失败")
|
| 169 |
+
print("请检查 IWENCAI_API_KEY 环境变量是否设置")
|
| 170 |
+
print()
|
| 171 |
+
show_platform_help()
|
| 172 |
+
sys.exit(1)
|
| 173 |
+
|
| 174 |
+
search = AnnouncementSearch()
|
| 175 |
+
|
| 176 |
+
all_results = []
|
| 177 |
+
|
| 178 |
+
if args.input:
|
| 179 |
+
queries = Utils.read_queries_from_file(args.input)
|
| 180 |
+
if not queries:
|
| 181 |
+
print(f"错误: 无法从文件读取查询: {args.input}")
|
| 182 |
+
sys.exit(1)
|
| 183 |
+
|
| 184 |
+
print(f"开始批量搜索,共 {len(queries)} 个查询")
|
| 185 |
+
|
| 186 |
+
batch_results = search.batch_search(queries, args.limit)
|
| 187 |
+
|
| 188 |
+
for query, (success, results, message) in batch_results.items():
|
| 189 |
+
if success:
|
| 190 |
+
all_results.extend(results)
|
| 191 |
+
print(f"✓ {query}: 找到 {len(results)} 条结果")
|
| 192 |
+
else:
|
| 193 |
+
print(f"✗ {query}: {message}")
|
| 194 |
+
|
| 195 |
+
if args.output:
|
| 196 |
+
save_success = save_results(all_results, args)
|
| 197 |
+
if save_success:
|
| 198 |
+
print(f"\n所有结果已保存到: {args.output}")
|
| 199 |
+
|
| 200 |
+
print(f"\n批量搜索完成,总共找到 {len(all_results)} 条公告")
|
| 201 |
+
else:
|
| 202 |
+
start_time = time.time()
|
| 203 |
+
|
| 204 |
+
success, results, message = search.search(args.query, args.limit)
|
| 205 |
+
|
| 206 |
+
execution_time = Utils.calculate_execution_time(start_time)
|
| 207 |
+
|
| 208 |
+
if success:
|
| 209 |
+
all_results = results
|
| 210 |
+
|
| 211 |
+
if args.output:
|
| 212 |
+
save_success = save_results(all_results, args)
|
| 213 |
+
if save_success:
|
| 214 |
+
print(f"结果已保存到: {args.output}")
|
| 215 |
+
|
| 216 |
+
display_results(all_results, args.query, execution_time)
|
| 217 |
+
else:
|
| 218 |
+
print(f"\n搜索失败: {message}")
|
| 219 |
+
sys.exit(1)
|
| 220 |
+
|
| 221 |
+
sys.exit(0)
|
| 222 |
+
|
| 223 |
+
if __name__ == "__main__":
|
| 224 |
+
try:
|
| 225 |
+
main()
|
| 226 |
+
except KeyboardInterrupt:
|
| 227 |
+
print("\n\n操作被用户中断")
|
| 228 |
+
sys.exit(130)
|
| 229 |
+
except Exception as e:
|
| 230 |
+
print(f"\n发生错误: {e}")
|
| 231 |
+
if args and args.verbose:
|
| 232 |
+
import traceback
|
| 233 |
+
traceback.print_exc()
|
| 234 |
+
sys.exit(1)
|
Skills/announcement-search/scripts/announcement_search.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import json
|
| 3 |
+
import time
|
| 4 |
+
import logging
|
| 5 |
+
import secrets
|
| 6 |
+
from typing import Dict, List, Any, Optional, Tuple
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
from config import config
|
| 10 |
+
from utils import Utils
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
class AnnouncementSearch:
|
| 15 |
+
def __init__(self):
|
| 16 |
+
self.api_config = config.get_api_config()
|
| 17 |
+
self.search_config = config.get_search_config()
|
| 18 |
+
self.api_key = config.get_api_key()
|
| 19 |
+
|
| 20 |
+
self.base_url = self.api_config["base_url"]
|
| 21 |
+
self.endpoint = self.api_config["endpoint"]
|
| 22 |
+
self.timeout = self.api_config["timeout"]
|
| 23 |
+
self.max_retries = self.api_config["max_retries"]
|
| 24 |
+
|
| 25 |
+
self.channels = self.search_config["channels"]
|
| 26 |
+
self.app_id = self.search_config["app_id"]
|
| 27 |
+
|
| 28 |
+
skill_config = config.get_skill_config()
|
| 29 |
+
self.skill_id = skill_config["id"]
|
| 30 |
+
self.skill_version = skill_config["version"]
|
| 31 |
+
self.plugin_id = skill_config["plugin_id"]
|
| 32 |
+
self.plugin_version = skill_config["plugin_version"]
|
| 33 |
+
|
| 34 |
+
self.base_headers = {
|
| 35 |
+
"Content-Type": "application/json",
|
| 36 |
+
"Authorization": f"Bearer {self.api_key}"
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
def _generate_trace_id(self) -> str:
|
| 40 |
+
return secrets.token_hex(32)
|
| 41 |
+
|
| 42 |
+
def _get_claw_headers(self, trace_id: str) -> Dict[str, str]:
|
| 43 |
+
return {
|
| 44 |
+
"X-Claw-Call-Type": "normal",
|
| 45 |
+
"X-Claw-Skill-Id": self.skill_id,
|
| 46 |
+
"X-Claw-Skill-Version": self.skill_version,
|
| 47 |
+
"X-Claw-Plugin-Id": self.plugin_id,
|
| 48 |
+
"X-Claw-Plugin-Version": self.plugin_version,
|
| 49 |
+
"X-Claw-Trace-Id": trace_id
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
def search(self, query: str, limit: int = 10) -> Dict[str, Any]:
|
| 53 |
+
if not Utils.validate_query(query):
|
| 54 |
+
return {
|
| 55 |
+
"success": False,
|
| 56 |
+
"error": "查询参数无效",
|
| 57 |
+
"raw_response": None
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
logger.info(f"搜索公告: {query}")
|
| 61 |
+
|
| 62 |
+
payload = {
|
| 63 |
+
"channels": self.channels,
|
| 64 |
+
"app_id": self.app_id,
|
| 65 |
+
"query": query
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
url = f"{self.base_url}{self.endpoint}"
|
| 69 |
+
|
| 70 |
+
for attempt in range(self.max_retries):
|
| 71 |
+
try:
|
| 72 |
+
trace_id = self._generate_trace_id()
|
| 73 |
+
claw_headers = self._get_claw_headers(trace_id)
|
| 74 |
+
headers = {**self.base_headers, **claw_headers}
|
| 75 |
+
|
| 76 |
+
logger.debug(f"请求URL: {url}")
|
| 77 |
+
logger.debug(f"请求参数: {payload}")
|
| 78 |
+
logger.debug(f"Trace ID: {trace_id}")
|
| 79 |
+
logger.debug(f"Claw Headers: {claw_headers}")
|
| 80 |
+
|
| 81 |
+
response = requests.post(
|
| 82 |
+
url,
|
| 83 |
+
headers=headers,
|
| 84 |
+
json=payload,
|
| 85 |
+
timeout=self.timeout
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
logger.debug(f"响应状态码: {response.status_code}")
|
| 89 |
+
|
| 90 |
+
try:
|
| 91 |
+
raw_response = response.json()
|
| 92 |
+
except json.JSONDecodeError:
|
| 93 |
+
raw_response = {"error": "响应不是有效的JSON格式", "raw_text": response.text}
|
| 94 |
+
|
| 95 |
+
result = {
|
| 96 |
+
"success": response.status_code == 200,
|
| 97 |
+
"status_code": response.status_code,
|
| 98 |
+
"trace_id": trace_id,
|
| 99 |
+
"raw_response": raw_response,
|
| 100 |
+
"query": query
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
if response.status_code == 200:
|
| 104 |
+
logger.info(f"API调用成功,Trace ID: {trace_id}")
|
| 105 |
+
return result
|
| 106 |
+
elif response.status_code == 429:
|
| 107 |
+
logger.warning("请求过于频繁,等待后重试")
|
| 108 |
+
if attempt < self.max_retries - 1:
|
| 109 |
+
time.sleep(2 ** attempt)
|
| 110 |
+
continue
|
| 111 |
+
else:
|
| 112 |
+
logger.warning("请求过于频繁,已达到最大重试次数")
|
| 113 |
+
return result
|
| 114 |
+
else:
|
| 115 |
+
logger.warning(f"API调用失败,状态码: {response.status_code}, Trace ID: {trace_id}")
|
| 116 |
+
return result
|
| 117 |
+
|
| 118 |
+
except requests.exceptions.Timeout:
|
| 119 |
+
logger.warning(f"请求超时 (尝试 {attempt + 1}/{self.max_retries})")
|
| 120 |
+
if attempt < self.max_retries - 1:
|
| 121 |
+
time.sleep(1)
|
| 122 |
+
continue
|
| 123 |
+
else:
|
| 124 |
+
logger.error("请求超时,已达到最大重试次数")
|
| 125 |
+
return {
|
| 126 |
+
"success": False,
|
| 127 |
+
"error": "请求超时,请检查网络连接",
|
| 128 |
+
"raw_response": None,
|
| 129 |
+
"query": query
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
except requests.exceptions.ConnectionError:
|
| 133 |
+
logger.error("网络连接错误")
|
| 134 |
+
return {
|
| 135 |
+
"success": False,
|
| 136 |
+
"error": "网络连接错误,请检查网络连接",
|
| 137 |
+
"raw_response": None,
|
| 138 |
+
"query": query
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
except requests.exceptions.RequestException as e:
|
| 142 |
+
logger.error(f"请求异常: {e}")
|
| 143 |
+
return {
|
| 144 |
+
"success": False,
|
| 145 |
+
"error": f"请求异常: {str(e)}",
|
| 146 |
+
"raw_response": None,
|
| 147 |
+
"query": query
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
except json.JSONDecodeError as e:
|
| 151 |
+
logger.error(f"JSON解析错误: {e}")
|
| 152 |
+
return {
|
| 153 |
+
"success": False,
|
| 154 |
+
"error": f"响应数据解析错误: {str(e)}",
|
| 155 |
+
"raw_response": None,
|
| 156 |
+
"query": query
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
except Exception as e:
|
| 160 |
+
logger.error(f"未知错误: {e}")
|
| 161 |
+
return {
|
| 162 |
+
"success": False,
|
| 163 |
+
"error": f"未知错误: {str(e)}",
|
| 164 |
+
"raw_response": None,
|
| 165 |
+
"query": query
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
return {
|
| 169 |
+
"success": False,
|
| 170 |
+
"error": "搜索失败,已达到最大重试次数",
|
| 171 |
+
"raw_response": None,
|
| 172 |
+
"query": query
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
def _process_results(self, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
| 176 |
+
processed = []
|
| 177 |
+
|
| 178 |
+
for result in results:
|
| 179 |
+
processed_result = {
|
| 180 |
+
"title": result.get("title", ""),
|
| 181 |
+
"summary": result.get("summary", ""),
|
| 182 |
+
"url": result.get("url", ""),
|
| 183 |
+
"publish_date": result.get("publish_date", "")
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
processed.append(processed_result)
|
| 187 |
+
|
| 188 |
+
return processed
|
| 189 |
+
|
| 190 |
+
def batch_search(self, queries: List[str], limit_per_query: int = 10) -> Dict[str, Dict[str, Any]]:
|
| 191 |
+
results = {}
|
| 192 |
+
|
| 193 |
+
logger.info(f"开始批量搜索,共 {len(queries)} 个查询")
|
| 194 |
+
|
| 195 |
+
for i, query in enumerate(queries, 1):
|
| 196 |
+
logger.info(f"处理查询 {i}/{len(queries)}: {query}")
|
| 197 |
+
|
| 198 |
+
result = self.search(query, limit_per_query)
|
| 199 |
+
results[query] = result
|
| 200 |
+
|
| 201 |
+
time.sleep(config.get_performance_config().get("request_delay", 0.5))
|
| 202 |
+
|
| 203 |
+
logger.info("批量搜索完成")
|
| 204 |
+
return results
|
| 205 |
+
|
| 206 |
+
def smart_query_analysis(self, user_query: str) -> List[str]:
|
| 207 |
+
logger.info(f"智能分析用户查询: {user_query}")
|
| 208 |
+
|
| 209 |
+
queries = []
|
| 210 |
+
|
| 211 |
+
if "和" in user_query or "与" in user_query or "、" in user_query:
|
| 212 |
+
parts = user_query.replace("和", ",").replace("与", ",").replace("、", ",").split(",")
|
| 213 |
+
for part in parts:
|
| 214 |
+
part = part.strip()
|
| 215 |
+
if part:
|
| 216 |
+
queries.append(f"{part} 公告")
|
| 217 |
+
else:
|
| 218 |
+
queries.append(user_query)
|
| 219 |
+
|
| 220 |
+
logger.info(f"分析后的查询列表: {queries}")
|
| 221 |
+
return queries
|
| 222 |
+
|
| 223 |
+
def evaluate_results(self, query: str, results: List[Dict[str, Any]]) -> Tuple[bool, str]:
|
| 224 |
+
if not results:
|
| 225 |
+
return False, "未找到相关结果"
|
| 226 |
+
|
| 227 |
+
query_keywords = set(query.lower().split())
|
| 228 |
+
|
| 229 |
+
relevance_scores = []
|
| 230 |
+
for result in results:
|
| 231 |
+
title = result.get("title", "").lower()
|
| 232 |
+
summary = result.get("summary", "").lower()
|
| 233 |
+
|
| 234 |
+
score = 0
|
| 235 |
+
for keyword in query_keywords:
|
| 236 |
+
if keyword in title:
|
| 237 |
+
score += 3
|
| 238 |
+
if keyword in summary:
|
| 239 |
+
score += 1
|
| 240 |
+
|
| 241 |
+
relevance_scores.append(score)
|
| 242 |
+
|
| 243 |
+
avg_score = sum(relevance_scores) / len(relevance_scores) if relevance_scores else 0
|
| 244 |
+
|
| 245 |
+
if avg_score >= 2:
|
| 246 |
+
return True, "结果相关性较高"
|
| 247 |
+
elif avg_score >= 1:
|
| 248 |
+
return True, "结果相关性一般"
|
| 249 |
+
else:
|
| 250 |
+
return False, "结果相关性较低"
|
| 251 |
+
|
| 252 |
+
def generate_search_summary(self, query: str, results: List[Dict[str, Any]]) -> str:
|
| 253 |
+
if not results:
|
| 254 |
+
return f"对于查询 '{query}',未找到相关公告信息。"
|
| 255 |
+
|
| 256 |
+
summary_lines = []
|
| 257 |
+
summary_lines.append(f"搜索查询:{query}")
|
| 258 |
+
summary_lines.append(f"找到 {len(results)} 条相关公告:")
|
| 259 |
+
summary_lines.append("")
|
| 260 |
+
|
| 261 |
+
for i, result in enumerate(results[:5], 1):
|
| 262 |
+
title = result.get("title", "")
|
| 263 |
+
publish_date = result.get("publish_date", "")
|
| 264 |
+
summary_lines.append(f"{i}. {title} ({publish_date})")
|
| 265 |
+
|
| 266 |
+
if len(results) > 5:
|
| 267 |
+
summary_lines.append(f"... 还有 {len(results) - 5} 条结果")
|
| 268 |
+
|
| 269 |
+
return "\n".join(summary_lines)
|
| 270 |
+
|
| 271 |
+
if __name__ == "__main__":
|
| 272 |
+
Utils.setup_logging("INFO")
|
| 273 |
+
|
| 274 |
+
search = AnnouncementSearch()
|
| 275 |
+
|
| 276 |
+
test_queries = [
|
| 277 |
+
"贵州茅台 公告",
|
| 278 |
+
"上市公司业绩预告",
|
| 279 |
+
"分红派息"
|
| 280 |
+
]
|
| 281 |
+
|
| 282 |
+
for query in test_queries:
|
| 283 |
+
print(f"\n测试查询: {query}")
|
| 284 |
+
print("-" * 50)
|
| 285 |
+
|
| 286 |
+
result = search.search(query, limit=3)
|
| 287 |
+
|
| 288 |
+
if result["success"]:
|
| 289 |
+
print(f"状态: API调用成功")
|
| 290 |
+
print(f"Trace ID: {result['trace_id']}")
|
| 291 |
+
print(f"状态码: {result['status_code']}")
|
| 292 |
+
|
| 293 |
+
raw_response = result["raw_response"]
|
| 294 |
+
if raw_response and "data" in raw_response:
|
| 295 |
+
data = raw_response.get("data", [])
|
| 296 |
+
print(f"找到 {len(data)} 条结果:")
|
| 297 |
+
|
| 298 |
+
for i, item in enumerate(data[:5], 1):
|
| 299 |
+
print(f"{i}. {item.get('title', '')}")
|
| 300 |
+
print(f" 发布时间: {item.get('publish_date', '')}")
|
| 301 |
+
summary = item.get('summary', '')
|
| 302 |
+
if summary:
|
| 303 |
+
print(f" 摘要: {summary[:50]}...")
|
| 304 |
+
print()
|
| 305 |
+
else:
|
| 306 |
+
print(f"API响应: {raw_response}")
|
| 307 |
+
else:
|
| 308 |
+
print(f"错误: {result.get('error', '未知错误')}")
|
| 309 |
+
print(f"API响应: {result.get('raw_response', '无响应')}")
|
| 310 |
+
|
| 311 |
+
print("\n测试智能查询分析:")
|
| 312 |
+
complex_query = "贵州茅台和五粮液最近有什么公告"
|
| 313 |
+
analyzed_queries = search.smart_query_analysis(complex_query)
|
| 314 |
+
print(f"原始查询: {complex_query}")
|
| 315 |
+
print(f"分析后的查询: {analyzed_queries}")
|
| 316 |
+
|
| 317 |
+
print("\n测试结果评估:")
|
| 318 |
+
test_results = [
|
| 319 |
+
{"title": "贵州茅台2023年度业绩预告", "summary": "公司预计2023年度净利润增长", "url": "", "publish_date": ""},
|
| 320 |
+
{"title": "五粮液分红公告", "summary": "公司宣布年度分红方案", "url": "", "publish_date": ""}
|
| 321 |
+
]
|
| 322 |
+
relevance, evaluation = search.evaluate_results("贵州茅台 业绩预告", test_results)
|
| 323 |
+
print(f"相关性: {relevance}, 评估: {evaluation}")
|
Skills/announcement-search/scripts/config.example.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"api": {
|
| 3 |
+
"base_url": "https://openapi.iwencai.com",
|
| 4 |
+
"endpoint": "/v1/comprehensive/search",
|
| 5 |
+
"timeout": 30,
|
| 6 |
+
"max_retries": 3
|
| 7 |
+
},
|
| 8 |
+
"auth": {
|
| 9 |
+
"api_key_env_var": "IWENCAI_API_KEY",
|
| 10 |
+
"api_key": "your_api_key_here"
|
| 11 |
+
},
|
| 12 |
+
"search": {
|
| 13 |
+
"channels": ["announcement"],
|
| 14 |
+
"app_id": "AIME_SKILL",
|
| 15 |
+
"default_limit": 10,
|
| 16 |
+
"max_limit": 50
|
| 17 |
+
},
|
| 18 |
+
"output": {
|
| 19 |
+
"default_format": "csv",
|
| 20 |
+
"supported_formats": ["csv", "json", "txt"],
|
| 21 |
+
"date_format": "%Y-%m-%d %H:%M:%S",
|
| 22 |
+
"encoding": "utf-8"
|
| 23 |
+
},
|
| 24 |
+
"logging": {
|
| 25 |
+
"level": "INFO",
|
| 26 |
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
| 27 |
+
"file": "announcement_search.log"
|
| 28 |
+
},
|
| 29 |
+
"cache": {
|
| 30 |
+
"enabled": false,
|
| 31 |
+
"ttl": 3600,
|
| 32 |
+
"cache_dir": ".cache"
|
| 33 |
+
},
|
| 34 |
+
"performance": {
|
| 35 |
+
"max_concurrent_requests": 5,
|
| 36 |
+
"request_delay": 0.5,
|
| 37 |
+
"batch_size": 10
|
| 38 |
+
}
|
| 39 |
+
}
|
Skills/announcement-search/scripts/config.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
from typing import Dict, Any, Optional
|
| 5 |
+
|
| 6 |
+
class Config:
|
| 7 |
+
def __init__(self, config_path: Optional[str] = None):
|
| 8 |
+
self.config = self._load_config(config_path)
|
| 9 |
+
self._setup_logging()
|
| 10 |
+
|
| 11 |
+
def _load_config(self, config_path: Optional[str] = None) -> Dict[str, Any]:
|
| 12 |
+
default_config = {
|
| 13 |
+
"api": {
|
| 14 |
+
"base_url": "https://openapi.iwencai.com",
|
| 15 |
+
"endpoint": "/v1/comprehensive/search",
|
| 16 |
+
"timeout": 30,
|
| 17 |
+
"max_retries": 3
|
| 18 |
+
},
|
| 19 |
+
"auth": {
|
| 20 |
+
"api_key_env_var": "IWENCAI_API_KEY",
|
| 21 |
+
"api_key": os.getenv("IWENCAI_API_KEY", "")
|
| 22 |
+
},
|
| 23 |
+
"search": {
|
| 24 |
+
"channels": ["announcement"],
|
| 25 |
+
"app_id": "AIME_SKILL",
|
| 26 |
+
"default_limit": 10,
|
| 27 |
+
"max_limit": 50
|
| 28 |
+
},
|
| 29 |
+
"skill": {
|
| 30 |
+
"id": "announcement-search",
|
| 31 |
+
"version": "1.0.0",
|
| 32 |
+
"plugin_id": "none",
|
| 33 |
+
"plugin_version": "none"
|
| 34 |
+
},
|
| 35 |
+
"output": {
|
| 36 |
+
"default_format": "csv",
|
| 37 |
+
"supported_formats": ["csv", "json", "txt"],
|
| 38 |
+
"date_format": "%Y-%m-%d %H:%M:%S",
|
| 39 |
+
"encoding": "utf-8"
|
| 40 |
+
},
|
| 41 |
+
"logging": {
|
| 42 |
+
"level": os.getenv("LOG_LEVEL", "INFO"),
|
| 43 |
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
| 44 |
+
"file": "announcement_search.log"
|
| 45 |
+
},
|
| 46 |
+
"cache": {
|
| 47 |
+
"enabled": False,
|
| 48 |
+
"ttl": 3600,
|
| 49 |
+
"cache_dir": ".cache"
|
| 50 |
+
},
|
| 51 |
+
"performance": {
|
| 52 |
+
"max_concurrent_requests": 5,
|
| 53 |
+
"request_delay": 0.5,
|
| 54 |
+
"batch_size": 10
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
if config_path and os.path.exists(config_path):
|
| 59 |
+
try:
|
| 60 |
+
with open(config_path, 'r', encoding='utf-8') as f:
|
| 61 |
+
file_config = json.load(f)
|
| 62 |
+
default_config = self._merge_configs(default_config, file_config)
|
| 63 |
+
except Exception as e:
|
| 64 |
+
logging.warning(f"Failed to load config file {config_path}: {e}")
|
| 65 |
+
|
| 66 |
+
return default_config
|
| 67 |
+
|
| 68 |
+
def _merge_configs(self, default: Dict[str, Any], custom: Dict[str, Any]) -> Dict[str, Any]:
|
| 69 |
+
result = default.copy()
|
| 70 |
+
for key, value in custom.items():
|
| 71 |
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
| 72 |
+
result[key] = self._merge_configs(result[key], value)
|
| 73 |
+
else:
|
| 74 |
+
result[key] = value
|
| 75 |
+
return result
|
| 76 |
+
|
| 77 |
+
def _setup_logging(self):
|
| 78 |
+
log_config = self.config["logging"]
|
| 79 |
+
log_level = getattr(logging, log_config["level"].upper(), logging.INFO)
|
| 80 |
+
|
| 81 |
+
logging.basicConfig(
|
| 82 |
+
level=log_level,
|
| 83 |
+
format=log_config["format"],
|
| 84 |
+
handlers=[
|
| 85 |
+
logging.StreamHandler(),
|
| 86 |
+
logging.FileHandler(log_config["file"])
|
| 87 |
+
]
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
def get_api_key(self) -> str:
|
| 91 |
+
api_key = self.config["auth"]["api_key"]
|
| 92 |
+
if not api_key:
|
| 93 |
+
raise ValueError("API Key not found. Please set IWENCAI_API_KEY environment variable.")
|
| 94 |
+
return api_key
|
| 95 |
+
|
| 96 |
+
def get_api_config(self) -> Dict[str, Any]:
|
| 97 |
+
return self.config["api"]
|
| 98 |
+
|
| 99 |
+
def get_search_config(self) -> Dict[str, Any]:
|
| 100 |
+
return self.config["search"]
|
| 101 |
+
|
| 102 |
+
def get_output_config(self) -> Dict[str, Any]:
|
| 103 |
+
return self.config["output"]
|
| 104 |
+
|
| 105 |
+
def get_cache_config(self) -> Dict[str, Any]:
|
| 106 |
+
return self.config["cache"]
|
| 107 |
+
|
| 108 |
+
def get_performance_config(self) -> Dict[str, Any]:
|
| 109 |
+
return self.config["performance"]
|
| 110 |
+
|
| 111 |
+
def get_skill_config(self) -> Dict[str, Any]:
|
| 112 |
+
return self.config["skill"]
|
| 113 |
+
|
| 114 |
+
def validate(self) -> bool:
|
| 115 |
+
try:
|
| 116 |
+
api_key = self.get_api_key()
|
| 117 |
+
if not api_key or api_key == "your_api_key_here":
|
| 118 |
+
return False
|
| 119 |
+
|
| 120 |
+
search_config = self.get_search_config()
|
| 121 |
+
if not search_config.get("channels") or "announcement" not in search_config["channels"]:
|
| 122 |
+
return False
|
| 123 |
+
|
| 124 |
+
if not search_config.get("app_id"):
|
| 125 |
+
return False
|
| 126 |
+
|
| 127 |
+
return True
|
| 128 |
+
except Exception as e:
|
| 129 |
+
logging.error(f"Config validation failed: {e}")
|
| 130 |
+
return False
|
| 131 |
+
|
| 132 |
+
config = Config()
|
| 133 |
+
|
| 134 |
+
if __name__ == "__main__":
|
| 135 |
+
print("Configuration loaded successfully")
|
| 136 |
+
print(f"API Base URL: {config.get_api_config()['base_url']}")
|
| 137 |
+
print(f"Search Channels: {config.get_search_config()['channels']}")
|
| 138 |
+
print(f"Default Output Format: {config.get_output_config()['default_format']}")
|
Skills/announcement-search/scripts/example_usage.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
公告搜索工具使用示例
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import time
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
|
| 11 |
+
# 添加当前目录到Python路径
|
| 12 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 13 |
+
|
| 14 |
+
from config import config
|
| 15 |
+
from utils import Utils
|
| 16 |
+
from announcement_search import AnnouncementSearch
|
| 17 |
+
|
| 18 |
+
def setup_environment():
|
| 19 |
+
"""设置环境"""
|
| 20 |
+
print("=" * 60)
|
| 21 |
+
print("公告搜索工具使用示例")
|
| 22 |
+
print("=" * 60)
|
| 23 |
+
|
| 24 |
+
# 检查API Key
|
| 25 |
+
api_key = os.getenv("IWENCAI_API_KEY")
|
| 26 |
+
if not api_key:
|
| 27 |
+
print("警告: IWENCAI_API_KEY 环境变量未设置")
|
| 28 |
+
print("请设置环境变量: export IWENCAI_API_KEY='your_api_key'")
|
| 29 |
+
return False
|
| 30 |
+
|
| 31 |
+
print(f"✓ API Key 已设置")
|
| 32 |
+
print(f"✓ 配置验证: {'通过' if config.validate() else '失败'}")
|
| 33 |
+
return True
|
| 34 |
+
|
| 35 |
+
def example_basic_search():
|
| 36 |
+
"""示例1: 基本搜索"""
|
| 37 |
+
print("\n" + "=" * 60)
|
| 38 |
+
print("示例1: 基本搜索")
|
| 39 |
+
print("=" * 60)
|
| 40 |
+
|
| 41 |
+
search = AnnouncementSearch()
|
| 42 |
+
|
| 43 |
+
# 搜索贵州茅台公告
|
| 44 |
+
query = "贵州茅台 公告"
|
| 45 |
+
print(f"搜索: {query}")
|
| 46 |
+
|
| 47 |
+
start_time = time.time()
|
| 48 |
+
success, results, message = search.search(query, limit=5)
|
| 49 |
+
execution_time = time.time() - start_time
|
| 50 |
+
|
| 51 |
+
if success:
|
| 52 |
+
print(f"状态: {message}")
|
| 53 |
+
print(f"找到 {len(results)} 条结果")
|
| 54 |
+
print(f"执行时间: {execution_time:.2f}秒")
|
| 55 |
+
|
| 56 |
+
for i, result in enumerate(results, 1):
|
| 57 |
+
print(f"\n{i}. {result.get('title', '')}")
|
| 58 |
+
print(f" 发布时间: {result.get('publish_date', '')}")
|
| 59 |
+
print(f" 摘要: {result.get('summary', '')[:80]}...")
|
| 60 |
+
else:
|
| 61 |
+
print(f"错误: {message}")
|
| 62 |
+
|
| 63 |
+
def example_multiple_queries():
|
| 64 |
+
"""示例2: 多个查询"""
|
| 65 |
+
print("\n" + "=" * 60)
|
| 66 |
+
print("示例2: 多个查询")
|
| 67 |
+
print("=" * 60)
|
| 68 |
+
|
| 69 |
+
search = AnnouncementSearch()
|
| 70 |
+
|
| 71 |
+
queries = [
|
| 72 |
+
"上市公司业绩预告",
|
| 73 |
+
"分红派息公告",
|
| 74 |
+
"回购增持公告"
|
| 75 |
+
]
|
| 76 |
+
|
| 77 |
+
for query in queries:
|
| 78 |
+
print(f"\n搜索: {query}")
|
| 79 |
+
|
| 80 |
+
success, results, message = search.search(query, limit=3)
|
| 81 |
+
|
| 82 |
+
if success:
|
| 83 |
+
print(f" 找到 {len(results)} 条结果")
|
| 84 |
+
if results:
|
| 85 |
+
print(f" 最新公告: {results[0].get('title', '')}")
|
| 86 |
+
else:
|
| 87 |
+
print(f" 错误: {message}")
|
| 88 |
+
|
| 89 |
+
time.sleep(1) # 避免请求过于频繁
|
| 90 |
+
|
| 91 |
+
def example_smart_query_analysis():
|
| 92 |
+
"""示例3: 智能查询分析"""
|
| 93 |
+
print("\n" + "=" * 60)
|
| 94 |
+
print("示例3: 智能查询分析")
|
| 95 |
+
print("=" * 60)
|
| 96 |
+
|
| 97 |
+
search = AnnouncementSearch()
|
| 98 |
+
|
| 99 |
+
complex_queries = [
|
| 100 |
+
"贵州茅台和五粮液最近有什么公告",
|
| 101 |
+
"查看宁德时代、比亚迪的业绩预告",
|
| 102 |
+
"搜索分红和回购相关的公告"
|
| 103 |
+
]
|
| 104 |
+
|
| 105 |
+
for query in complex_queries:
|
| 106 |
+
print(f"\n原始查询: {query}")
|
| 107 |
+
|
| 108 |
+
analyzed_queries = search.smart_query_analysis(query)
|
| 109 |
+
print(f"分析后的查询: {analyzed_queries}")
|
| 110 |
+
|
| 111 |
+
# 执行第一个分析后的查询作为示例
|
| 112 |
+
if analyzed_queries:
|
| 113 |
+
success, results, message = search.search(analyzed_queries[0], limit=2)
|
| 114 |
+
if success:
|
| 115 |
+
print(f" 示例结果: 找到 {len(results)} 条相关公告")
|
| 116 |
+
|
| 117 |
+
def example_batch_processing():
|
| 118 |
+
"""示例4: 批量处理"""
|
| 119 |
+
print("\n" + "=" * 60)
|
| 120 |
+
print("示例4: 批量处理")
|
| 121 |
+
print("=" * 60)
|
| 122 |
+
|
| 123 |
+
# 创建测试查询文件
|
| 124 |
+
test_queries = [
|
| 125 |
+
"# 测试查询文件",
|
| 126 |
+
"贵州茅台 公告",
|
| 127 |
+
"中国平安 分红",
|
| 128 |
+
"宁德时代 业绩预告",
|
| 129 |
+
"",
|
| 130 |
+
"# 空行会被忽略",
|
| 131 |
+
"回购增持"
|
| 132 |
+
]
|
| 133 |
+
|
| 134 |
+
queries_file = "test_queries.txt"
|
| 135 |
+
with open(queries_file, 'w', encoding='utf-8') as f:
|
| 136 |
+
f.write("\n".join(test_queries))
|
| 137 |
+
|
| 138 |
+
print(f"创建测试查询文件: {queries_file}")
|
| 139 |
+
|
| 140 |
+
# 读取查询文件
|
| 141 |
+
queries = Utils.read_queries_from_file(queries_file)
|
| 142 |
+
print(f"读取到 {len(queries)} 个查询:")
|
| 143 |
+
for i, query in enumerate(queries, 1):
|
| 144 |
+
print(f" {i}. {query}")
|
| 145 |
+
|
| 146 |
+
# 清理测试文件
|
| 147 |
+
os.remove(queries_file)
|
| 148 |
+
print(f"已清理测试文件")
|
| 149 |
+
|
| 150 |
+
def example_data_export():
|
| 151 |
+
"""示例5: 数据导出"""
|
| 152 |
+
print("\n" + "=" * 60)
|
| 153 |
+
print("示例5: 数据导出")
|
| 154 |
+
print("=" * 60)
|
| 155 |
+
|
| 156 |
+
# 创建测试数据
|
| 157 |
+
test_data = [
|
| 158 |
+
{
|
| 159 |
+
"title": "某某公司2023年度业绩预告",
|
| 160 |
+
"summary": "公司预计2023年度净利润同比增长50%-70%",
|
| 161 |
+
"url": "https://example.com/announcement/12345",
|
| 162 |
+
"publish_date": "2024-01-15 09:30:00"
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"title": "另一家公司重大合同公告",
|
| 166 |
+
"summary": "公司与客户签订重大销售合同,金额约10亿元",
|
| 167 |
+
"url": "https://example.com/announcement/12346",
|
| 168 |
+
"publish_date": "2024-01-14 16:45:00"
|
| 169 |
+
}
|
| 170 |
+
]
|
| 171 |
+
|
| 172 |
+
# 导出为不同格式
|
| 173 |
+
formats = ["csv", "json", "txt"]
|
| 174 |
+
|
| 175 |
+
for fmt in formats:
|
| 176 |
+
filename = f"test_output.{fmt}"
|
| 177 |
+
|
| 178 |
+
if fmt == "csv":
|
| 179 |
+
success = Utils.save_to_csv(test_data, filename)
|
| 180 |
+
elif fmt == "json":
|
| 181 |
+
success = Utils.save_to_json(test_data, filename)
|
| 182 |
+
elif fmt == "txt":
|
| 183 |
+
success = Utils.save_to_txt(test_data, filename)
|
| 184 |
+
|
| 185 |
+
if success:
|
| 186 |
+
print(f"✓ 数据已导出为 {fmt.upper()} 格式: {filename}")
|
| 187 |
+
|
| 188 |
+
# 显示文件内容预览
|
| 189 |
+
try:
|
| 190 |
+
with open(filename, 'r', encoding='utf-8') as f:
|
| 191 |
+
content = f.read()
|
| 192 |
+
print(f" 文件大小: {len(content)} 字节")
|
| 193 |
+
|
| 194 |
+
# 清理测试文件
|
| 195 |
+
os.remove(filename)
|
| 196 |
+
except:
|
| 197 |
+
pass
|
| 198 |
+
else:
|
| 199 |
+
print(f"✗ 导出为 {fmt.upper()} 格式失败")
|
| 200 |
+
|
| 201 |
+
def example_error_handling():
|
| 202 |
+
"""示例6: 错误处理"""
|
| 203 |
+
print("\n" + "=" * 60)
|
| 204 |
+
print("示例6: 错误处理")
|
| 205 |
+
print("=" * 60)
|
| 206 |
+
|
| 207 |
+
search = AnnouncementSearch()
|
| 208 |
+
|
| 209 |
+
# 测试无效查询
|
| 210 |
+
invalid_queries = [
|
| 211 |
+
"", # 空查询
|
| 212 |
+
"a", # 太短的查询
|
| 213 |
+
" ", # 只有空格的查询
|
| 214 |
+
]
|
| 215 |
+
|
| 216 |
+
for query in invalid_queries:
|
| 217 |
+
print(f"\n测试查询: '{query}'")
|
| 218 |
+
|
| 219 |
+
if Utils.validate_query(query):
|
| 220 |
+
success, results, message = search.search(query)
|
| 221 |
+
print(f" 结果: {message}")
|
| 222 |
+
else:
|
| 223 |
+
print(f" 验证失败: 查询无效")
|
| 224 |
+
|
| 225 |
+
def main():
|
| 226 |
+
"""主函数"""
|
| 227 |
+
if not setup_environment():
|
| 228 |
+
print("环境设置失败,请检查配置")
|
| 229 |
+
return
|
| 230 |
+
|
| 231 |
+
try:
|
| 232 |
+
example_basic_search()
|
| 233 |
+
example_multiple_queries()
|
| 234 |
+
example_smart_query_analysis()
|
| 235 |
+
example_batch_processing()
|
| 236 |
+
example_data_export()
|
| 237 |
+
example_error_handling()
|
| 238 |
+
|
| 239 |
+
print("\n" + "=" * 60)
|
| 240 |
+
print("所有示例执行完成!")
|
| 241 |
+
print("=" * 60)
|
| 242 |
+
print("\n更多使用方式:")
|
| 243 |
+
print("1. 使用命令行: announcement-search --help")
|
| 244 |
+
print("2. 查看详细文档: 参考 README.md 和 SKILL.md")
|
| 245 |
+
except KeyboardInterrupt:
|
| 246 |
+
print("\n\n示例被用户中断")
|
| 247 |
+
except Exception as e:
|
| 248 |
+
print(f"\n发生错误: {e}")
|
| 249 |
+
import traceback
|
| 250 |
+
traceback.print_exc()
|
| 251 |
+
|
| 252 |
+
if __name__ == "__main__":
|
| 253 |
+
main()
|
Skills/announcement-search/scripts/requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
requests>=2.28.0
|
| 2 |
+
pandas>=1.5.0
|
| 3 |
+
python-dotenv>=0.21.0
|
| 4 |
+
argparse>=1.4.0
|
| 5 |
+
logging>=0.4.9.6
|
| 6 |
+
json>=2.0.9
|
| 7 |
+
csv>=1.0
|
| 8 |
+
datetime>=4.3
|
| 9 |
+
os-sys>=2.0.1
|
| 10 |
+
sys>=3.0.0
|
Skills/announcement-search/scripts/setup.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from setuptools import setup, find_packages
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
def read_file(filename):
|
| 6 |
+
with open(os.path.join(os.path.dirname(__file__), filename), 'r', encoding='utf-8') as f:
|
| 7 |
+
return f.read()
|
| 8 |
+
|
| 9 |
+
def read_requirements():
|
| 10 |
+
requirements_file = os.path.join(os.path.dirname(__file__), 'requirements.txt')
|
| 11 |
+
if os.path.exists(requirements_file):
|
| 12 |
+
with open(requirements_file, 'r', encoding='utf-8') as f:
|
| 13 |
+
return [line.strip() for line in f if line.strip() and not line.startswith('#')]
|
| 14 |
+
return []
|
| 15 |
+
|
| 16 |
+
setup(
|
| 17 |
+
name="announcement-search",
|
| 18 |
+
version="1.0.0",
|
| 19 |
+
description="金融公告搜索工具 - 搜索A股、港股、基金、ETF等金融标的公告",
|
| 20 |
+
long_description=read_file('../README.md'),
|
| 21 |
+
long_description_content_type="text/markdown",
|
| 22 |
+
author="公告搜索技能开发团队",
|
| 23 |
+
author_email="",
|
| 24 |
+
url="",
|
| 25 |
+
packages=find_packages(),
|
| 26 |
+
py_modules=[
|
| 27 |
+
'config',
|
| 28 |
+
'utils',
|
| 29 |
+
'announcement_search'
|
| 30 |
+
],
|
| 31 |
+
scripts=['__main__.py'],
|
| 32 |
+
install_requires=read_requirements(),
|
| 33 |
+
entry_points={
|
| 34 |
+
'console_scripts': [
|
| 35 |
+
'announcement-search=__main__:main',
|
| 36 |
+
],
|
| 37 |
+
},
|
| 38 |
+
classifiers=[
|
| 39 |
+
'Development Status :: 4 - Beta',
|
| 40 |
+
'Intended Audience :: Financial and Insurance Industry',
|
| 41 |
+
'Topic :: Office/Business :: Financial :: Investment',
|
| 42 |
+
'License :: OSI Approved :: MIT License',
|
| 43 |
+
'Programming Language :: Python :: 3',
|
| 44 |
+
'Programming Language :: Python :: 3.7',
|
| 45 |
+
'Programming Language :: Python :: 3.8',
|
| 46 |
+
'Programming Language :: Python :: 3.9',
|
| 47 |
+
'Programming Language :: Python :: 3.10',
|
| 48 |
+
'Programming Language :: Python :: 3.11',
|
| 49 |
+
],
|
| 50 |
+
keywords='finance, announcement, search, stock, fund, etf',
|
| 51 |
+
project_urls={
|
| 52 |
+
'Documentation': 'https://github.com/example/announcement-search',
|
| 53 |
+
'Source': 'https://github.com/example/announcement-search',
|
| 54 |
+
'Tracker': 'https://github.com/example/announcement-search/issues',
|
| 55 |
+
},
|
| 56 |
+
python_requires='>=3.7',
|
| 57 |
+
include_package_data=True,
|
| 58 |
+
zip_safe=False,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
if __name__ == "__main__":
|
| 62 |
+
print("公告搜索工具安装脚本")
|
| 63 |
+
print("=" * 40)
|
| 64 |
+
print("安装方法:")
|
| 65 |
+
print("1. 基本安装: pip install -e .")
|
| 66 |
+
print("2. 开发安装: pip install -e .[dev]")
|
| 67 |
+
print("3. 生产安装: pip install .")
|
| 68 |
+
print()
|
| 69 |
+
print("安装后可以使用: announcement-search --help")
|
Skills/announcement-search/scripts/test_basic.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
公告搜索工具基本测试
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import unittest
|
| 9 |
+
import tempfile
|
| 10 |
+
import json
|
| 11 |
+
from unittest.mock import patch, MagicMock
|
| 12 |
+
|
| 13 |
+
# 添加当前目录到Python路径
|
| 14 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 15 |
+
|
| 16 |
+
from config import Config
|
| 17 |
+
from utils import Utils
|
| 18 |
+
from announcement_search import AnnouncementSearch
|
| 19 |
+
|
| 20 |
+
class TestConfig(unittest.TestCase):
|
| 21 |
+
def test_config_initialization(self):
|
| 22 |
+
"""测试配置初始化"""
|
| 23 |
+
config = Config()
|
| 24 |
+
self.assertIsNotNone(config.config)
|
| 25 |
+
|
| 26 |
+
# 检查基本配置项
|
| 27 |
+
self.assertIn("api", config.config)
|
| 28 |
+
self.assertIn("auth", config.config)
|
| 29 |
+
self.assertIn("search", config.config)
|
| 30 |
+
|
| 31 |
+
def test_get_api_key(self):
|
| 32 |
+
"""测试获取API Key"""
|
| 33 |
+
config = Config()
|
| 34 |
+
|
| 35 |
+
# 测试环境变量中的API Key
|
| 36 |
+
with patch.dict(os.environ, {"IWENCAI_API_KEY": "test_key"}):
|
| 37 |
+
config = Config()
|
| 38 |
+
api_key = config.get_api_key()
|
| 39 |
+
self.assertEqual(api_key, "test_key")
|
| 40 |
+
|
| 41 |
+
# 测试缺少API Key的情况
|
| 42 |
+
with patch.dict(os.environ, {}, clear=True):
|
| 43 |
+
config = Config()
|
| 44 |
+
with self.assertRaises(ValueError):
|
| 45 |
+
config.get_api_key()
|
| 46 |
+
|
| 47 |
+
def test_validate_config(self):
|
| 48 |
+
"""测试配置验证"""
|
| 49 |
+
config = Config()
|
| 50 |
+
|
| 51 |
+
# 测试有效配置
|
| 52 |
+
with patch.dict(os.environ, {"IWENCAI_API_KEY": "valid_key"}):
|
| 53 |
+
config = Config()
|
| 54 |
+
self.assertTrue(config.validate())
|
| 55 |
+
|
| 56 |
+
# 测试无效配置(缺少API Key)
|
| 57 |
+
with patch.dict(os.environ, {}, clear=True):
|
| 58 |
+
config = Config()
|
| 59 |
+
self.assertFalse(config.validate())
|
| 60 |
+
|
| 61 |
+
class TestUtils(unittest.TestCase):
|
| 62 |
+
def test_validate_query(self):
|
| 63 |
+
"""测试查询验证"""
|
| 64 |
+
# 有效查询
|
| 65 |
+
self.assertTrue(Utils.validate_query("贵州茅台"))
|
| 66 |
+
self.assertTrue(Utils.validate_query("上市公司业绩预告"))
|
| 67 |
+
|
| 68 |
+
# 无效查询
|
| 69 |
+
self.assertFalse(Utils.validate_query(""))
|
| 70 |
+
self.assertFalse(Utils.validate_query(" "))
|
| 71 |
+
self.assertFalse(Utils.validate_query("a"))
|
| 72 |
+
|
| 73 |
+
def test_generate_cache_key(self):
|
| 74 |
+
"""测试缓存键生成"""
|
| 75 |
+
key1 = Utils.generate_cache_key("test query", 10)
|
| 76 |
+
key2 = Utils.generate_cache_key("test query", 10)
|
| 77 |
+
key3 = Utils.generate_cache_key("different query", 10)
|
| 78 |
+
|
| 79 |
+
# 相同查询应生成相同键
|
| 80 |
+
self.assertEqual(key1, key2)
|
| 81 |
+
|
| 82 |
+
# 不同查询应生成不同键
|
| 83 |
+
self.assertNotEqual(key1, key3)
|
| 84 |
+
|
| 85 |
+
def test_save_to_csv(self):
|
| 86 |
+
"""测试保存为CSV"""
|
| 87 |
+
test_data = [
|
| 88 |
+
{"title": "测试标题1", "summary": "测试摘要1", "url": "http://test1.com", "publish_date": "2024-01-01 10:00:00"},
|
| 89 |
+
{"title": "测试标题2", "summary": "测试摘要2", "url": "http://test2.com", "publish_date": "2024-01-02 11:00:00"}
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as tmp:
|
| 93 |
+
tmp_path = tmp.name
|
| 94 |
+
|
| 95 |
+
try:
|
| 96 |
+
success = Utils.save_to_csv(test_data, tmp_path)
|
| 97 |
+
self.assertTrue(success)
|
| 98 |
+
|
| 99 |
+
# 验证文件内容
|
| 100 |
+
with open(tmp_path, 'r', encoding='utf-8') as f:
|
| 101 |
+
content = f.read()
|
| 102 |
+
self.assertIn("测试标题1", content)
|
| 103 |
+
self.assertIn("测试标题2", content)
|
| 104 |
+
self.assertIn("title,summary,url,publish_date", content)
|
| 105 |
+
finally:
|
| 106 |
+
if os.path.exists(tmp_path):
|
| 107 |
+
os.remove(tmp_path)
|
| 108 |
+
|
| 109 |
+
def test_save_to_json(self):
|
| 110 |
+
"""测试保存为JSON"""
|
| 111 |
+
test_data = [
|
| 112 |
+
{"title": "测试标题", "summary": "测试摘要", "url": "http://test.com", "publish_date": "2024-01-01 10:00:00"}
|
| 113 |
+
]
|
| 114 |
+
|
| 115 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp:
|
| 116 |
+
tmp_path = tmp.name
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
success = Utils.save_to_json(test_data, tmp_path)
|
| 120 |
+
self.assertTrue(success)
|
| 121 |
+
|
| 122 |
+
# 验证文件内容
|
| 123 |
+
with open(tmp_path, 'r', encoding='utf-8') as f:
|
| 124 |
+
loaded_data = json.load(f)
|
| 125 |
+
self.assertEqual(len(loaded_data), 1)
|
| 126 |
+
self.assertEqual(loaded_data[0]["title"], "测试标题")
|
| 127 |
+
finally:
|
| 128 |
+
if os.path.exists(tmp_path):
|
| 129 |
+
os.remove(tmp_path)
|
| 130 |
+
|
| 131 |
+
def test_read_queries_from_file(self):
|
| 132 |
+
"""测试从文件读取查询"""
|
| 133 |
+
test_queries = [
|
| 134 |
+
"# 注释行",
|
| 135 |
+
"查询1",
|
| 136 |
+
"",
|
| 137 |
+
"查询2",
|
| 138 |
+
" 查询3 " # 带空格的查询
|
| 139 |
+
]
|
| 140 |
+
|
| 141 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as tmp:
|
| 142 |
+
tmp.write("\n".join(test_queries))
|
| 143 |
+
tmp_path = tmp.name
|
| 144 |
+
|
| 145 |
+
try:
|
| 146 |
+
queries = Utils.read_queries_from_file(tmp_path)
|
| 147 |
+
|
| 148 |
+
# 应该读取3个查询(忽略注释行和空行)
|
| 149 |
+
self.assertEqual(len(queries), 3)
|
| 150 |
+
self.assertIn("查询1", queries)
|
| 151 |
+
self.assertIn("查询2", queries)
|
| 152 |
+
self.assertIn("查询3", queries) # 空格应该被去除
|
| 153 |
+
finally:
|
| 154 |
+
if os.path.exists(tmp_path):
|
| 155 |
+
os.remove(tmp_path)
|
| 156 |
+
|
| 157 |
+
def test_format_results_for_display(self):
|
| 158 |
+
"""测试结果格式化显示"""
|
| 159 |
+
test_results = [
|
| 160 |
+
{"title": "测试标题1", "summary": "测试摘要1", "url": "http://test1.com", "publish_date": "2024-01-01 10:00:00"},
|
| 161 |
+
{"title": "测试标题2", "summary": "测试摘要2", "url": "http://test2.com", "publish_date": "2024-01-02 11:00:00"}
|
| 162 |
+
]
|
| 163 |
+
|
| 164 |
+
formatted = Utils.format_results_for_display(test_results)
|
| 165 |
+
|
| 166 |
+
# 检查基本内容
|
| 167 |
+
self.assertIn("找到 2 条相关公告", formatted)
|
| 168 |
+
self.assertIn("测试标题1", formatted)
|
| 169 |
+
self.assertIn("测试标题2", formatted)
|
| 170 |
+
|
| 171 |
+
# 测试空结果
|
| 172 |
+
empty_formatted = Utils.format_results_for_display([])
|
| 173 |
+
self.assertEqual(empty_formatted, "未找到相关公告信息。")
|
| 174 |
+
|
| 175 |
+
class TestAnnouncementSearch(unittest.TestCase):
|
| 176 |
+
def setUp(self):
|
| 177 |
+
"""测试前置设置"""
|
| 178 |
+
self.search = AnnouncementSearch()
|
| 179 |
+
|
| 180 |
+
def test_smart_query_analysis(self):
|
| 181 |
+
"""测试智能查询分析"""
|
| 182 |
+
# 测试简单查询
|
| 183 |
+
queries = self.search.smart_query_analysis("贵州茅台")
|
| 184 |
+
self.assertEqual(queries, ["贵州茅台"])
|
| 185 |
+
|
| 186 |
+
# 测试包含"和"的查询
|
| 187 |
+
queries = self.search.smart_query_analysis("贵州茅台和五粮液")
|
| 188 |
+
self.assertIn("贵州茅台 公告", queries)
|
| 189 |
+
self.assertIn("五粮液 公告", queries)
|
| 190 |
+
|
| 191 |
+
# 测试包含"与"的查询
|
| 192 |
+
queries = self.search.smart_query_analysis("宁德时代与比亚迪")
|
| 193 |
+
self.assertIn("宁德时代 公告", queries)
|
| 194 |
+
self.assertIn("比亚迪 公告", queries)
|
| 195 |
+
|
| 196 |
+
# 测试包含"、"的查询
|
| 197 |
+
queries = self.search.smart_query_analysis("中国平安、中国人寿")
|
| 198 |
+
self.assertIn("中国平安 公告", queries)
|
| 199 |
+
self.assertIn("中国人寿 公告", queries)
|
| 200 |
+
|
| 201 |
+
def test_evaluate_results(self):
|
| 202 |
+
"""测试结果评估"""
|
| 203 |
+
test_results = [
|
| 204 |
+
{"title": "贵州茅台2023年度业绩预告", "summary": "公司预计2023年度净利润增长"},
|
| 205 |
+
{"title": "五粮液分红公告", "summary": "公司宣布年度分红方案"},
|
| 206 |
+
{"title": "无关公告", "summary": "其他内容"}
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
# 测试高相关性
|
| 210 |
+
relevant, evaluation = self.search.evaluate_results("贵州茅台 业绩预告", test_results)
|
| 211 |
+
self.assertTrue(relevant)
|
| 212 |
+
self.assertIn("较高", evaluation)
|
| 213 |
+
|
| 214 |
+
# 测试低相关性
|
| 215 |
+
relevant, evaluation = self.search.evaluate_results("完全无关的查询", test_results)
|
| 216 |
+
self.assertFalse(relevant)
|
| 217 |
+
self.assertIn("较低", evaluation)
|
| 218 |
+
|
| 219 |
+
def test_generate_search_summary(self):
|
| 220 |
+
"""测试搜索摘要生成"""
|
| 221 |
+
test_results = [
|
| 222 |
+
{"title": "标题1", "summary": "摘要1", "url": "http://test1.com", "publish_date": "2024-01-01 10:00:00"},
|
| 223 |
+
{"title": "标题2", "summary": "摘要2", "url": "http://test2.com", "publish_date": "2024-01-02 11:00:00"},
|
| 224 |
+
{"title": "标题3", "summary": "摘要3", "url": "http://test3.com", "publish_date": "2024-01-03 12:00:00"},
|
| 225 |
+
{"title": "标题4", "summary": "摘要4", "url": "http://test4.com", "publish_date": "2024-01-04 13:00:00"},
|
| 226 |
+
{"title": "标题5", "summary": "摘要5", "url": "http://test5.com", "publish_date": "2024-01-05 14:00:00"},
|
| 227 |
+
{"title": "标题6", "summary": "摘要6", "url": "http://test6.com", "publish_date": "2024-01-06 15:00:00"}
|
| 228 |
+
]
|
| 229 |
+
|
| 230 |
+
summary = self.search.generate_search_summary("测试查询", test_results)
|
| 231 |
+
|
| 232 |
+
# 检查基本内容
|
| 233 |
+
self.assertIn("测试查询", summary)
|
| 234 |
+
self.assertIn("找到 6 条相关公告", summary)
|
| 235 |
+
self.assertIn("标题1", summary)
|
| 236 |
+
self.assertIn("标题5", summary)
|
| 237 |
+
self.assertIn("还有 1 条结果", summary) # 只显示前5条
|
| 238 |
+
|
| 239 |
+
# 测试空结果
|
| 240 |
+
empty_summary = self.search.generate_search_summary("空查询", [])
|
| 241 |
+
self.assertIn("未找到相关公告信息", empty_summary)
|
| 242 |
+
|
| 243 |
+
@patch('announcement_search.requests.post')
|
| 244 |
+
def test_search_success(self, mock_post):
|
| 245 |
+
"""测试成功搜索"""
|
| 246 |
+
# 模拟API响应
|
| 247 |
+
mock_response = MagicMock()
|
| 248 |
+
mock_response.status_code = 200
|
| 249 |
+
mock_response.json.return_value = {
|
| 250 |
+
"data": [
|
| 251 |
+
{"title": "测试公告1", "summary": "测试摘要1", "url": "http://test1.com", "publish_date": "2024-01-01 10:00:00"},
|
| 252 |
+
{"title": "测试公告2", "summary": "测试摘要2", "url": "http://test2.com", "publish_date": "2024-01-02 11:00:00"}
|
| 253 |
+
]
|
| 254 |
+
}
|
| 255 |
+
mock_post.return_value = mock_response
|
| 256 |
+
|
| 257 |
+
success, results, message = self.search.search("测试查询", limit=2)
|
| 258 |
+
|
| 259 |
+
self.assertTrue(success)
|
| 260 |
+
self.assertEqual(len(results), 2)
|
| 261 |
+
self.assertEqual(message, "搜索成功")
|
| 262 |
+
self.assertEqual(results[0]["title"], "测试公告1")
|
| 263 |
+
self.assertEqual(results[1]["title"], "测试公告2")
|
| 264 |
+
|
| 265 |
+
@patch('announcement_search.requests.post')
|
| 266 |
+
def test_search_empty_results(self, mock_post):
|
| 267 |
+
"""测试空结果搜索"""
|
| 268 |
+
mock_response = MagicMock()
|
| 269 |
+
mock_response.status_code = 200
|
| 270 |
+
mock_response.json.return_value = {"data": []}
|
| 271 |
+
mock_post.return_value = mock_response
|
| 272 |
+
|
| 273 |
+
success, results, message = self.search.search("测试查询")
|
| 274 |
+
|
| 275 |
+
self.assertTrue(success)
|
| 276 |
+
self.assertEqual(len(results), 0)
|
| 277 |
+
self.assertEqual(message, "未找到相关公告")
|
| 278 |
+
|
| 279 |
+
@patch('announcement_search.requests.post')
|
| 280 |
+
def test_search_auth_failure(self, mock_post):
|
| 281 |
+
"""测试认证失败"""
|
| 282 |
+
mock_response = MagicMock()
|
| 283 |
+
mock_response.status_code = 401
|
| 284 |
+
mock_post.return_value = mock_response
|
| 285 |
+
|
| 286 |
+
success, results, message = self.search.search("测试查询")
|
| 287 |
+
|
| 288 |
+
self.assertFalse(success)
|
| 289 |
+
self.assertEqual(len(results), 0)
|
| 290 |
+
self.assertIn("API认证失败", message)
|
| 291 |
+
|
| 292 |
+
@patch('announcement_search.requests.post')
|
| 293 |
+
def test_search_network_error(self, mock_post):
|
| 294 |
+
"""测试网络错误"""
|
| 295 |
+
mock_post.side_effect = ConnectionError("网络连接错误")
|
| 296 |
+
|
| 297 |
+
success, results, message = self.search.search("测试查询")
|
| 298 |
+
|
| 299 |
+
self.assertFalse(success)
|
| 300 |
+
self.assertEqual(len(results), 0)
|
| 301 |
+
self.assertIn("网络连接错误", message)
|
| 302 |
+
|
| 303 |
+
def run_tests():
|
| 304 |
+
"""运行所有测试"""
|
| 305 |
+
print("运行公告搜索工具测试...")
|
| 306 |
+
print("=" * 60)
|
| 307 |
+
|
| 308 |
+
# 创建测试套件
|
| 309 |
+
loader = unittest.TestLoader()
|
| 310 |
+
suite = unittest.TestSuite()
|
| 311 |
+
|
| 312 |
+
# 添加测试类
|
| 313 |
+
suite.addTests(loader.loadTestsFromTestCase(TestConfig))
|
| 314 |
+
suite.addTests(loader.loadTestsFromTestCase(TestUtils))
|
| 315 |
+
suite.addTests(loader.loadTestsFromTestCase(TestAnnouncementSearch))
|
| 316 |
+
|
| 317 |
+
# 运行测试
|
| 318 |
+
runner = unittest.TextTestRunner(verbosity=2)
|
| 319 |
+
result = runner.run(suite)
|
| 320 |
+
|
| 321 |
+
print("=" * 60)
|
| 322 |
+
print(f"测试完成: {result.testsRun} 个测试用例")
|
| 323 |
+
print(f"通过: {result.testsRun - len(result.failures) - len(result.errors)}")
|
| 324 |
+
print(f"失败: {len(result.failures)}")
|
| 325 |
+
print(f"错误: {len(result.errors)}")
|
| 326 |
+
|
| 327 |
+
return result.wasSuccessful()
|
| 328 |
+
|
| 329 |
+
if __name__ == "__main__":
|
| 330 |
+
# 设置环境变量用于测试
|
| 331 |
+
os.environ["IWENCAI_API_KEY"] = "test_key_for_unit_tests"
|
| 332 |
+
os.environ["LOG_LEVEL"] = "ERROR" # 测试时减少日志输出
|
| 333 |
+
|
| 334 |
+
success = run_tests()
|
| 335 |
+
sys.exit(0 if success else 1)
|
Skills/announcement-search/scripts/test_queries.txt
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 公告搜索测试查询文件
|
| 2 |
+
# 每行一个查询,空行和以#开头的行会被忽略
|
| 3 |
+
|
| 4 |
+
# 1. 公司公告查询
|
| 5 |
+
贵州茅台 公告
|
| 6 |
+
中国平安 最新公告
|
| 7 |
+
宁德时代 业绩预告
|
| 8 |
+
|
| 9 |
+
# 2. 公告类型查询
|
| 10 |
+
分红派息公告
|
| 11 |
+
回购增持公告
|
| 12 |
+
资产重组公告
|
| 13 |
+
|
| 14 |
+
# 3. 定期报告查询
|
| 15 |
+
上市公司年报
|
| 16 |
+
2023年年度报告
|
| 17 |
+
第一季度报告
|
| 18 |
+
|
| 19 |
+
# 4. 综合查询
|
| 20 |
+
重大合同公告
|
| 21 |
+
业绩快报
|
| 22 |
+
权益变动公告
|
| 23 |
+
|
| 24 |
+
# 5. 行业公告查询
|
| 25 |
+
银行股 公告
|
| 26 |
+
券商 最新公告
|
| 27 |
+
保险行业 公告
|
| 28 |
+
|
| 29 |
+
# 6. 事件驱动查询
|
| 30 |
+
限售股解禁
|
| 31 |
+
股东减持
|
| 32 |
+
股权激励
|
| 33 |
+
|
| 34 |
+
# 7. 市场热点查询
|
| 35 |
+
注册制相关公告
|
| 36 |
+
北交所 公告
|
| 37 |
+
科创板 业绩预告
|
| 38 |
+
|
| 39 |
+
# 8. 基金公告查询
|
| 40 |
+
基金分红公告
|
| 41 |
+
ETF 公告
|
| 42 |
+
公募基金 最新公告
|
| 43 |
+
|
| 44 |
+
# 9. 港股公告查询
|
| 45 |
+
港股通 公告
|
| 46 |
+
H股 最新公告
|
| 47 |
+
红筹股 公告
|
| 48 |
+
|
| 49 |
+
# 10. 综合金融查询
|
| 50 |
+
债券发行公告
|
| 51 |
+
可转债 公告
|
| 52 |
+
资产证券化 公告
|
Skills/announcement-search/scripts/utils.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
import csv
|
| 5 |
+
import logging
|
| 6 |
+
import hashlib
|
| 7 |
+
import time
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import Dict, List, Any, Optional, Union
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
class Utils:
|
| 15 |
+
@staticmethod
|
| 16 |
+
def setup_logging(level: str = "INFO"):
|
| 17 |
+
log_level = getattr(logging, level.upper(), logging.INFO)
|
| 18 |
+
logging.basicConfig(
|
| 19 |
+
level=log_level,
|
| 20 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
| 21 |
+
handlers=[
|
| 22 |
+
logging.StreamHandler(sys.stdout)
|
| 23 |
+
]
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
@staticmethod
|
| 27 |
+
def validate_query(query: str) -> bool:
|
| 28 |
+
if not query or not query.strip():
|
| 29 |
+
logger.error("Query cannot be empty")
|
| 30 |
+
return False
|
| 31 |
+
|
| 32 |
+
if len(query.strip()) < 2:
|
| 33 |
+
logger.error("Query is too short")
|
| 34 |
+
return False
|
| 35 |
+
|
| 36 |
+
return True
|
| 37 |
+
|
| 38 |
+
@staticmethod
|
| 39 |
+
def parse_date(date_str: str, format_str: str = "%Y-%m-%d %H:%M:%S") -> Optional[datetime]:
|
| 40 |
+
try:
|
| 41 |
+
return datetime.strptime(date_str, format_str)
|
| 42 |
+
except ValueError:
|
| 43 |
+
logger.warning(f"Failed to parse date: {date_str}")
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
@staticmethod
|
| 47 |
+
def format_date(date_obj: datetime, format_str: str = "%Y-%m-%d %H:%M:%S") -> str:
|
| 48 |
+
return date_obj.strftime(format_str)
|
| 49 |
+
|
| 50 |
+
@staticmethod
|
| 51 |
+
def generate_cache_key(query: str, limit: int = 10) -> str:
|
| 52 |
+
key_str = f"{query}_{limit}"
|
| 53 |
+
return hashlib.md5(key_str.encode()).hexdigest()
|
| 54 |
+
|
| 55 |
+
@staticmethod
|
| 56 |
+
def save_to_csv(data: List[Dict[str, Any]], filepath: str) -> bool:
|
| 57 |
+
try:
|
| 58 |
+
if not data:
|
| 59 |
+
logger.warning("No data to save")
|
| 60 |
+
return False
|
| 61 |
+
|
| 62 |
+
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
| 63 |
+
|
| 64 |
+
fieldnames = data[0].keys()
|
| 65 |
+
with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
|
| 66 |
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
| 67 |
+
writer.writeheader()
|
| 68 |
+
writer.writerows(data)
|
| 69 |
+
|
| 70 |
+
logger.info(f"Data saved to CSV: {filepath}")
|
| 71 |
+
return True
|
| 72 |
+
except Exception as e:
|
| 73 |
+
logger.error(f"Failed to save CSV: {e}")
|
| 74 |
+
return False
|
| 75 |
+
|
| 76 |
+
@staticmethod
|
| 77 |
+
def save_to_json(data: List[Dict[str, Any]], filepath: str) -> bool:
|
| 78 |
+
try:
|
| 79 |
+
if not data:
|
| 80 |
+
logger.warning("No data to save")
|
| 81 |
+
return False
|
| 82 |
+
|
| 83 |
+
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
| 84 |
+
|
| 85 |
+
with open(filepath, 'w', encoding='utf-8') as jsonfile:
|
| 86 |
+
json.dump(data, jsonfile, ensure_ascii=False, indent=2)
|
| 87 |
+
|
| 88 |
+
logger.info(f"Data saved to JSON: {filepath}")
|
| 89 |
+
return True
|
| 90 |
+
except Exception as e:
|
| 91 |
+
logger.error(f"Failed to save JSON: {e}")
|
| 92 |
+
return False
|
| 93 |
+
|
| 94 |
+
@staticmethod
|
| 95 |
+
def save_to_txt(data: List[Dict[str, Any]], filepath: str) -> bool:
|
| 96 |
+
try:
|
| 97 |
+
if not data:
|
| 98 |
+
logger.warning("No data to save")
|
| 99 |
+
return False
|
| 100 |
+
|
| 101 |
+
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
| 102 |
+
|
| 103 |
+
with open(filepath, 'w', encoding='utf-8') as txtfile:
|
| 104 |
+
for i, item in enumerate(data, 1):
|
| 105 |
+
txtfile.write(f"结果 {i}:\n")
|
| 106 |
+
txtfile.write(f"标题: {item.get('title', '')}\n")
|
| 107 |
+
txtfile.write(f"摘要: {item.get('summary', '')}\n")
|
| 108 |
+
txtfile.write(f"链接: {item.get('url', '')}\n")
|
| 109 |
+
txtfile.write(f"发布时间: {item.get('publish_date', '')}\n")
|
| 110 |
+
txtfile.write("-" * 50 + "\n\n")
|
| 111 |
+
|
| 112 |
+
logger.info(f"Data saved to TXT: {filepath}")
|
| 113 |
+
return True
|
| 114 |
+
except Exception as e:
|
| 115 |
+
logger.error(f"Failed to save TXT: {e}")
|
| 116 |
+
return False
|
| 117 |
+
|
| 118 |
+
@staticmethod
|
| 119 |
+
def read_queries_from_file(filepath: str) -> List[str]:
|
| 120 |
+
try:
|
| 121 |
+
if not os.path.exists(filepath):
|
| 122 |
+
logger.error(f"Input file not found: {filepath}")
|
| 123 |
+
return []
|
| 124 |
+
|
| 125 |
+
queries = []
|
| 126 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 127 |
+
for line in f:
|
| 128 |
+
line = line.strip()
|
| 129 |
+
if line and not line.startswith('#'):
|
| 130 |
+
queries.append(line)
|
| 131 |
+
|
| 132 |
+
logger.info(f"Read {len(queries)} queries from {filepath}")
|
| 133 |
+
return queries
|
| 134 |
+
except Exception as e:
|
| 135 |
+
logger.error(f"Failed to read queries from file: {e}")
|
| 136 |
+
return []
|
| 137 |
+
|
| 138 |
+
@staticmethod
|
| 139 |
+
def ensure_directory(directory: str) -> bool:
|
| 140 |
+
try:
|
| 141 |
+
Path(directory).mkdir(parents=True, exist_ok=True)
|
| 142 |
+
return True
|
| 143 |
+
except Exception as e:
|
| 144 |
+
logger.error(f"Failed to create directory {directory}: {e}")
|
| 145 |
+
return False
|
| 146 |
+
|
| 147 |
+
@staticmethod
|
| 148 |
+
def get_file_extension(filepath: str) -> str:
|
| 149 |
+
return os.path.splitext(filepath)[1].lower().lstrip('.')
|
| 150 |
+
|
| 151 |
+
@staticmethod
|
| 152 |
+
def format_results_for_display(results: List[Dict[str, Any]]) -> str:
|
| 153 |
+
if not results:
|
| 154 |
+
return "未找到相关公告信息。"
|
| 155 |
+
|
| 156 |
+
output_lines = []
|
| 157 |
+
output_lines.append(f"找到 {len(results)} 条相关公告:")
|
| 158 |
+
output_lines.append("=" * 60)
|
| 159 |
+
|
| 160 |
+
for i, result in enumerate(results, 1):
|
| 161 |
+
output_lines.append(f"{i}. {result.get('title', '')}")
|
| 162 |
+
output_lines.append(f" 摘要: {result.get('summary', '')}")
|
| 163 |
+
output_lines.append(f" 发布时间: {result.get('publish_date', '')}")
|
| 164 |
+
output_lines.append(f" 链接: {result.get('url', '')}")
|
| 165 |
+
output_lines.append("-" * 40)
|
| 166 |
+
|
| 167 |
+
return "\n".join(output_lines)
|
| 168 |
+
|
| 169 |
+
@staticmethod
|
| 170 |
+
def calculate_execution_time(start_time: float) -> float:
|
| 171 |
+
return time.time() - start_time
|
| 172 |
+
|
| 173 |
+
@staticmethod
|
| 174 |
+
def format_execution_time(seconds: float) -> str:
|
| 175 |
+
if seconds < 1:
|
| 176 |
+
return f"{seconds*1000:.0f}毫秒"
|
| 177 |
+
elif seconds < 60:
|
| 178 |
+
return f"{seconds:.1f}秒"
|
| 179 |
+
else:
|
| 180 |
+
minutes = seconds / 60
|
| 181 |
+
return f"{minutes:.1f}分钟"
|
| 182 |
+
|
| 183 |
+
if __name__ == "__main__":
|
| 184 |
+
Utils.setup_logging()
|
| 185 |
+
logger.info("Utils module loaded successfully")
|
| 186 |
+
|
| 187 |
+
test_data = [
|
| 188 |
+
{
|
| 189 |
+
"title": "测试公告标题",
|
| 190 |
+
"summary": "测试公告摘要",
|
| 191 |
+
"url": "https://example.com/test",
|
| 192 |
+
"publish_date": "2024-01-01 10:00:00"
|
| 193 |
+
}
|
| 194 |
+
]
|
| 195 |
+
|
| 196 |
+
print("Testing CSV save:")
|
| 197 |
+
Utils.save_to_csv(test_data, "test_output.csv")
|
| 198 |
+
|
| 199 |
+
print("\nTesting JSON save:")
|
| 200 |
+
Utils.save_to_json(test_data, "test_output.json")
|
| 201 |
+
|
| 202 |
+
print("\nTesting TXT save:")
|
| 203 |
+
Utils.save_to_txt(test_data, "test_output.txt")
|
| 204 |
+
|
| 205 |
+
print("\nTesting display formatting:")
|
| 206 |
+
print(Utils.format_results_for_display(test_data))
|
Skills/financial-query/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
Skills/financial-query/SKILL.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: financial-query
|
| 3 |
+
displayName: 金融数据查询
|
| 4 |
+
description: 金融结构化数据统一查询入口。通过同花顺问财 OpenAPI `query2data` 端点,用自然语言查询A股、指数、港股、美股、基金、ETF、期货、宏观、可转债等全市场金融结构化数据,支持行情指标、技术形态、财务指标、行业概念等多条件组合筛选标的,同时支持事件数据、经营数据、财务数据等查询。新闻/公告/研报全文等纯文本类查询走 `news-search` / `announcement-search` / `report-search`这几个SKill。
|
| 5 |
+
license: Complete terms in LICENSE.txt
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
# 金融数据查询 使用指南
|
| 9 |
+
|
| 10 |
+
## 版本
|
| 11 |
+
|
| 12 |
+
`2.0.0`(与 `X-Claw-Skill-Version` 保持一致)。本地 `name` 为 `financial-query`;同花顺问财平台上的注册名(`X-Claw-Skill-Id`)仍为 `hithink-financial-query`,API 调用必须沿用注册名。
|
| 13 |
+
|
| 14 |
+
## 技能概述
|
| 15 |
+
|
| 16 |
+
本 skill 是**金融结构化数据查询的统一入口**,通过调用同花顺问财 OpenAPI 的 `query2data` 端点,用一句自然语言就能拿到 A 股 / 港股 / 美股 / 基金 / 期货 / ETF / 板块 / 概念 / 指数 等全市场的结构化数据。
|
| 17 |
+
|
| 18 |
+
**覆盖标的(标的种类,不限于此):**
|
| 19 |
+
|
| 20 |
+
- **股票**:A 股、港股、美股
|
| 21 |
+
- **基金**:公募基金、ETF、LOF、指数基金
|
| 22 |
+
- **指数**:宽基指数、行业指数、概念指数
|
| 23 |
+
- **期货 / 债券 / 可转债**
|
| 24 |
+
- **板块 / 概念 / 行业 / 地域**:成分、资金、涨跌
|
| 25 |
+
- **宏观经济**:GDP、CPI、PMI、社融、LPR
|
| 26 |
+
|
| 27 |
+
**支持查询的指标维度(不限于此):**
|
| 28 |
+
|
| 29 |
+
- **行情**:最新价、涨跌幅、成交量、成交额、换手率、市值、振幅
|
| 30 |
+
- **估值**:PE、PB、PS、PEG、股息率、估值分位
|
| 31 |
+
- **财务**:营收、净利润、ROE、毛利率、负债率、现金流、EPS、研发投入
|
| 32 |
+
- **事件**:业绩预告、分红、回购、增持、解禁、龙虎榜、大宗交易、调研
|
| 33 |
+
- **资金**:主力资金净流入、北向资金、融资融券
|
| 34 |
+
- **指数 / 板块数据**:指数点位、成分股、板块涨跌
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
## 使用前
|
| 38 |
+
|
| 39 |
+
> **首次使用 - 获取 API Key**
|
| 40 |
+
> 所有技能都需要 `IWENCAI_API_KEY` 环境变量。如果用户尚未配置,按以下步骤引导:
|
| 41 |
+
>
|
| 42 |
+
> **步骤 1**:打开同花顺 i 问财 SkillHub → `https://www.iwencai.com/skillhub`
|
| 43 |
+
>
|
| 44 |
+
> **步骤 2**:登录
|
| 45 |
+
>
|
| 46 |
+
> **步骤 3**:点击具体的 Skill,在弹窗的"安装方式 → Agent 用户"中复制 `IWENCAI_API_KEY`
|
| 47 |
+
>
|
| 48 |
+
> **步骤 4**:配置环境变量
|
| 49 |
+
>
|
| 50 |
+
> ```bash
|
| 51 |
+
> # macOS / Linux
|
| 52 |
+
> export IWENCAI_API_KEY="your-api-key"
|
| 53 |
+
>
|
| 54 |
+
> # PowerShell
|
| 55 |
+
> $env:IWENCAI_API_KEY = "your-api-key"
|
| 56 |
+
> ```
|
| 57 |
+
|
| 58 |
+
## 核心处理流程
|
| 59 |
+
|
| 60 |
+
### 步骤 1: 接收用户 Query
|
| 61 |
+
|
| 62 |
+
接收自然语言请求,识别**标的(股票/基金/指数/板块/期货/债券)**、**指标(行情/财务/估值/事件)**、**时间范围**。
|
| 63 |
+
|
| 64 |
+
> **注意:本 skill 不做"筛选/选股/TopN/排名"**——若用户问的是"前 N 名 / 满足条件的所有标的 / 帮我选股"等,请改交给 `hithink-astock-selector` 等专用选股 skill。
|
| 65 |
+
|
| 66 |
+
### 步骤 2: Query 改写
|
| 67 |
+
|
| 68 |
+
将口语化问句改写为标准问财查询问句,**保持原意不变**:
|
| 69 |
+
|
| 70 |
+
- `"贵州茅台现在多少钱"` → `"贵州茅台 最新价"`
|
| 71 |
+
- `"贵州茅台的 PE 是多少"` → `"贵州茅台 PE(TTM)"`
|
| 72 |
+
- `"中证 500 指数当前点位"` → `"中证 500 指数 最新点位"`
|
| 73 |
+
- `"比亚迪最近的销量"` → `"比亚迪 月度销量"`
|
| 74 |
+
- `"宁德时代去年营收"` → `"宁德时代 2024 营业收入"`
|
| 75 |
+
|
| 76 |
+
**思维链拆解(按需):**
|
| 77 |
+
- 单次查询:能一句话答的,直接发
|
| 78 |
+
- 多次查询:需要多角度数据的,拆成 2-4 个独立 query 并行调用
|
| 79 |
+
|
| 80 |
+
### 步骤 3: API 调用
|
| 81 |
+
|
| 82 |
+
调用同花顺问财 OpenAPI 网关的 `query2data` 端点,使用 `scripts/cli.py` CLI 或直接构造 HTTP 请求。**所有请求必须严格携带 8 个 Header**:
|
| 83 |
+
|
| 84 |
+
| Header | 取值说明 |
|
| 85 |
+
|--------|----------|
|
| 86 |
+
| `Authorization` | `Bearer <IWENCAI_API_KEY>` |
|
| 87 |
+
| `Content-Type` | `application/json` |
|
| 88 |
+
| `X-Claw-Call-Type` | `normal`(正常请求)/ `retry`(失败重试) |
|
| 89 |
+
| `X-Claw-Skill-Id` | **`hithink-financial-query`** |
|
| 90 |
+
| `X-Claw-Skill-Version` | **`2.0.0`** |
|
| 91 |
+
| `X-Claw-Plugin-Id` | `none` |
|
| 92 |
+
| `X-Claw-Plugin-Version` | `none` |
|
| 93 |
+
| `X-Claw-Trace-Id` | 64 字符 hex(`secrets.token_hex(32)`) |
|
| 94 |
+
|
| 95 |
+
**请求体:**
|
| 96 |
+
```json
|
| 97 |
+
{
|
| 98 |
+
"query": "改写后的查询语句",
|
| 99 |
+
"page": "1",
|
| 100 |
+
"limit": "100",
|
| 101 |
+
"is_cache": "1",
|
| 102 |
+
"expand_index": "true"
|
| 103 |
+
}
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
**Python 调用示例:**
|
| 107 |
+
```python
|
| 108 |
+
import os, json, secrets, urllib.request
|
| 109 |
+
|
| 110 |
+
url = "https://openapi.iwencai.com/v1/query2data"
|
| 111 |
+
api_key = os.environ["IWENCAI_API_KEY"]
|
| 112 |
+
trace_id = secrets.token_hex(32)
|
| 113 |
+
|
| 114 |
+
payload = {
|
| 115 |
+
"query": "贵州茅台 最新价",
|
| 116 |
+
"page": "1", "limit": "100",
|
| 117 |
+
"is_cache": "1", "expand_index": "true",
|
| 118 |
+
}
|
| 119 |
+
headers = {
|
| 120 |
+
"Authorization": f"Bearer {api_key}",
|
| 121 |
+
"Content-Type": "application/json",
|
| 122 |
+
"X-Claw-Call-Type": "normal",
|
| 123 |
+
"X-Claw-Skill-Id": "hithink-financial-query",
|
| 124 |
+
"X-Claw-Skill-Version": "2.0.0",
|
| 125 |
+
"X-Claw-Plugin-Id": "none",
|
| 126 |
+
"X-Claw-Plugin-Version": "none",
|
| 127 |
+
"X-Claw-Trace-Id": trace_id,
|
| 128 |
+
}
|
| 129 |
+
req = urllib.request.Request(url, data=json.dumps(payload).encode("utf-8"),
|
| 130 |
+
headers=headers, method="POST")
|
| 131 |
+
resp = urllib.request.urlopen(req, timeout=30)
|
| 132 |
+
result = json.loads(resp.read().decode("utf-8"))
|
| 133 |
+
|
| 134 |
+
datas = result.get("datas", [])
|
| 135 |
+
code_count = result.get("code_count", 0)
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
### 步骤 4: 空数据处理
|
| 139 |
+
|
| 140 |
+
`datas` 为空时**最多重试 2 次**逐步放宽:
|
| 141 |
+
|
| 142 |
+
- 首次重试:去掉最苛刻的次要条件
|
| 143 |
+
- 二次重试:用更通用表述(如"贵州茅台"代替"600519.SH 贵州茅台")
|
| 144 |
+
|
| 145 |
+
每次重试把 `X-Claw-Call-Type` 改为 `retry`。
|
| 146 |
+
|
| 147 |
+
### 步骤 5: 数据解析
|
| 148 |
+
|
| 149 |
+
`datas` 是对象数组,列名随查询变化(中文 key,如"股票代码 / 股票简称 / 最新价 / 涨跌幅 / 市值 / PE / 主营行业"等)。`code_count` 是符合条件的总条数(> `len(datas)` 时需翻页)。
|
| 150 |
+
|
| 151 |
+
## 请求参数
|
| 152 |
+
|
| 153 |
+
| 参数名 | 类型 | 必填 | 说明 |
|
| 154 |
+
|--------|------|------|------|
|
| 155 |
+
| `query` | string | 是 | 用户/改写后的查询问句(中文自然语言) |
|
| 156 |
+
| `page` | string | 否 | 分页页码,默认 `1` |
|
| 157 |
+
| `limit` | string | 否 | 每页条数,默认 `100`(最高 500) |
|
| 158 |
+
| `is_cache` | string | 否 | 是否走缓存,默认 `1` |
|
| 159 |
+
| `expand_index` | string | 否 | 是否展开指数,默认 `true` |
|
| 160 |
+
|
| 161 |
+
## 响应参数
|
| 162 |
+
|
| 163 |
+
| 参数名 | 类型 | 说明 |
|
| 164 |
+
|--------|------|------|
|
| 165 |
+
| `datas` | array | 金融数据对象数组,列名随 query 变化 |
|
| 166 |
+
| `code_count` | int | 符合查询条件的总条数(可能 > `len(datas)`) |
|
| 167 |
+
| `chunks_info` | object | 查询字句信息(解析后的条件) |
|
| 168 |
+
| `status_code` | int | `0` = 成功;非 0 = 错误(见错误码) |
|
| 169 |
+
|
| 170 |
+
## CLI 使用
|
| 171 |
+
|
| 172 |
+
`scripts/cli.py` 提供跨平台命令行入口。
|
| 173 |
+
|
| 174 |
+
```bash
|
| 175 |
+
python3 scripts/cli.py --query "贵州茅台 最新价"
|
| 176 |
+
python3 scripts/cli.py --query "今日涨停 行业=科技" --page 1 --limit 50
|
| 177 |
+
python3 scripts/cli.py --query "银行 股息率前10" --timeout 60
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
**参数:**
|
| 181 |
+
- `--query` 必填
|
| 182 |
+
- `--page` 默认 1
|
| 183 |
+
- `--limit` 默认 10
|
| 184 |
+
- `--api-key` 可选(默认从 env 读)
|
| 185 |
+
- `--call-type` 默认 `normal`
|
| 186 |
+
- `--timeout` 默认 30
|
| 187 |
+
|
| 188 |
+
## 错误码
|
| 189 |
+
|
| 190 |
+
| 状态码 | 含义 | 建议 |
|
| 191 |
+
|---|---|---|
|
| 192 |
+
| `-2326 / -2126 / -1325` | 统计量过大或超时 | 缩小时间范围 / 减少指标 / 收窄标的 |
|
| 193 |
+
| `-2331 / -1330 / -2309` | 数据库查询超时 | 稍后重试 |
|
| 194 |
+
| `-2322 / -2321 / -1321` | 指标不存在 | 调整指标表达 |
|
| 195 |
+
| `-225` | 当前周期表无此指标 | 调整周期或口径 |
|
| 196 |
+
|
| 197 |
+
## 与其他 skill 的边界
|
| 198 |
+
|
| 199 |
+
| 问句 | 用 `financial-query` (本) | 用专门 skill |
|
| 200 |
+
|---|---|---|
|
| 201 |
+
| "贵州茅台最新价" | ✅ | — |
|
| 202 |
+
| "贵州茅台的 PE / 营收 / ROE" | ✅ | — |
|
| 203 |
+
| "中证 500 指数当前点位" | ✅ | — |
|
| 204 |
+
| "比亚迪上个月销量" | ✅ | — |
|
| 205 |
+
| "宁德时代 最近新闻全文" | ❌ | `news-search` |
|
| 206 |
+
| "宁德时代 最近公告" | ❌(除非只要摘要数字) | `announcement-search` |
|
| 207 |
+
| "宁德时代 最新研报" | ❌(除非只要研报评级汇总) | `report-search` |
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
**简单规则:**
|
| 211 |
+
- 问的是**"某个标的的某个数字/字段是多少"** → 本 skill
|
| 212 |
+
- 问的是**"符合某条件的所有标的 / 排名 / TopN / 选股"** → 专用选股 skill
|
| 213 |
+
- 问的是**"全文/原文/URL"** → 专门检索 skill
|
| 214 |
+
- 问的是**"执行操作"(模拟交易)** → `simulated-trading`
|
| 215 |
+
|
| 216 |
+
## 代码结构
|
| 217 |
+
|
| 218 |
+
```
|
| 219 |
+
financial-query/
|
| 220 |
+
├── SKILL.md # 本文件
|
| 221 |
+
├── LICENSE.txt # 许可证
|
| 222 |
+
└── scripts/
|
| 223 |
+
└── cli.py # CLI 入口(封装 8 个 Header + 重试 + 解析)
|
| 224 |
+
```
|
Skills/financial-query/scripts/cli.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
金融结构化数据查询 - 通用金融数据查询工具(financial-query)
|
| 4 |
+
|
| 5 |
+
通过同花顺问财 OpenAPI `query2data` 端点,查询 A 股 / 港股 / 美股 / 基金 /
|
| 6 |
+
期货 / ETF / 板块 / 概念 / 指数 等全市场金融结构化数据(行情、估值、财务、
|
| 7 |
+
事件、资金流向、宏观经济等)。
|
| 8 |
+
|
| 9 |
+
严格遵循 Iwencai (问财) OpenAPI 网关规范:
|
| 10 |
+
- 每次请求携带 8 个 X-Claw-* Header(X-Claw-Skill-Id 沿用平台注册名 `hithink-financial-query`)
|
| 11 |
+
- X-Claw-Trace-Id 为每次新生成的 64 字符十六进制唯一 ID
|
| 12 |
+
- Authorization Bearer 仅从环境变量 IWENCAI_API_KEY 读取
|
| 13 |
+
- 优先使用 POST
|
| 14 |
+
- 使用 Python3 标准库,跨平台兼容
|
| 15 |
+
|
| 16 |
+
注意:默认返回 10 条数据,可通过 --page 和 --limit 参数翻页获取更多数据
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import argparse
|
| 20 |
+
import json
|
| 21 |
+
import os
|
| 22 |
+
import secrets
|
| 23 |
+
import sys
|
| 24 |
+
import urllib.error
|
| 25 |
+
import urllib.request
|
| 26 |
+
from typing import Optional, Union
|
| 27 |
+
|
| 28 |
+
SKILL_NAME = "hithink-financial-query"
|
| 29 |
+
SKILL_VERSION = "2.0.0"
|
| 30 |
+
DEFAULT_API_URL = "https://openapi.iwencai.com/v1/query2data"
|
| 31 |
+
DEFAULT_PAGE = "1"
|
| 32 |
+
DEFAULT_LIMIT = "10"
|
| 33 |
+
DEFAULT_TIMEOUT = 30
|
| 34 |
+
|
| 35 |
+
class AStockAPIError(Exception):
|
| 36 |
+
"""API 错误异常类"""
|
| 37 |
+
def __init__(self, message: str, status_code: int = None, response: Union[str, dict, None] = None):
|
| 38 |
+
super().__init__(message)
|
| 39 |
+
self.message = message
|
| 40 |
+
self.status_code = status_code
|
| 41 |
+
self.response = response
|
| 42 |
+
|
| 43 |
+
def generate_trace_id() -> str:
|
| 44 |
+
"""生成 64 字符十六进制全局唯一追踪 ID。"""
|
| 45 |
+
return secrets.token_hex(32)
|
| 46 |
+
|
| 47 |
+
def get_api_key(cli_api_key: Optional[str]) -> str:
|
| 48 |
+
"""获取 API 密钥:优先 CLI 参数,其次环境变量。"""
|
| 49 |
+
key = cli_api_key or os.environ.get("IWENCAI_API_KEY", "")
|
| 50 |
+
if not key:
|
| 51 |
+
raise AStockAPIError(
|
| 52 |
+
"API 密钥未设置。请通过 --api-key 参数或环境变量 IWENCAI_API_KEY 指定。\n"
|
| 53 |
+
"首次使用获取指引:打开 https://www.iwencai.com/skillhub → 登录 → 点击 Skill → "
|
| 54 |
+
"安装方式-Agent用户-复制您的 IWENCAI_API_KEY。"
|
| 55 |
+
)
|
| 56 |
+
return key
|
| 57 |
+
|
| 58 |
+
def build_headers(api_key: str, trace_id: str, call_type: str = "normal") -> dict:
|
| 59 |
+
"""构造符合问财网关规范的请求头。"""
|
| 60 |
+
return {
|
| 61 |
+
"Authorization": f"Bearer {api_key}",
|
| 62 |
+
"Content-Type": "application/json",
|
| 63 |
+
"X-Claw-Call-Type": call_type,
|
| 64 |
+
"X-Claw-Skill-Id": SKILL_NAME,
|
| 65 |
+
"X-Claw-Skill-Version": SKILL_VERSION,
|
| 66 |
+
"X-Claw-Plugin-Id": "none",
|
| 67 |
+
"X-Claw-Plugin-Version": "none",
|
| 68 |
+
"X-Claw-Trace-Id": trace_id,
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
def query_astock(
|
| 72 |
+
query: str,
|
| 73 |
+
page: str,
|
| 74 |
+
limit: str,
|
| 75 |
+
api_key: Optional[str],
|
| 76 |
+
call_type: str = "normal",
|
| 77 |
+
timeout: int = DEFAULT_TIMEOUT,
|
| 78 |
+
) -> dict:
|
| 79 |
+
"""
|
| 80 |
+
调用数据查询接口。
|
| 81 |
+
"""
|
| 82 |
+
api_key = get_api_key(api_key)
|
| 83 |
+
api_url = DEFAULT_API_URL
|
| 84 |
+
trace_id = generate_trace_id()
|
| 85 |
+
|
| 86 |
+
payload = {
|
| 87 |
+
"query": query,
|
| 88 |
+
"page": page,
|
| 89 |
+
"limit": limit,
|
| 90 |
+
"is_cache": "1",
|
| 91 |
+
"expand_index": "true",
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
headers = build_headers(api_key, trace_id, call_type)
|
| 95 |
+
claw_headers = {k: v for k, v in headers.items() if k.startswith("X-Claw-")}
|
| 96 |
+
request = urllib.request.Request(
|
| 97 |
+
api_url,
|
| 98 |
+
data=json.dumps(payload).encode("utf-8"),
|
| 99 |
+
headers=headers,
|
| 100 |
+
method="POST",
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
| 105 |
+
response_body = response.read().decode("utf-8")
|
| 106 |
+
|
| 107 |
+
if not response_body.strip():
|
| 108 |
+
return {"text_response": "", "trace_id": trace_id, "claw_headers": claw_headers}
|
| 109 |
+
|
| 110 |
+
try:
|
| 111 |
+
parsed_response = json.loads(response_body)
|
| 112 |
+
if isinstance(parsed_response, dict):
|
| 113 |
+
parsed_response["trace_id"] = trace_id
|
| 114 |
+
parsed_response["claw_headers"] = claw_headers
|
| 115 |
+
return parsed_response
|
| 116 |
+
elif isinstance(parsed_response, list):
|
| 117 |
+
return {"data": parsed_response, "trace_id": trace_id, "claw_headers": claw_headers}
|
| 118 |
+
else:
|
| 119 |
+
return {"text_response": str(parsed_response), "trace_id": trace_id, "claw_headers": claw_headers}
|
| 120 |
+
except json.JSONDecodeError:
|
| 121 |
+
return {"text_response": response_body, "trace_id": trace_id, "claw_headers": claw_headers}
|
| 122 |
+
|
| 123 |
+
except urllib.error.HTTPError as e:
|
| 124 |
+
error_body = e.read().decode("utf-8") if e.fp else ""
|
| 125 |
+
|
| 126 |
+
if error_body.strip():
|
| 127 |
+
try:
|
| 128 |
+
error_json = json.loads(error_body)
|
| 129 |
+
raise AStockAPIError(
|
| 130 |
+
f"HTTP 错误 {e.code}: {e.reason}",
|
| 131 |
+
status_code=e.code,
|
| 132 |
+
response=error_json,
|
| 133 |
+
)
|
| 134 |
+
except json.JSONDecodeError:
|
| 135 |
+
raise AStockAPIError(
|
| 136 |
+
f"HTTP 错误 {e.code}: {e.reason}",
|
| 137 |
+
status_code=e.code,
|
| 138 |
+
response=error_body,
|
| 139 |
+
)
|
| 140 |
+
else:
|
| 141 |
+
raise AStockAPIError(
|
| 142 |
+
f"HTTP 错误 {e.code}: {e.reason}",
|
| 143 |
+
status_code=e.code,
|
| 144 |
+
response="",
|
| 145 |
+
)
|
| 146 |
+
except urllib.error.URLError as e:
|
| 147 |
+
raise AStockAPIError(f"网络错误: {e.reason}")
|
| 148 |
+
|
| 149 |
+
def parse_args():
|
| 150 |
+
"""解析命令行参数"""
|
| 151 |
+
parser = argparse.ArgumentParser(
|
| 152 |
+
description="金融结构化数据查询 - 通用金融数据查询工具(不包含选股/筛选)",
|
| 153 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
parser.add_argument(
|
| 157 |
+
"--query", "-q",
|
| 158 |
+
type=str,
|
| 159 |
+
required=True,
|
| 160 |
+
help="查询字符串(必填)"
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
parser.add_argument(
|
| 164 |
+
"--page",
|
| 165 |
+
type=str,
|
| 166 |
+
default=DEFAULT_PAGE,
|
| 167 |
+
help=f"分页参数,值必须为正整数(默认: {DEFAULT_PAGE})"
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
parser.add_argument(
|
| 171 |
+
"--limit",
|
| 172 |
+
type=str,
|
| 173 |
+
default=DEFAULT_LIMIT,
|
| 174 |
+
help=f"每页条数,值必须为正整数(默认: {DEFAULT_LIMIT})"
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
parser.add_argument(
|
| 178 |
+
"--api-key",
|
| 179 |
+
type=str,
|
| 180 |
+
default=None,
|
| 181 |
+
help="API 密钥(默认从环境变量 IWENCAI_API_KEY 读取)"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
parser.add_argument(
|
| 185 |
+
"--call-type",
|
| 186 |
+
type=str,
|
| 187 |
+
choices=["normal", "retry"],
|
| 188 |
+
default="normal",
|
| 189 |
+
help="调用类型: normal(正常请求)或 retry(重试请求)(默认: normal)"
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
parser.add_argument(
|
| 193 |
+
"--timeout",
|
| 194 |
+
type=int,
|
| 195 |
+
default=DEFAULT_TIMEOUT,
|
| 196 |
+
help=f"请求超时时间,单位秒(默认: {DEFAULT_TIMEOUT})"
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
args = parser.parse_args()
|
| 200 |
+
|
| 201 |
+
try:
|
| 202 |
+
page_val = int(args.page)
|
| 203 |
+
if page_val < 1:
|
| 204 |
+
parser.error(f"--page 必须为正整数,当前值: {args.page}")
|
| 205 |
+
except ValueError:
|
| 206 |
+
parser.error(f"--page 必须为正整数,当前值: {args.page}")
|
| 207 |
+
|
| 208 |
+
try:
|
| 209 |
+
limit_val = int(args.limit)
|
| 210 |
+
if limit_val < 1:
|
| 211 |
+
parser.error(f"--limit 必须为正整数,当前值: {args.limit}")
|
| 212 |
+
except ValueError:
|
| 213 |
+
parser.error(f"--limit 必须为正整数,当前值: {args.limit}")
|
| 214 |
+
|
| 215 |
+
return args
|
| 216 |
+
|
| 217 |
+
def main():
|
| 218 |
+
"""主函数"""
|
| 219 |
+
args = parse_args()
|
| 220 |
+
|
| 221 |
+
try:
|
| 222 |
+
result = query_astock(
|
| 223 |
+
query=args.query,
|
| 224 |
+
page=args.page,
|
| 225 |
+
limit=args.limit,
|
| 226 |
+
api_key=args.api_key,
|
| 227 |
+
call_type=args.call_type,
|
| 228 |
+
timeout=args.timeout,
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
if isinstance(result, dict) and "text_response" not in result:
|
| 232 |
+
if "datas" not in result:
|
| 233 |
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
| 234 |
+
sys.exit(1)
|
| 235 |
+
|
| 236 |
+
datas = result["datas"]
|
| 237 |
+
code_count = int(result.get("code_count", 0))
|
| 238 |
+
chunks_info = result.get("chunks_info", {})
|
| 239 |
+
trace_id = result.get("trace_id", "")
|
| 240 |
+
|
| 241 |
+
current_page = int(args.page)
|
| 242 |
+
current_limit = int(args.limit)
|
| 243 |
+
has_more = current_page * current_limit < code_count
|
| 244 |
+
|
| 245 |
+
output = {
|
| 246 |
+
"success": True,
|
| 247 |
+
"query": args.query,
|
| 248 |
+
"code_count": code_count,
|
| 249 |
+
"returned_count": len(datas),
|
| 250 |
+
"page": args.page,
|
| 251 |
+
"limit": args.limit,
|
| 252 |
+
"has_more": has_more,
|
| 253 |
+
"chunks_info": chunks_info,
|
| 254 |
+
"trace_id": trace_id,
|
| 255 |
+
"datas": datas,
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
if has_more:
|
| 259 |
+
output["pagination_tip"] = (
|
| 260 |
+
f"共查到 {code_count} 条记录,当前返回第 {args.page} 页的 {len(datas)} 条。"
|
| 261 |
+
f"如需更多数据,请使用 --page 参数翻页。"
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
if not datas:
|
| 265 |
+
output["empty_data_tip"] = (
|
| 266 |
+
"未查询到符合条件的数据。建议放宽或简化查询条件后重试"
|
| 267 |
+
"(使用 --call-type retry 标记重试请求)。"
|
| 268 |
+
"如仍无数据,可引导用户访问同花顺问财: https://www.iwencai.com/unifiedwap/chat"
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
print(json.dumps(output, ensure_ascii=False, indent=2))
|
| 272 |
+
else:
|
| 273 |
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
| 274 |
+
|
| 275 |
+
except AStockAPIError as e:
|
| 276 |
+
if isinstance(e.response, dict):
|
| 277 |
+
gateway_output = dict(e.response)
|
| 278 |
+
if e.status_code is not None:
|
| 279 |
+
gateway_output.setdefault("status_code", e.status_code)
|
| 280 |
+
print(json.dumps(gateway_output, ensure_ascii=False, indent=2))
|
| 281 |
+
elif isinstance(e.response, str) and e.response.strip():
|
| 282 |
+
print(e.response)
|
| 283 |
+
else:
|
| 284 |
+
error_output = {"error": e.message}
|
| 285 |
+
if e.status_code is not None:
|
| 286 |
+
error_output["status_code"] = e.status_code
|
| 287 |
+
print(json.dumps(error_output, ensure_ascii=False, indent=2))
|
| 288 |
+
sys.exit(1)
|
| 289 |
+
except KeyboardInterrupt:
|
| 290 |
+
print("\n操作已取消。", file=sys.stderr)
|
| 291 |
+
sys.exit(130)
|
| 292 |
+
except Exception as e:
|
| 293 |
+
error_output = {
|
| 294 |
+
"error": f"发生错误: {str(e)}"
|
| 295 |
+
}
|
| 296 |
+
print(json.dumps(error_output, ensure_ascii=False, indent=2))
|
| 297 |
+
sys.exit(1)
|
| 298 |
+
|
| 299 |
+
if __name__ == "__main__":
|
| 300 |
+
main()
|
Skills/news-search/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 新闻搜索技能
|
| 2 |
+
|
| 3 |
+
财经领域资讯搜索引擎,调用同花顺问财的财经资讯搜索接口。
|
| 4 |
+
|
| 5 |
+
## 功能特点
|
| 6 |
+
|
| 7 |
+
- **财经资讯搜索**: 搜索各类财经新闻和资讯
|
| 8 |
+
- **多关键词处理**: 自动拆解复杂查询为多个简单查询
|
| 9 |
+
- **结果过滤**: 按时间、相关性等条件过滤结果
|
| 10 |
+
- **数据导出**: 支持将搜索结果导出为CSV、JSON等格式
|
| 11 |
+
- **批量处理**: 支持从文件读取多个查询并批量处理
|
| 12 |
+
- **错误处理**: 完善的错误处理和重试机制
|
| 13 |
+
- **日志记录**: 详细的运行日志和调试信息
|
| 14 |
+
|
| 15 |
+
## 安装要求
|
| 16 |
+
|
| 17 |
+
- Python 3.7+
|
| 18 |
+
- requests库
|
| 19 |
+
|
| 20 |
+
## 快速开始
|
| 21 |
+
|
| 22 |
+
### 1. 设置API密钥
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
# 设置环境变量
|
| 26 |
+
export IWENCAI_API_KEY="your_api_key_here"
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
### 2. 基本使用
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
# 搜索财经新闻
|
| 33 |
+
python news_search.py -q "人工智能"
|
| 34 |
+
|
| 35 |
+
# 搜索最近7天的新闻
|
| 36 |
+
python news_search.py -q "芯片行业" -d 7
|
| 37 |
+
|
| 38 |
+
# 限制返回结果数量
|
| 39 |
+
python news_search.py -q "新能源汽车" -l 5
|
| 40 |
+
|
| 41 |
+
# 导出为CSV格式
|
| 42 |
+
python news_search.py -q "人工智能" -o results.csv -f csv
|
| 43 |
+
|
| 44 |
+
# 导出为JSON格式
|
| 45 |
+
python news_search.py -q "人工智能" -o results.json -f json
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### 3. 批量处理
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
# 创建查询文件
|
| 52 |
+
echo "人工智能" > queries.txt
|
| 53 |
+
echo "芯片行业" >> queries.txt
|
| 54 |
+
echo "新能源汽车" >> queries.txt
|
| 55 |
+
|
| 56 |
+
# 批量处理并导出到目录
|
| 57 |
+
python news_search.py -i queries.txt -o output/ -f csv
|
| 58 |
+
|
| 59 |
+
# 批量处理并合并到一个文件
|
| 60 |
+
python news_search.py -i queries.txt -o all_results.csv -f csv
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
### 4. 管道操作
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
# 从管道读取查询
|
| 67 |
+
echo "人工智能" | python news_search.py
|
| 68 |
+
|
| 69 |
+
# 从文件读取并管道处理
|
| 70 |
+
cat queries.txt | xargs -I {} python news_search.py -q "{}"
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
## 命令行参数
|
| 74 |
+
|
| 75 |
+
```
|
| 76 |
+
usage: news_search.py [-h] (-q QUERY | -i INPUT) [-o OUTPUT] [-f {csv,json,text}] [-l LIMIT] [-d DAYS] [--api-key API_KEY] [--debug]
|
| 77 |
+
|
| 78 |
+
财经新闻搜索工具 - 调用同花顺问财的财经资讯搜索接口
|
| 79 |
+
|
| 80 |
+
optional arguments:
|
| 81 |
+
-h, --help 显示帮助信息
|
| 82 |
+
-q QUERY, --query QUERY
|
| 83 |
+
搜索关键词,支持中文
|
| 84 |
+
-i INPUT, --input INPUT
|
| 85 |
+
输入文件路径,每行一个查询词(批量处理)
|
| 86 |
+
-o OUTPUT, --output OUTPUT
|
| 87 |
+
输出文件路径或目录
|
| 88 |
+
-f {csv,json,text}, --format {csv,json,text}
|
| 89 |
+
输出格式 (默认: text)
|
| 90 |
+
-l LIMIT, --limit LIMIT
|
| 91 |
+
每查询返回的最大文章数量 (默认: 10)
|
| 92 |
+
-d DAYS, --days DAYS 搜索最近多少天内的文章 (默认: 30)
|
| 93 |
+
--api-key API_KEY API密钥,如果不提供则从环境变量 IWENCAI_API_KEY 获取
|
| 94 |
+
--debug 启用调试模式
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
## 配置说明
|
| 98 |
+
|
| 99 |
+
### 环境变量
|
| 100 |
+
|
| 101 |
+
- `IWENCAI_API_KEY`: API密钥(必需)
|
| 102 |
+
- `NEWS_SEARCH_DEFAULT_LIMIT`: 默认返回结果数量
|
| 103 |
+
- `NEWS_SEARCH_DEFAULT_DAYS`: 默认搜索天数
|
| 104 |
+
- `NEWS_SEARCH_LOG_LEVEL`: 日志级别(DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
| 105 |
+
|
| 106 |
+
### 配置文件
|
| 107 |
+
|
| 108 |
+
技能支持JSON格式的配置文件,可以创建 `config.json` 文件:
|
| 109 |
+
|
| 110 |
+
```json
|
| 111 |
+
{
|
| 112 |
+
"api": {
|
| 113 |
+
"base_url": "https://openapi.iwencai.com",
|
| 114 |
+
"endpoint": "/v1/comprehensive/search",
|
| 115 |
+
"timeout": 30,
|
| 116 |
+
"max_retries": 3
|
| 117 |
+
},
|
| 118 |
+
"search": {
|
| 119 |
+
"default_limit": 10,
|
| 120 |
+
"default_days": 30
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
## 代码示例
|
| 126 |
+
|
| 127 |
+
### Python API调用
|
| 128 |
+
|
| 129 |
+
```python
|
| 130 |
+
from news_search import NewsSearchAPI, NewsProcessor
|
| 131 |
+
|
| 132 |
+
# 初始化API客户端
|
| 133 |
+
api = NewsSearchAPI()
|
| 134 |
+
|
| 135 |
+
# 搜索新闻
|
| 136 |
+
articles = api.search("人工智能")
|
| 137 |
+
|
| 138 |
+
# 处理结果
|
| 139 |
+
processor = NewsProcessor()
|
| 140 |
+
filtered_articles = processor.filter_by_date(articles, days=7)
|
| 141 |
+
sorted_articles = processor.sort_by_date(filtered_articles)
|
| 142 |
+
|
| 143 |
+
# 保存结果
|
| 144 |
+
processor.save_to_csv(sorted_articles, "results.csv")
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
### 查询拆解示例
|
| 148 |
+
|
| 149 |
+
```python
|
| 150 |
+
from news_search import QueryProcessor
|
| 151 |
+
|
| 152 |
+
# 复杂查询拆解
|
| 153 |
+
query = "人工智能和芯片行业最新动态"
|
| 154 |
+
sub_queries = QueryProcessor.split_complex_query(query)
|
| 155 |
+
# 返回: ["人工智能最新动态", "芯片行业最新动态"]
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
## 错误处理
|
| 159 |
+
|
| 160 |
+
技能包含完善的错误处理机制:
|
| 161 |
+
|
| 162 |
+
1. **网络异常**: 自动重试机制
|
| 163 |
+
2. **API认证失败**: 清晰的错误提示
|
| 164 |
+
3. **请求频率限制**: 指数退避重试
|
| 165 |
+
4. **数据解析错误**: 优雅降级处理
|
| 166 |
+
|
| 167 |
+
## 输出格式
|
| 168 |
+
|
| 169 |
+
### CSV格式
|
| 170 |
+
```
|
| 171 |
+
title,summary,url,publish_date,source
|
| 172 |
+
文章标题,文章摘要,文章网址,发布时间,同花顺问财
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
### JSON格式
|
| 176 |
+
```json
|
| 177 |
+
[
|
| 178 |
+
{
|
| 179 |
+
"title": "文章标题",
|
| 180 |
+
"summary": "文章摘要",
|
| 181 |
+
"url": "文章网址",
|
| 182 |
+
"publish_date": "发布时间",
|
| 183 |
+
"source": "同花顺问财"
|
| 184 |
+
}
|
| 185 |
+
]
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
+
### 文本格式
|
| 189 |
+
```
|
| 190 |
+
============================================================
|
| 191 |
+
查询: 人工智能
|
| 192 |
+
找到 8 篇文章 (最近 30 天)
|
| 193 |
+
============================================================
|
| 194 |
+
|
| 195 |
+
1. 人工智能助力金融行业数字化转型
|
| 196 |
+
摘要: 近日,多家金融机构宣布采用人工智能技术优化风��系统...
|
| 197 |
+
发布时间: 2024-01-15 10:30:00
|
| 198 |
+
链接: https://example.com/article/123
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
## 使用场景
|
| 202 |
+
|
| 203 |
+
### 财经新闻搜索
|
| 204 |
+
```bash
|
| 205 |
+
python news_search.py -q "央行货币政策"
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
### 行业趋势分析
|
| 209 |
+
```bash
|
| 210 |
+
python news_search.py -q "人工智能行业发展趋势" -d 90 -l 20
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
### 企业信息查询
|
| 214 |
+
```bash
|
| 215 |
+
python news_search.py -q "腾讯公司最新动态" -o tencent_news.csv -f csv
|
| 216 |
+
```
|
| 217 |
+
|
| 218 |
+
### 批量数据收集
|
| 219 |
+
```bash
|
| 220 |
+
# 收集多个行业的新闻
|
| 221 |
+
echo "人工智能" > industries.txt
|
| 222 |
+
echo "新能源汽车" >> industries.txt
|
| 223 |
+
echo "生物医药" >> industries.txt
|
| 224 |
+
python news_search.py -i industries.txt -o industry_news/ -f json
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
## 注意事项
|
| 228 |
+
|
| 229 |
+
1. **API密钥安全**: API密钥应从环境变量获取,不要硬编码在代码中
|
| 230 |
+
2. **请求频率限制**: 注意API提供商的请求频率限制
|
| 231 |
+
3. **商业用途**: 不得将数据用于商业用途或违反相关法律法规
|
| 232 |
+
|
| 233 |
+
## 故障排除
|
| 234 |
+
|
| 235 |
+
### 常见问题
|
| 236 |
+
|
| 237 |
+
1. **API认证失败**
|
| 238 |
+
```
|
| 239 |
+
错误: API认证失败,请检查API密钥
|
| 240 |
+
解决方案: 检查 IWENCAI_API_KEY 环境变量是否正确设置
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
2. **网络连接问题**
|
| 244 |
+
```
|
| 245 |
+
错误: 连接错误
|
| 246 |
+
解决方案: 检查网络连接,或使用 --debug 参数查看详细错误
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
3. **无结果返回**
|
| 250 |
+
```
|
| 251 |
+
未找到相关文章
|
| 252 |
+
解决方案: 尝试调整查询关键词,或增加搜索天数 (-d 参数)
|
| 253 |
+
```
|
| 254 |
+
|
| 255 |
+
### 调试模式
|
| 256 |
+
|
| 257 |
+
使用 `--debug` 参数启用详细日志:
|
| 258 |
+
|
| 259 |
+
```bash
|
| 260 |
+
python news_search.py -q "测试" --debug
|
| 261 |
+
```
|
| 262 |
+
|
| 263 |
+
## 更新日志
|
| 264 |
+
|
| 265 |
+
### v1.0.0
|
| 266 |
+
- 实现基础财经资讯搜索功能
|
| 267 |
+
- 支持查询拆解和合并
|
| 268 |
+
- 实现完整错误处理机制
|
| 269 |
+
- 提供详细的使用文档
|
| 270 |
+
|
| 271 |
+
## 许可证
|
| 272 |
+
|
| 273 |
+
MIT License
|
| 274 |
+
|
| 275 |
+
## 技术支持
|
| 276 |
+
|
| 277 |
+
如有问题或建议,请参考代码注释或联系开发者。
|
Skills/news-search/SKILL.md
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: news-search
|
| 3 |
+
description: 财经领域为主的资讯搜索引擎,囊获了各类型媒体:官媒、主流财经媒体、垂直行业网站、知名上市公司/非上市公司官网等,可以帮助你了解最新财经事件、政策动态、行业革新、企业业务进展等等。
|
| 4 |
+
version: 1.0.0
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# 新闻搜索技能
|
| 8 |
+
|
| 9 |
+
## 版本
|
| 10 |
+
当前技能版本:1.0.0(与X-Claw-Skill-Version头一致)
|
| 11 |
+
|
| 12 |
+
## 技能概述
|
| 13 |
+
|
| 14 |
+
本技能是一个财经领域为主的资讯搜索引擎,通过调用同花顺问财的财经资讯搜索接口,帮助用户获取最新的财经新闻、政策动态、行业革新和企业业务进展等信息。本技能严格遵守问财OpenAPI网关规范。
|
| 15 |
+
|
| 16 |
+
## 首次使用 - 获取 API Key
|
| 17 |
+
|
| 18 |
+
所有技能都需要 IWENCAI_API_KEY 环境变量才能使用。 如果用户尚未配置,按以下步骤引导:
|
| 19 |
+
|
| 20 |
+
步骤 1:获取 API Key
|
| 21 |
+
在浏览器内打同花顺i问财SkillHub页面:https://www.iwencai.com/skillhub
|
| 22 |
+
|
| 23 |
+
步骤 2:登录
|
| 24 |
+
|
| 25 |
+
步骤 3:点击具体的Skill,打开弹窗查看详情,在安装方式-Agent用户-找到您的IWENCAI_API_KEY这一段,复制
|
| 26 |
+
|
| 27 |
+
步骤 4:配置环境变量
|
| 28 |
+
获取到 API Key 后,直接复制指引文字发送给AI助手,或手动设置环境变量:
|
| 29 |
+
|
| 30 |
+
**Unix/Linux/macOS (bash/zsh):**
|
| 31 |
+
```bash
|
| 32 |
+
export IWENCAI_API_KEY="your_api_key_here"
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
**Windows (PowerShell):**
|
| 36 |
+
```powershell
|
| 37 |
+
$env:IWENCAI_API_KEY="your_api_key_here"
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
**Windows (CMD):**
|
| 41 |
+
```cmd
|
| 42 |
+
set IWENCAI_API_KEY=your_api_key_here
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
## 技能功能
|
| 46 |
+
|
| 47 |
+
### 1. 财经资讯搜索
|
| 48 |
+
- 搜索各类财经新闻和资讯
|
| 49 |
+
- 覆盖官媒、主流财经媒体、垂直行业网站、知名上市公司/非上市公司官网等
|
| 50 |
+
- 支持中文关键词搜索
|
| 51 |
+
|
| 52 |
+
### 2. 查询处理能力
|
| 53 |
+
- 自动拆解复杂查询为多个简单查询
|
| 54 |
+
- 示例:用户问"最近人工智能和芯片行业有什么新闻?"可以拆分为"人工智能最新动态"和"芯片行业新闻"两个查询
|
| 55 |
+
- 根据查询复杂度决定调用接口的次数
|
| 56 |
+
|
| 57 |
+
### 3. 数据评估与扩展
|
| 58 |
+
- 自动评估搜索结果是否能回答用户问题
|
| 59 |
+
- 如有必要,可调用其他技能或工具扩展数据
|
| 60 |
+
- 对搜索结果进行质量评估
|
| 61 |
+
|
| 62 |
+
### 4. 数据处理与返回
|
| 63 |
+
- 对搜索结果进行排序、过滤和摘要处理
|
| 64 |
+
- **⚠️ 重要警告:根据问财OpenAPI网关规范条件六,API原始响应必须透明传递**
|
| 65 |
+
- **必须遵守**:不得对API响应进行二次解析、清洗、重组或再加工
|
| 66 |
+
- **透明传递要求**:
|
| 67 |
+
- 直接返回API原始响应JSON,不做任何包装
|
| 68 |
+
- 错误响应也必须原样传递,不得替换为自定义错误信息
|
| 69 |
+
- 网络层错误(超时、连接失败等)可提供技术性错误信息
|
| 70 |
+
- 将透明传递的响应数据返回给大模型进行处理
|
| 71 |
+
- 大模型负责生成用户友好的回答格式
|
| 72 |
+
|
| 73 |
+
## 接口信息
|
| 74 |
+
|
| 75 |
+
### 基础信息
|
| 76 |
+
- **Base URL**: `https://openapi.iwencai.com`
|
| 77 |
+
- **接口路径**: `/v1/comprehensive/search`
|
| 78 |
+
- **请求方式**: POST
|
| 79 |
+
- **认证方式**: API Key (Bearer Token)
|
| 80 |
+
|
| 81 |
+
### 问财OpenAPI网关规范要求
|
| 82 |
+
|
| 83 |
+
所有发往问财OpenAPI网关的请求必须遵守以下规范:
|
| 84 |
+
|
| 85 |
+
#### 1. HTTP请求头要求
|
| 86 |
+
所有请求必须在Header中包含以下字段:
|
| 87 |
+
|
| 88 |
+
| Header | 取值说明 |
|
| 89 |
+
|--------|----------|
|
| 90 |
+
| `X-Claw-Call-Type` | `normal`:正常请求;`retry`:失败后的重试。按实际调用场景二选一。 |
|
| 91 |
+
| `X-Claw-Skill-Id` | 技能标识,填写 `news-search`。 |
|
| 92 |
+
| `X-Claw-Skill-Version` | 当前技能版本号,填写 `1.0.0`。 |
|
| 93 |
+
| `X-Claw-Plugin-Id` | 插件ID,当前阶段统一填写 `none`。 |
|
| 94 |
+
| `X-Claw-Plugin-Version` | 插件版本,当前阶段统一填写 `none`。 |
|
| 95 |
+
| `X-Claw-Trace-Id` | **每次请求必须新生成**的**全局唯一**追踪ID;**长度为64个字符**(推荐64位十六进制字符串)。 |
|
| 96 |
+
|
| 97 |
+
#### 2. 认证要求
|
| 98 |
+
使用OAuth2.0/JWT风格认证:
|
| 99 |
+
```
|
| 100 |
+
Authorization: Bearer {IWENCAI_API_KEY}
|
| 101 |
+
```
|
| 102 |
+
其中 `IWENCAI_API_KEY` 必须从环境变量读取,禁止硬编码在代码中。
|
| 103 |
+
|
| 104 |
+
#### 3. 请求参数
|
| 105 |
+
```json
|
| 106 |
+
{
|
| 107 |
+
"channels": ["news"],
|
| 108 |
+
"app_id": "AIME_SKILL",
|
| 109 |
+
"query": "搜索关键词"
|
| 110 |
+
}
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
### curl示例(脱敏)
|
| 114 |
+
```bash
|
| 115 |
+
# 生成64位十六进制Trace ID
|
| 116 |
+
TRACE_ID=$(python3 -c "import secrets; print(secrets.token_hex(32))")
|
| 117 |
+
|
| 118 |
+
# 调用新闻搜索接口
|
| 119 |
+
curl -X POST "https://openapi.iwencai.com/v1/comprehensive/search" \
|
| 120 |
+
-H "Content-Type: application/json" \
|
| 121 |
+
-H "Authorization: Bearer $IWENCAI_API_KEY" \
|
| 122 |
+
-H "X-Claw-Call-Type: normal" \
|
| 123 |
+
-H "X-Claw-Skill-Id: news-search" \
|
| 124 |
+
-H "X-Claw-Skill-Version: 1.0.0" \
|
| 125 |
+
-H "X-Claw-Plugin-Id: none" \
|
| 126 |
+
-H "X-Claw-Plugin-Version: none" \
|
| 127 |
+
-H "X-Claw-Trace-Id: $TRACE_ID" \
|
| 128 |
+
-d '{
|
| 129 |
+
"channels": ["news"],
|
| 130 |
+
"app_id": "AIME_SKILL",
|
| 131 |
+
"query": "人工智能"
|
| 132 |
+
}'
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
**Windows PowerShell示例:**
|
| 136 |
+
```powershell
|
| 137 |
+
# 生成64位十六进制Trace ID
|
| 138 |
+
$TRACE_ID = python -c "import secrets; print(secrets.token_hex(32))"
|
| 139 |
+
|
| 140 |
+
# 调用新闻搜索接口
|
| 141 |
+
$headers = @{
|
| 142 |
+
"Content-Type" = "application/json"
|
| 143 |
+
"Authorization" = "Bearer $env:IWENCAI_API_KEY"
|
| 144 |
+
"X-Claw-Call-Type" = "normal"
|
| 145 |
+
"X-Claw-Skill-Id" = "news-search"
|
| 146 |
+
"X-Claw-Skill-Version" = "1.0.0"
|
| 147 |
+
"X-Claw-Plugin-Id" = "none"
|
| 148 |
+
"X-Claw-Plugin-Version" = "none"
|
| 149 |
+
"X-Claw-Trace-Id" = $TRACE_ID
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
$body = @{
|
| 153 |
+
channels = @("news")
|
| 154 |
+
app_id = "AIME_SKILL"
|
| 155 |
+
query = "人工智能"
|
| 156 |
+
} | ConvertTo-Json
|
| 157 |
+
|
| 158 |
+
Invoke-RestMethod -Uri "https://openapi.iwencai.com/v1/comprehensive/search" -Method Post -Headers $headers -Body $body
|
| 159 |
+
```
|
| 160 |
+
|
| 161 |
+
## 问财OpenAPI网关核心规范(必须遵守)
|
| 162 |
+
|
| 163 |
+
### 条件六:返回结果透明传递(Non-Negotiable)
|
| 164 |
+
|
| 165 |
+
**核心原则:技能生成的代码必须透明传递API响应,不得对返回内容做任何修改、过滤、重组或再加工后再交付给调用方。**
|
| 166 |
+
|
| 167 |
+
#### 1. 禁止行为
|
| 168 |
+
- 不得对网关返回的 `data`、`result`、`response` 等字段进行二次解析、清洗、重组
|
| 169 |
+
- 不得自行添加、删除、修改返回结果的任何键值或结构
|
| 170 |
+
- 不得在技能生成的代码中将API原始响应包装成另一套 `result` / `output` / `data` 等结构再返回
|
| 171 |
+
- 不得在返回前对响应内容做任何「业务逻辑层」的处理(如字段映射、类型转换、格式化等),这些应由调用方决定如何处理
|
| 172 |
+
|
| 173 |
+
#### 2. 要求行为
|
| 174 |
+
- **直接透传**:对网关返回的完整HTTP响应体(Body),应在获取后**原封不动**地传递给调用方(或返回给LLM)
|
| 175 |
+
- **透明返回**:若使用Python等语言实现,返回值应为对API响应的直接赋值或简单的 `return response`,不做任何中间transformation
|
| 176 |
+
- **错误传递**:API返回的错误状态码与错误Body也应完整传递,不得替换为自定义错误信息(除非是网络层超时、连接失败等技术性错误)
|
| 177 |
+
|
| 178 |
+
#### 3. 正确实现示例
|
| 179 |
+
```python
|
| 180 |
+
# ✅ 正确:直接返回API响应
|
| 181 |
+
def search_news(query: str):
|
| 182 |
+
response = requests.post(url, headers=headers, json=payload)
|
| 183 |
+
# 直接返回API响应,不做任何处理
|
| 184 |
+
return response.json() # 或者 response.text / response.content
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
#### 4. 错误实现示例
|
| 188 |
+
```python
|
| 189 |
+
# ❌ 错误:对API响应做了二次组装
|
| 190 |
+
def search_news(query: str):
|
| 191 |
+
resp = requests.post(url, headers=headers, json=payload)
|
| 192 |
+
data = resp.json()
|
| 193 |
+
result = {"code": 0, "data": data["result"], "msg": "success"} # 禁止:自行包装
|
| 194 |
+
return result
|
| 195 |
+
```
|
| 196 |
+
|
| 197 |
+
## 使用场景
|
| 198 |
+
|
| 199 |
+
### 何时调用本技能
|
| 200 |
+
1. **财经新闻搜索**: 当用户需要了解特定行业、公司或主题的最新财经新闻时
|
| 201 |
+
2. **政策动态查询**: 当用户需要了解最新的财经政策、法规变化时
|
| 202 |
+
3. **行业趋势分析**: 当用户需要了解某个行业的最新发展动态和趋势时
|
| 203 |
+
4. **企业信息查询**: 当用户需要了解特定上市公司或非上市公司的业务进展时
|
| 204 |
+
5. **市场动态跟踪**: 当用户需要了解股票市场、债券市场等金融市场的最新动态时
|
| 205 |
+
|
| 206 |
+
### 调用示例
|
| 207 |
+
1. 用户问:"最近人工智能行业有什么新政策?"
|
| 208 |
+
2. 用户问:"特斯拉最近的业务进展如何?"
|
| 209 |
+
3. 用户问:"芯片行业的最新动态有哪些?"
|
| 210 |
+
4. 用户问:"央行最近发布了什么货币政策?"
|
| 211 |
+
|
| 212 |
+
## 技能内部逻辑
|
| 213 |
+
|
| 214 |
+
### 查询处理流程
|
| 215 |
+
1. **接收用户查询**: 获取用户的搜索需求
|
| 216 |
+
2. **查询拆解**: 分析查询复杂度,决定是否需要拆分为多个子查询
|
| 217 |
+
3. **API调用**: 生成并执行API调用代码
|
| 218 |
+
4. **数据评估**: 检查返回的数据是否足够回答用户问题
|
| 219 |
+
5. **数据处理**: 对搜索结果进行排序、过滤和摘要
|
| 220 |
+
6. **结果返回**: 将处理后的结果返回给大模型
|
| 221 |
+
|
| 222 |
+
### 代码生成要求
|
| 223 |
+
- 生成完整的API调用代码,包括认证、请求构造、错误处理
|
| 224 |
+
- 处理网络异常和接口错误
|
| 225 |
+
- 实现重试机制
|
| 226 |
+
- 支持并发处理(可选)
|
| 227 |
+
|
| 228 |
+
## 技术实现
|
| 229 |
+
|
| 230 |
+
### Python代码要求
|
| 231 |
+
- 使用Python标准库和常用库
|
| 232 |
+
- 代码结构清晰,模块化设计
|
| 233 |
+
- 包含完整的错误处理
|
| 234 |
+
- 支持配置文件和环境变量
|
| 235 |
+
- 实现日志记录功能
|
| 236 |
+
- 尽量少依赖第三方库
|
| 237 |
+
- 确保代码的可读性和可维护性
|
| 238 |
+
|
| 239 |
+
### CLI接口要求
|
| 240 |
+
- 提供友好的命令行接口支持
|
| 241 |
+
- 支持以下命令行参数:
|
| 242 |
+
- `--query` 或 `-q`: 搜索关键词
|
| 243 |
+
- `--output` 或 `-o`: 输出文件路径
|
| 244 |
+
- `--input` 或 `-i`: 输入文件路径(批量处理)
|
| 245 |
+
- `--format` 或 `-f`: 输出格式(csv, json, text)
|
| 246 |
+
- `--limit` 或 `-l`: 结果数量限制
|
| 247 |
+
- `--help` 或 `-h`: 显示帮助信息
|
| 248 |
+
- 提供详细的帮助文档和示例用法
|
| 249 |
+
- 支持管道操作和输入输出重定向
|
| 250 |
+
- 支持文件到文件处理,即CLI允许 `input-paths` 和 `output-paths`
|
| 251 |
+
- 数据表格保存为CSV格式
|
| 252 |
+
- 图片URL下载为文件
|
| 253 |
+
- 支持批量处理和分页查询
|
| 254 |
+
|
| 255 |
+
### 大数据处理能力
|
| 256 |
+
- 探索api接口的输出,考虑数据量较大的情况
|
| 257 |
+
- 支持文件到文件处理
|
| 258 |
+
- 数据表格保存为CSV格式
|
| 259 |
+
- 图片URL下载为文件
|
| 260 |
+
- 支持批量处理和分页查询
|
| 261 |
+
|
| 262 |
+
### 错误处理
|
| 263 |
+
- 网络异常处理
|
| 264 |
+
- API���证失败处理
|
| 265 |
+
- 请求频率限制处理
|
| 266 |
+
- 数据解析错误处理
|
| 267 |
+
|
| 268 |
+
### 性能优化
|
| 269 |
+
- 支持大数据量处理
|
| 270 |
+
- 实现缓存机制(可选)
|
| 271 |
+
- 支持并发查询(可选)
|
| 272 |
+
- 优化响应时间
|
| 273 |
+
|
| 274 |
+
## 注意事项
|
| 275 |
+
|
| 276 |
+
### API使用规范
|
| 277 |
+
1. API密钥需要从环境变量安全获取:`IWENCAI_API_KEY`
|
| 278 |
+
2. 注意请求频率限制,避免被限制访问
|
| 279 |
+
3. 请求参数中的`channels`和`app_id`为固定值,不要修改
|
| 280 |
+
4. `query`参数支持中文关键词搜索
|
| 281 |
+
|
| 282 |
+
### 数据使用规范
|
| 283 |
+
1. 确保数据处理的准确性和完整性
|
| 284 |
+
2. 遵循数据隐私和安全规范
|
| 285 |
+
3. 不得将数据用于商业用途或违反相关法律法规
|
| 286 |
+
|
| 287 |
+
### 技能调用规范
|
| 288 |
+
1. 技能描述使用中文
|
| 289 |
+
2. 提供清晰的调用说明
|
| 290 |
+
3. 支持多种查询场景
|
| 291 |
+
4. 确保技能稳定性和可靠性
|
| 292 |
+
|
| 293 |
+
## 技能验证
|
| 294 |
+
|
| 295 |
+
### 验收标准
|
| 296 |
+
1. 技能能够正确安装和运行
|
| 297 |
+
2. 能够成功调用财经资讯搜索接口
|
| 298 |
+
3. 能够处理各种查询场景
|
| 299 |
+
4. 支持大数据量的处理和导出
|
| 300 |
+
5. 代码质量高,符合Python最佳实践
|
| 301 |
+
6. 技能描述使用中文
|
| 302 |
+
|
| 303 |
+
### 测试用例
|
| 304 |
+
1. 简单查询测试:搜索"人工智能"
|
| 305 |
+
2. 复杂查询测试:搜索"人工智能和芯片行业最新动态"
|
| 306 |
+
3. 错误处理测试:无效API密钥测试
|
| 307 |
+
4. 性能测试:大数据量查询测试
|
| 308 |
+
5. 格式测试:不同输出格式测试
|
| 309 |
+
|
| 310 |
+
## 更新日志
|
| 311 |
+
|
| 312 |
+
### v1.0.0 (初始版本)
|
| 313 |
+
- 实现基础财经资讯搜索功能
|
| 314 |
+
- 支持查询拆解和合并
|
| 315 |
+
- 实现完整错误处理机制
|
| 316 |
+
- 提供详细的使用文档
|
Skills/news-search/references/api.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 财经资讯搜索接口文档
|
| 2 |
+
|
| 3 |
+
## 接口概述
|
| 4 |
+
- **接口名称**: 财经资讯搜索接口
|
| 5 |
+
- **接口说明**: 财经领域为主的资讯搜索引擎,囊获了各类型媒体:官媒、主流财经媒体、垂直行业网站、知名上市公司/非上市公司官网等,可以帮助你了解最新财经事件、政策动态、行业革新、企业业务进展等
|
| 6 |
+
|
| 7 |
+
## 基础信息
|
| 8 |
+
- **Base URL**: `https://openapi.iwencai.com`
|
| 9 |
+
- **接口路径**: `/v1/comprehensive/search`
|
| 10 |
+
- **请求方式**: POST
|
| 11 |
+
- **认证方式**: API Key (Bearer Token)
|
| 12 |
+
|
| 13 |
+
## 认证要求
|
| 14 |
+
在请求头中需要携带API Key进行认证:
|
| 15 |
+
```
|
| 16 |
+
Authorization: Bearer {IWENCAI_API_KEY}
|
| 17 |
+
```
|
| 18 |
+
其中 `IWENCAI_API_KEY` 是用户申请的有效API密钥,需要设置为环境变量。
|
| 19 |
+
|
| 20 |
+
## 请求头
|
| 21 |
+
```
|
| 22 |
+
Content-Type: application/json
|
| 23 |
+
Authorization: Bearer {IWENCAI_API_KEY}
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
## 请求参数
|
| 27 |
+
|
| 28 |
+
### 固定参数
|
| 29 |
+
| 参数名 | 类型 | 说明 | 值 |
|
| 30 |
+
|--------|------|------|-----|
|
| 31 |
+
| channels | LIST | 搜索渠道类型 | `["news"]` |
|
| 32 |
+
| app_id | STRING | 应用ID | `AIME_SKILL` |
|
| 33 |
+
|
| 34 |
+
### 可变参数
|
| 35 |
+
| 参数名 | 类型 | 说明 | 必填 |
|
| 36 |
+
|--------|------|------|------|
|
| 37 |
+
| query | STRING | 用户问句,即搜索关键词 | 是 |
|
| 38 |
+
|
| 39 |
+
### 请求示例
|
| 40 |
+
```json
|
| 41 |
+
{
|
| 42 |
+
"channels": ["news"],
|
| 43 |
+
"app_id": "AIME_SKILL",
|
| 44 |
+
"query": "人工智能行业最新动态"
|
| 45 |
+
}
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
## 响应参数
|
| 49 |
+
|
| 50 |
+
### 响应结构
|
| 51 |
+
```json
|
| 52 |
+
{
|
| 53 |
+
"data": [
|
| 54 |
+
{
|
| 55 |
+
"title": "文章标题",
|
| 56 |
+
"summary": "文章摘要",
|
| 57 |
+
"url": "文章网址",
|
| 58 |
+
"publish_date": "文章发布时间"
|
| 59 |
+
}
|
| 60 |
+
]
|
| 61 |
+
}
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
### 字段说明
|
| 65 |
+
| 字段名 | 类型 | 说明 | 格式 |
|
| 66 |
+
|--------|------|------|------|
|
| 67 |
+
| data | LIST | 返回的文章信息列表 | - |
|
| 68 |
+
| title | STRING | 文章标题 | - |
|
| 69 |
+
| summary | STRING | 文章摘要 | - |
|
| 70 |
+
| url | STRING | 文章网址 | URL格式 |
|
| 71 |
+
| publish_date | STRING | 文章发布时间 | `YYYY-MM-DD HH:MM:SS` |
|
| 72 |
+
|
| 73 |
+
## 响应示例
|
| 74 |
+
```json
|
| 75 |
+
{
|
| 76 |
+
"data": [
|
| 77 |
+
{
|
| 78 |
+
"title": "人工智能助力金融行业数字化转型",
|
| 79 |
+
"summary": "近日,多家金融机构宣布采用人工智能技术优化风控系统...",
|
| 80 |
+
"url": "https://example.com/article/123",
|
| 81 |
+
"publish_date": "2024-01-15 10:30:00"
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"title": "AI芯片市场迎来爆发式增长",
|
| 85 |
+
"summary": "随着人工智能应用的普及,AI芯片市场需求持续攀升...",
|
| 86 |
+
"url": "https://example.com/article/124",
|
| 87 |
+
"publish_date": "2024-01-14 14:20:00"
|
| 88 |
+
}
|
| 89 |
+
]
|
| 90 |
+
}
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
## 错误码
|
| 94 |
+
| 状态码 | 说明 | 处理建议 |
|
| 95 |
+
|--------|------|----------|
|
| 96 |
+
| 200 | 请求成功 | - |
|
| 97 |
+
| 400 | 请求参数错误 | 检查请求参数格式和必填项 |
|
| 98 |
+
| 401 | 认证失败 | 检查API Key是否正确且有效 |
|
| 99 |
+
| 403 | 权限不足 | 检查API Key是否有访问此接口的权限 |
|
| 100 |
+
| 500 | 服务器内部错误 | 稍后重试或联系技术支持 |
|
| 101 |
+
|
| 102 |
+
## 使用限制
|
| 103 |
+
- 请求频率限制:请参考API提供商的具体限制
|
| 104 |
+
- 数据返回限制:每次请求返回的文章数量可能有上限
|
| 105 |
+
- 数据时效性:返回的文章按发布时间倒序排列
|
| 106 |
+
|
| 107 |
+
## 注意事项
|
| 108 |
+
1. API Key需要妥善保管,不要泄露
|
| 109 |
+
2. 请求参数中的`channels`和`app_id`为固定值,不要修改
|
| 110 |
+
3. `query`参数支持中文关键词搜索
|
| 111 |
+
4. 接口响应时间可能因网络状况和查询复杂度而异
|
Skills/news-search/scripts/__main__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
新闻搜索技能命令行入口点
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
# 添加当前目录到Python路径
|
| 11 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 12 |
+
|
| 13 |
+
from news_search import main
|
| 14 |
+
|
| 15 |
+
if __name__ == "__main__":
|
| 16 |
+
main()
|
Skills/news-search/scripts/config.example.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"api": {
|
| 3 |
+
"base_url": "https://openapi.iwencai.com",
|
| 4 |
+
"endpoint": "/v1/comprehensive/search",
|
| 5 |
+
"timeout": 30,
|
| 6 |
+
"max_retries": 3,
|
| 7 |
+
"retry_delay": 1.0
|
| 8 |
+
},
|
| 9 |
+
"search": {
|
| 10 |
+
"default_limit": 10,
|
| 11 |
+
"default_days": 30,
|
| 12 |
+
"min_articles_for_sufficient": 3
|
| 13 |
+
},
|
| 14 |
+
"output": {
|
| 15 |
+
"default_format": "text",
|
| 16 |
+
"csv_encoding": "utf-8-sig",
|
| 17 |
+
"json_indent": 2
|
| 18 |
+
},
|
| 19 |
+
"logging": {
|
| 20 |
+
"level": "INFO",
|
| 21 |
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 22 |
+
},
|
| 23 |
+
"注意": "API密钥应从环境变量 IWENCAI_API_KEY 获取,不要在此配置文件中硬编码"
|
| 24 |
+
}
|
Skills/news-search/scripts/config.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
新闻搜索技能配置文件
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Dict, Any, Optional
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class Config:
|
| 17 |
+
"""配置管理类"""
|
| 18 |
+
|
| 19 |
+
DEFAULT_CONFIG = {
|
| 20 |
+
"api": {
|
| 21 |
+
"base_url": "https://openapi.iwencai.com",
|
| 22 |
+
"endpoint": "/v1/comprehensive/search",
|
| 23 |
+
"timeout": 30,
|
| 24 |
+
"max_retries": 3,
|
| 25 |
+
"retry_delay": 1.0,
|
| 26 |
+
},
|
| 27 |
+
"search": {
|
| 28 |
+
"default_limit": 10,
|
| 29 |
+
"default_days": 30,
|
| 30 |
+
"min_articles_for_sufficient": 3,
|
| 31 |
+
},
|
| 32 |
+
"output": {
|
| 33 |
+
"default_format": "text",
|
| 34 |
+
"csv_encoding": "utf-8-sig",
|
| 35 |
+
"json_indent": 2,
|
| 36 |
+
},
|
| 37 |
+
"logging": {
|
| 38 |
+
"level": "INFO",
|
| 39 |
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
def __init__(self, config_file: Optional[str] = None):
|
| 44 |
+
"""
|
| 45 |
+
初始化配置
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
config_file: 配置文件路径,如果为None则使用默认配置
|
| 49 |
+
"""
|
| 50 |
+
self.config_file = config_file
|
| 51 |
+
self.config = self.DEFAULT_CONFIG.copy()
|
| 52 |
+
|
| 53 |
+
if config_file and Path(config_file).exists():
|
| 54 |
+
self.load_config(config_file)
|
| 55 |
+
else:
|
| 56 |
+
# 尝试从环境变量加载配置
|
| 57 |
+
self.load_from_env()
|
| 58 |
+
|
| 59 |
+
logger.debug("配置初始化完成")
|
| 60 |
+
|
| 61 |
+
def load_config(self, config_file: str) -> None:
|
| 62 |
+
"""
|
| 63 |
+
从配置文件加载配置
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
config_file: 配置文件路径
|
| 67 |
+
"""
|
| 68 |
+
try:
|
| 69 |
+
with open(config_file, 'r', encoding='utf-8') as f:
|
| 70 |
+
user_config = json.load(f)
|
| 71 |
+
|
| 72 |
+
# 深度合并配置
|
| 73 |
+
self._merge_config(self.config, user_config)
|
| 74 |
+
logger.info(f"已从配置文件加载配置: {config_file}")
|
| 75 |
+
|
| 76 |
+
except json.JSONDecodeError as e:
|
| 77 |
+
logger.error(f"配置文件格式错误: {config_file} - {str(e)}")
|
| 78 |
+
except Exception as e:
|
| 79 |
+
logger.error(f"加载配置文件失败: {config_file} - {str(e)}")
|
| 80 |
+
|
| 81 |
+
def load_from_env(self) -> None:
|
| 82 |
+
"""从环境变量加载配置"""
|
| 83 |
+
env_config = {}
|
| 84 |
+
|
| 85 |
+
# API配置
|
| 86 |
+
api_key = os.getenv("IWENCAI_API_KEY")
|
| 87 |
+
if api_key:
|
| 88 |
+
env_config["api_key"] = api_key
|
| 89 |
+
|
| 90 |
+
# 搜索配置
|
| 91 |
+
default_limit = os.getenv("NEWS_SEARCH_DEFAULT_LIMIT")
|
| 92 |
+
if default_limit:
|
| 93 |
+
try:
|
| 94 |
+
env_config.setdefault("search", {})["default_limit"] = int(default_limit)
|
| 95 |
+
except ValueError:
|
| 96 |
+
logger.warning(f"无效的环境变量值: NEWS_SEARCH_DEFAULT_LIMIT={default_limit}")
|
| 97 |
+
|
| 98 |
+
default_days = os.getenv("NEWS_SEARCH_DEFAULT_DAYS")
|
| 99 |
+
if default_days:
|
| 100 |
+
try:
|
| 101 |
+
env_config.setdefault("search", {})["default_days"] = int(default_days)
|
| 102 |
+
except ValueError:
|
| 103 |
+
logger.warning(f"无效的环境变量值: NEWS_SEARCH_DEFAULT_DAYS={default_days}")
|
| 104 |
+
|
| 105 |
+
# 日志配置
|
| 106 |
+
log_level = os.getenv("NEWS_SEARCH_LOG_LEVEL")
|
| 107 |
+
if log_level and log_level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
|
| 108 |
+
env_config.setdefault("logging", {})["level"] = log_level
|
| 109 |
+
|
| 110 |
+
if env_config:
|
| 111 |
+
self._merge_config(self.config, env_config)
|
| 112 |
+
logger.debug("已从环境变量加载配置")
|
| 113 |
+
|
| 114 |
+
def _merge_config(self, base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
|
| 115 |
+
"""
|
| 116 |
+
深度合并两个配置字典
|
| 117 |
+
|
| 118 |
+
Args:
|
| 119 |
+
base: 基础配置
|
| 120 |
+
update: 更新配置
|
| 121 |
+
|
| 122 |
+
Returns:
|
| 123 |
+
合并后的配置
|
| 124 |
+
"""
|
| 125 |
+
for key, value in update.items():
|
| 126 |
+
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
|
| 127 |
+
base[key] = self._merge_config(base[key], value)
|
| 128 |
+
else:
|
| 129 |
+
base[key] = value
|
| 130 |
+
return base
|
| 131 |
+
|
| 132 |
+
def save_config(self, config_file: Optional[str] = None) -> None:
|
| 133 |
+
"""
|
| 134 |
+
保存配置到文件
|
| 135 |
+
|
| 136 |
+
Args:
|
| 137 |
+
config_file: 配置文件路径,如果为None则使用初始化时的路径
|
| 138 |
+
"""
|
| 139 |
+
save_file = config_file or self.config_file
|
| 140 |
+
if not save_file:
|
| 141 |
+
logger.error("未指定配置文件路径")
|
| 142 |
+
return
|
| 143 |
+
|
| 144 |
+
try:
|
| 145 |
+
# 确保目录存在
|
| 146 |
+
Path(save_file).parent.mkdir(parents=True, exist_ok=True)
|
| 147 |
+
|
| 148 |
+
with open(save_file, 'w', encoding='utf-8') as f:
|
| 149 |
+
json.dump(self.config, f, ensure_ascii=False, indent=2)
|
| 150 |
+
|
| 151 |
+
logger.info(f"配置已保存到: {save_file}")
|
| 152 |
+
|
| 153 |
+
except Exception as e:
|
| 154 |
+
logger.error(f"保存配置失败: {save_file} - {str(e)}")
|
| 155 |
+
|
| 156 |
+
def get(self, key: str, default: Any = None) -> Any:
|
| 157 |
+
"""
|
| 158 |
+
获取配置值
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
key: 配置键,支持点号分隔,如 "api.base_url"
|
| 162 |
+
default: 默认值
|
| 163 |
+
|
| 164 |
+
Returns:
|
| 165 |
+
配置值
|
| 166 |
+
"""
|
| 167 |
+
keys = key.split('.')
|
| 168 |
+
value = self.config
|
| 169 |
+
|
| 170 |
+
try:
|
| 171 |
+
for k in keys:
|
| 172 |
+
value = value[k]
|
| 173 |
+
return value
|
| 174 |
+
except (KeyError, TypeError):
|
| 175 |
+
return default
|
| 176 |
+
|
| 177 |
+
def set(self, key: str, value: Any) -> None:
|
| 178 |
+
"""
|
| 179 |
+
设置配置值
|
| 180 |
+
|
| 181 |
+
Args:
|
| 182 |
+
key: 配置键,支持点号分隔,如 "api.base_url"
|
| 183 |
+
value: 配置值
|
| 184 |
+
"""
|
| 185 |
+
keys = key.split('.')
|
| 186 |
+
config = self.config
|
| 187 |
+
|
| 188 |
+
# 遍历到倒数第二个键
|
| 189 |
+
for k in keys[:-1]:
|
| 190 |
+
if k not in config or not isinstance(config[k], dict):
|
| 191 |
+
config[k] = {}
|
| 192 |
+
config = config[k]
|
| 193 |
+
|
| 194 |
+
# 设置最后一个键的值
|
| 195 |
+
config[keys[-1]] = value
|
| 196 |
+
logger.debug(f"配置已更新: {key} = {value}")
|
| 197 |
+
|
| 198 |
+
def get_api_key(self) -> Optional[str]:
|
| 199 |
+
"""获取API密钥"""
|
| 200 |
+
return os.getenv("IWENCAI_API_KEY") or self.get("api_key")
|
| 201 |
+
|
| 202 |
+
def get_api_config(self) -> Dict[str, Any]:
|
| 203 |
+
"""获取API配置"""
|
| 204 |
+
return self.get("api", {})
|
| 205 |
+
|
| 206 |
+
def get_search_config(self) -> Dict[str, Any]:
|
| 207 |
+
"""获取搜索配置"""
|
| 208 |
+
return self.get("search", {})
|
| 209 |
+
|
| 210 |
+
def get_output_config(self) -> Dict[str, Any]:
|
| 211 |
+
"""获取输出配置"""
|
| 212 |
+
return self.get("output", {})
|
| 213 |
+
|
| 214 |
+
def get_logging_config(self) -> Dict[str, Any]:
|
| 215 |
+
"""获取日志配置"""
|
| 216 |
+
return self.get("logging", {})
|
| 217 |
+
|
| 218 |
+
def setup_logging(self) -> None:
|
| 219 |
+
"""设置日志配置"""
|
| 220 |
+
log_config = self.get_logging_config()
|
| 221 |
+
level = getattr(logging, log_config.get("level", "INFO"))
|
| 222 |
+
format_str = log_config.get("format", "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
| 223 |
+
|
| 224 |
+
logging.basicConfig(
|
| 225 |
+
level=level,
|
| 226 |
+
format=format_str
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
logger.debug(f"日志配置已设置: level={level}")
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
# 全局配置实例
|
| 233 |
+
_config_instance: Optional[Config] = None
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def get_config(config_file: Optional[str] = None) -> Config:
|
| 237 |
+
"""
|
| 238 |
+
获取配置实例(单例模式)
|
| 239 |
+
|
| 240 |
+
Args:
|
| 241 |
+
config_file: 配置文件路径
|
| 242 |
+
|
| 243 |
+
Returns:
|
| 244 |
+
配置实例
|
| 245 |
+
"""
|
| 246 |
+
global _config_instance
|
| 247 |
+
|
| 248 |
+
if _config_instance is None:
|
| 249 |
+
_config_instance = Config(config_file)
|
| 250 |
+
|
| 251 |
+
return _config_instance
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def create_default_config(config_file: str) -> None:
|
| 255 |
+
"""
|
| 256 |
+
创建默认配置文件
|
| 257 |
+
|
| 258 |
+
Args:
|
| 259 |
+
config_file: 配置文件路径
|
| 260 |
+
"""
|
| 261 |
+
config = Config()
|
| 262 |
+
config.save_config(config_file)
|
| 263 |
+
logger.info(f"已创建默认配置文件: {config_file}")
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
if __name__ == "__main__":
|
| 267 |
+
# 测试配置类
|
| 268 |
+
config = get_config()
|
| 269 |
+
print("API配置:", config.get_api_config())
|
| 270 |
+
print("搜索配置:", config.get_search_config())
|
| 271 |
+
print("API密钥:", config.get_api_key())
|
Skills/news-search/scripts/example_usage.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
新闻搜索技能使用示例
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
# 添加当前目录到Python路径
|
| 12 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 13 |
+
|
| 14 |
+
from news_search import NewsSearchAPI, NewsProcessor, QueryProcessor
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def example_basic_search():
|
| 18 |
+
"""基本搜索示例"""
|
| 19 |
+
print("=" * 60)
|
| 20 |
+
print("示例1: 基本搜索")
|
| 21 |
+
print("=" * 60)
|
| 22 |
+
|
| 23 |
+
# 注意: 实际使用时需要设置 IWENCAI_API_KEY 环境变量
|
| 24 |
+
# export IWENCAI_API_KEY="your_api_key_here"
|
| 25 |
+
|
| 26 |
+
api_key = os.getenv("IWENCAI_API_KEY")
|
| 27 |
+
if not api_key:
|
| 28 |
+
print("警告: 未设置 IWENCAI_API_KEY 环境变量")
|
| 29 |
+
print("请先运行: export IWENCAI_API_KEY='your_api_key_here'")
|
| 30 |
+
print("使用模拟数据进行演示...")
|
| 31 |
+
|
| 32 |
+
# 模拟数据演示
|
| 33 |
+
processor = NewsProcessor()
|
| 34 |
+
test_articles = [
|
| 35 |
+
{
|
| 36 |
+
"title": "人工智能助力金融行业数字化转型",
|
| 37 |
+
"summary": "近日,多家金融机构宣布采用人工智能技术优化风控系统...",
|
| 38 |
+
"url": "https://example.com/article/123",
|
| 39 |
+
"publish_date": "2026-04-01 10:30:00"
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"title": "AI芯片市场迎来爆发式增长",
|
| 43 |
+
"summary": "随着人工智能应用的普及,AI芯片市场需求持续攀升...",
|
| 44 |
+
"url": "https://example.com/article/124",
|
| 45 |
+
"publish_date": "2026-03-28 14:20:00"
|
| 46 |
+
}
|
| 47 |
+
]
|
| 48 |
+
|
| 49 |
+
print(f"查询: 人工智能")
|
| 50 |
+
print(f"找到 {len(test_articles)} 篇文章")
|
| 51 |
+
print()
|
| 52 |
+
|
| 53 |
+
for i, article in enumerate(test_articles, 1):
|
| 54 |
+
print(f"{i}. {article['title']}")
|
| 55 |
+
print(f" 摘要: {article['summary']}")
|
| 56 |
+
print(f" 发布时间: {article['publish_date']}")
|
| 57 |
+
print(f" 链接: {article['url']}")
|
| 58 |
+
print()
|
| 59 |
+
|
| 60 |
+
return
|
| 61 |
+
|
| 62 |
+
# 实际API调用
|
| 63 |
+
try:
|
| 64 |
+
api = NewsSearchAPI(api_key=api_key)
|
| 65 |
+
processor = NewsProcessor()
|
| 66 |
+
|
| 67 |
+
# 搜索
|
| 68 |
+
query = "人工智能"
|
| 69 |
+
print(f"搜索查询: {query}")
|
| 70 |
+
articles = api.search(query)
|
| 71 |
+
|
| 72 |
+
# 处理结果
|
| 73 |
+
articles = processor.filter_by_date(articles, days=30)
|
| 74 |
+
articles = processor.sort_by_date(articles)
|
| 75 |
+
articles = processor.limit_results(articles, 5)
|
| 76 |
+
|
| 77 |
+
print(f"找到 {len(articles)} 篇文章 (最近30天)")
|
| 78 |
+
print()
|
| 79 |
+
|
| 80 |
+
for i, article in enumerate(articles, 1):
|
| 81 |
+
print(f"{i}. {article.get('title', '无标题')}")
|
| 82 |
+
print(f" 摘要: {article.get('summary', '无摘要')[:100]}...")
|
| 83 |
+
print(f" 发布时间: {article.get('publish_date', '未知时间')}")
|
| 84 |
+
print(f" 链接: {article.get('url', '无链接')}")
|
| 85 |
+
print()
|
| 86 |
+
|
| 87 |
+
except Exception as e:
|
| 88 |
+
print(f"搜索失败: {str(e)}")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def example_complex_query():
|
| 92 |
+
"""复杂查询示例"""
|
| 93 |
+
print("\n" + "=" * 60)
|
| 94 |
+
print("示例2: 复杂查询拆解")
|
| 95 |
+
print("=" * 60)
|
| 96 |
+
|
| 97 |
+
query_processor = QueryProcessor()
|
| 98 |
+
|
| 99 |
+
test_queries = [
|
| 100 |
+
"人工智能和芯片行业",
|
| 101 |
+
"新能源汽车与锂电池技术",
|
| 102 |
+
"央行货币政策以及财政政策",
|
| 103 |
+
]
|
| 104 |
+
|
| 105 |
+
for query in test_queries:
|
| 106 |
+
sub_queries = query_processor.split_complex_query(query)
|
| 107 |
+
print(f"原始查询: '{query}'")
|
| 108 |
+
print(f"拆解结果: {sub_queries}")
|
| 109 |
+
print()
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def example_data_processing():
|
| 113 |
+
"""数据处理示例"""
|
| 114 |
+
print("\n" + "=" * 60)
|
| 115 |
+
print("示例3: 数据处理")
|
| 116 |
+
print("=" * 60)
|
| 117 |
+
|
| 118 |
+
processor = NewsProcessor()
|
| 119 |
+
|
| 120 |
+
# 模拟数据
|
| 121 |
+
test_articles = [
|
| 122 |
+
{
|
| 123 |
+
"title": "文章A",
|
| 124 |
+
"summary": "摘要A",
|
| 125 |
+
"url": "http://example.com/a",
|
| 126 |
+
"publish_date": "2026-04-01 10:30:00"
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"title": "文章B",
|
| 130 |
+
"summary": "摘要B",
|
| 131 |
+
"url": "http://example.com/b",
|
| 132 |
+
"publish_date": "2026-03-15 14:20:00"
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"title": "文章C",
|
| 136 |
+
"summary": "摘要C",
|
| 137 |
+
"url": "http://example.com/c",
|
| 138 |
+
"publish_date": "2026-02-01 09:15:00"
|
| 139 |
+
}
|
| 140 |
+
]
|
| 141 |
+
|
| 142 |
+
print("原始数据:")
|
| 143 |
+
for article in test_articles:
|
| 144 |
+
print(f" - {article['title']} ({article['publish_date']})")
|
| 145 |
+
|
| 146 |
+
# 时间过滤
|
| 147 |
+
filtered = processor.filter_by_date(test_articles, days=60)
|
| 148 |
+
print(f"\n时间过滤 (最近60天): {len(filtered)} 篇文章")
|
| 149 |
+
|
| 150 |
+
# 排序
|
| 151 |
+
sorted_articles = processor.sort_by_date(filtered, reverse=True)
|
| 152 |
+
print("\n按时间排序 (从新到旧):")
|
| 153 |
+
for article in sorted_articles:
|
| 154 |
+
print(f" - {article['title']} ({article['publish_date']})")
|
| 155 |
+
|
| 156 |
+
# 提取关键信息
|
| 157 |
+
print("\n关键信息提取示例:")
|
| 158 |
+
key_info = processor.extract_key_info(test_articles[0])
|
| 159 |
+
for key, value in key_info.items():
|
| 160 |
+
print(f" {key}: {value}")
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def example_file_export():
|
| 164 |
+
"""文件导出示例"""
|
| 165 |
+
print("\n" + "=" * 60)
|
| 166 |
+
print("示例4: 文件导出")
|
| 167 |
+
print("=" * 60)
|
| 168 |
+
|
| 169 |
+
processor = NewsProcessor()
|
| 170 |
+
|
| 171 |
+
# 模拟数据
|
| 172 |
+
test_articles = [
|
| 173 |
+
{
|
| 174 |
+
"title": "人工智能行业报告",
|
| 175 |
+
"summary": "2026年人工智能行业发展前景分析",
|
| 176 |
+
"url": "http://example.com/ai-report",
|
| 177 |
+
"publish_date": "2026-04-01 10:30:00"
|
| 178 |
+
},
|
| 179 |
+
{
|
| 180 |
+
"title": "芯片技术突破",
|
| 181 |
+
"summary": "国产芯片实现技术突破,性能提升显著",
|
| 182 |
+
"url": "http://example.com/chip-breakthrough",
|
| 183 |
+
"publish_date": "2026-03-28 14:20:00"
|
| 184 |
+
}
|
| 185 |
+
]
|
| 186 |
+
|
| 187 |
+
import tempfile
|
| 188 |
+
import json
|
| 189 |
+
|
| 190 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 191 |
+
tmpdir = Path(tmpdir)
|
| 192 |
+
|
| 193 |
+
# CSV导出
|
| 194 |
+
csv_path = tmpdir / "news.csv"
|
| 195 |
+
processor.save_to_csv(test_articles, str(csv_path))
|
| 196 |
+
print(f"CSV文件已保存: {csv_path}")
|
| 197 |
+
|
| 198 |
+
# JSON导出
|
| 199 |
+
json_path = tmpdir / "news.json"
|
| 200 |
+
processor.save_to_json(test_articles, str(json_path))
|
| 201 |
+
print(f"JSON文件已保存: {json_path}")
|
| 202 |
+
|
| 203 |
+
# 验证JSON内容
|
| 204 |
+
with open(json_path, 'r', encoding='utf-8') as f:
|
| 205 |
+
data = json.load(f)
|
| 206 |
+
print(f"\nJSON文件内容验证:")
|
| 207 |
+
print(f" 文章数量: {len(data)}")
|
| 208 |
+
print(f" 第一篇文章标题: {data[0]['title']}")
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def example_cli_usage():
|
| 212 |
+
"""CLI使用示例"""
|
| 213 |
+
print("\n" + "=" * 60)
|
| 214 |
+
print("示例5: CLI命令行使用")
|
| 215 |
+
print("=" * 60)
|
| 216 |
+
|
| 217 |
+
print("""
|
| 218 |
+
基本用法:
|
| 219 |
+
python news_search.py -q "人工智能"
|
| 220 |
+
|
| 221 |
+
搜索最近7天的新闻:
|
| 222 |
+
python news_search.py -q "芯片行业" -d 7
|
| 223 |
+
|
| 224 |
+
限制返回结果数量:
|
| 225 |
+
python news_search.py -q "新能源汽车" -l 5
|
| 226 |
+
|
| 227 |
+
导出为CSV格式:
|
| 228 |
+
python news_search.py -q "人工智能" -o results.csv -f csv
|
| 229 |
+
|
| 230 |
+
导出为JSON格式:
|
| 231 |
+
python news_search.py -q "人工智能" -o results.json -f json
|
| 232 |
+
|
| 233 |
+
批量处理:
|
| 234 |
+
python news_search.py -i queries.txt -o output/ -f csv
|
| 235 |
+
|
| 236 |
+
管道操作:
|
| 237 |
+
echo "人工智能" | python news_search.py
|
| 238 |
+
|
| 239 |
+
启用调试模式:
|
| 240 |
+
python news_search.py -q "测试" --debug
|
| 241 |
+
""")
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def main():
|
| 245 |
+
"""主函数"""
|
| 246 |
+
print("新闻搜索技能使用示例")
|
| 247 |
+
print("=" * 60)
|
| 248 |
+
print()
|
| 249 |
+
|
| 250 |
+
examples = [
|
| 251 |
+
example_basic_search,
|
| 252 |
+
example_complex_query,
|
| 253 |
+
example_data_processing,
|
| 254 |
+
example_file_export,
|
| 255 |
+
example_cli_usage,
|
| 256 |
+
]
|
| 257 |
+
|
| 258 |
+
for example_func in examples:
|
| 259 |
+
example_func()
|
| 260 |
+
input("\n按Enter键继续...")
|
| 261 |
+
print()
|
| 262 |
+
|
| 263 |
+
print("=" * 60)
|
| 264 |
+
print("示例演示完成!")
|
| 265 |
+
print("=" * 60)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
if __name__ == "__main__":
|
| 269 |
+
main()
|
Skills/news-search/scripts/news_search.py
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
新闻搜索技能主文件
|
| 5 |
+
财经领域资讯搜索引擎,调用同花顺问财的财经资讯搜索接口
|
| 6 |
+
|
| 7 |
+
本技能遵守问财OpenAPI网关规范:
|
| 8 |
+
1. 所有请求必须包含X-Claw-* HTTP头
|
| 9 |
+
2. 使用Bearer Token认证,从IWENCAI_API_KEY环境变量读取
|
| 10 |
+
3. 每次请求生成64字符唯一Trace ID
|
| 11 |
+
4. 支持跨平台使用
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import sys
|
| 16 |
+
import json
|
| 17 |
+
import logging
|
| 18 |
+
import argparse
|
| 19 |
+
import secrets
|
| 20 |
+
from typing import List, Dict, Any, Optional
|
| 21 |
+
from datetime import datetime
|
| 22 |
+
import csv
|
| 23 |
+
import time
|
| 24 |
+
import requests
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
|
| 27 |
+
# 配置日志
|
| 28 |
+
logging.basicConfig(
|
| 29 |
+
level=logging.INFO,
|
| 30 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 31 |
+
)
|
| 32 |
+
logger = logging.getLogger(__name__)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class NewsSearchAPI:
|
| 36 |
+
"""同花顺问财经资讯搜索API封装类"""
|
| 37 |
+
|
| 38 |
+
def __init__(self, api_key: Optional[str] = None):
|
| 39 |
+
"""
|
| 40 |
+
初始化API客户端
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
api_key: API密钥,如果为None则从环境变量获取
|
| 44 |
+
"""
|
| 45 |
+
self.base_url = "https://openapi.iwencai.com"
|
| 46 |
+
self.endpoint = "/v1/comprehensive/search"
|
| 47 |
+
self.api_key = api_key or os.getenv("IWENCAI_API_KEY")
|
| 48 |
+
|
| 49 |
+
if not self.api_key:
|
| 50 |
+
raise ValueError("API密钥未设置。请设置环境变量 IWENCAI_API_KEY 或通过参数传入")
|
| 51 |
+
|
| 52 |
+
self.session = requests.Session()
|
| 53 |
+
|
| 54 |
+
# 基础请求头
|
| 55 |
+
base_headers = {
|
| 56 |
+
"Content-Type": "application/json",
|
| 57 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 58 |
+
"User-Agent": "NewsSearchSkill/1.0.0"
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
self.session.headers.update(base_headers)
|
| 62 |
+
|
| 63 |
+
logger.info("NewsSearchAPI客户端初始化完成")
|
| 64 |
+
|
| 65 |
+
def _generate_trace_id(self) -> str:
|
| 66 |
+
"""
|
| 67 |
+
生成64字符的唯一Trace ID
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
64字符的十六进制字符串
|
| 71 |
+
"""
|
| 72 |
+
return secrets.token_hex(32)
|
| 73 |
+
|
| 74 |
+
def _get_headers(self, call_type: str = "normal") -> Dict[str, str]:
|
| 75 |
+
"""
|
| 76 |
+
获取问财OpenAPI网关要求的HTTP头
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
call_type: 调用类型,normal或retry
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
包含问财OpenAPI网关要求的HTTP头的字典
|
| 83 |
+
"""
|
| 84 |
+
headers = self.session.headers.copy()
|
| 85 |
+
|
| 86 |
+
# 添加问财OpenAPI网关要求的HTTP头
|
| 87 |
+
headers.update({
|
| 88 |
+
"X-Claw-Call-Type": call_type,
|
| 89 |
+
"X-Claw-Skill-Id": "news-search",
|
| 90 |
+
"X-Claw-Skill-Version": "1.0.0",
|
| 91 |
+
"X-Claw-Plugin-Id": "none",
|
| 92 |
+
"X-Claw-Plugin-Version": "none",
|
| 93 |
+
"X-Claw-Trace-Id": self._generate_trace_id()
|
| 94 |
+
})
|
| 95 |
+
|
| 96 |
+
return headers
|
| 97 |
+
|
| 98 |
+
def search_raw(self, query: str, max_retries: int = 3, call_type: str = "normal") -> Dict[str, Any]:
|
| 99 |
+
"""
|
| 100 |
+
搜索财经资讯(原始响应,符合问财OpenAPI网关透明传递要求)
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
query: 搜索关键词
|
| 104 |
+
max_retries: 最大重试次数
|
| 105 |
+
call_type: 调用类型,normal或retry
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
API原始响应JSON,不做任何处理
|
| 109 |
+
|
| 110 |
+
Note:
|
| 111 |
+
根据问财OpenAPI网关规范条件六,此方法必须透明传递所有API响应,
|
| 112 |
+
包括错误响应,不做任何修改、过滤或重组。
|
| 113 |
+
"""
|
| 114 |
+
payload = {
|
| 115 |
+
"channels": ["news"],
|
| 116 |
+
"app_id": "AIME_SKILL",
|
| 117 |
+
"query": query
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
url = f"{self.base_url}{self.endpoint}"
|
| 121 |
+
|
| 122 |
+
for attempt in range(max_retries):
|
| 123 |
+
try:
|
| 124 |
+
logger.info(f"搜索查询: {query} (尝试 {attempt + 1}/{max_retries})")
|
| 125 |
+
|
| 126 |
+
# 获取问财OpenAPI网关要求的HTTP头
|
| 127 |
+
headers = self._get_headers(call_type)
|
| 128 |
+
|
| 129 |
+
response = self.session.post(url, json=payload, headers=headers, timeout=30)
|
| 130 |
+
|
| 131 |
+
# 完全透明传递:尝试返回JSON响应,如果不是JSON则返回文本
|
| 132 |
+
try:
|
| 133 |
+
return response.json()
|
| 134 |
+
except ValueError:
|
| 135 |
+
# API返回的不是JSON格式,返回原始文本
|
| 136 |
+
logger.warning(f"API返回非JSON响应,状态码: {response.status_code}")
|
| 137 |
+
return {
|
| 138 |
+
"error": "invalid_json_response",
|
| 139 |
+
"raw_response": response.text,
|
| 140 |
+
"status_code": response.status_code,
|
| 141 |
+
"headers": dict(response.headers)
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
except requests.exceptions.Timeout:
|
| 145 |
+
logger.warning(f"请求超时 (尝试 {attempt + 1}/{max_retries})")
|
| 146 |
+
if attempt < max_retries - 1:
|
| 147 |
+
time.sleep(2 ** attempt) # 指数退避
|
| 148 |
+
else:
|
| 149 |
+
# 网络错误:返回错误信息,但仍保持透明结构
|
| 150 |
+
raise requests.exceptions.Timeout(f"请求超时,已达到最大重试次数: {max_retries}")
|
| 151 |
+
except requests.exceptions.ConnectionError:
|
| 152 |
+
logger.warning(f"连接错误 (尝试 {attempt + 1}/{max_retries})")
|
| 153 |
+
if attempt < max_retries - 1:
|
| 154 |
+
time.sleep(2 ** attempt) # 指数退避
|
| 155 |
+
else:
|
| 156 |
+
# 网络错误:抛出异常,由调用方处理
|
| 157 |
+
raise requests.exceptions.ConnectionError(f"连接错误,已达到最大重试次数: {max_retries}")
|
| 158 |
+
except Exception as e:
|
| 159 |
+
logger.error(f"搜索过程中发生错误: {str(e)}")
|
| 160 |
+
if attempt < max_retries - 1:
|
| 161 |
+
time.sleep(2 ** attempt) # 指数退避
|
| 162 |
+
else:
|
| 163 |
+
raise
|
| 164 |
+
|
| 165 |
+
# 不应该执行到这里,但如果执行到了,抛出异常
|
| 166 |
+
raise Exception(f"搜索失败,已达到最大重试次数: {max_retries}")
|
| 167 |
+
|
| 168 |
+
def search(self, query: str, max_retries: int = 3, call_type: str = "normal") -> Dict[str, Any]:
|
| 169 |
+
"""
|
| 170 |
+
搜索财经资讯(透明传递包装方法)
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
query: 搜索关键词
|
| 174 |
+
max_retries: 最大重试次数
|
| 175 |
+
call_type: 调用类型,normal或retry
|
| 176 |
+
|
| 177 |
+
Returns:
|
| 178 |
+
API原始响应JSON,完全透明传递,不做任何修改
|
| 179 |
+
|
| 180 |
+
Note:
|
| 181 |
+
根据问财OpenAPI网关规范条件六,此方法完全透明传递API响应,
|
| 182 |
+
调用方需要自己处理响应数据。这是推荐的使用方式。
|
| 183 |
+
"""
|
| 184 |
+
return self.search_raw(query, max_retries, call_type)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def batch_search(self, queries: List[str], delay: float = 1.0, call_type: str = "normal") -> Dict[str, Dict[str, Any]]:
|
| 189 |
+
"""
|
| 190 |
+
批量搜索(透明传递版本)
|
| 191 |
+
|
| 192 |
+
Args:
|
| 193 |
+
queries: 搜索关键词列表
|
| 194 |
+
delay: 每次请求之间的延迟(秒),避免触发频率限制
|
| 195 |
+
call_type: 调用类型,normal或retry
|
| 196 |
+
|
| 197 |
+
Returns:
|
| 198 |
+
字典,键为查询词,值为对应的透明传递的API响应
|
| 199 |
+
|
| 200 |
+
Note:
|
| 201 |
+
此方法完全透明传递API响应,符合问财规范条件六。
|
| 202 |
+
调用方需要自己处理响应数据。
|
| 203 |
+
"""
|
| 204 |
+
results = {}
|
| 205 |
+
|
| 206 |
+
for i, query in enumerate(queries):
|
| 207 |
+
try:
|
| 208 |
+
logger.info(f"批量搜索进度: {i+1}/{len(queries)} - {query}")
|
| 209 |
+
response = self.search(query, call_type=call_type)
|
| 210 |
+
results[query] = response
|
| 211 |
+
|
| 212 |
+
# 避免触发频率限制
|
| 213 |
+
if i < len(queries) - 1:
|
| 214 |
+
time.sleep(delay)
|
| 215 |
+
|
| 216 |
+
except Exception as e:
|
| 217 |
+
logger.error(f"查询 '{query}' 搜索失败: {str(e)}")
|
| 218 |
+
results[query] = {"error": "search_failed", "message": str(e)}
|
| 219 |
+
|
| 220 |
+
return results
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
class NewsProcessor:
|
| 224 |
+
"""新闻数据处理类"""
|
| 225 |
+
|
| 226 |
+
@staticmethod
|
| 227 |
+
def filter_by_date(articles: List[Dict[str, Any]], days: int = 7) -> List[Dict[str, Any]]:
|
| 228 |
+
"""
|
| 229 |
+
按时间过滤文章
|
| 230 |
+
|
| 231 |
+
Args:
|
| 232 |
+
articles: 文章列表
|
| 233 |
+
days: 最近多少天内的文章
|
| 234 |
+
|
| 235 |
+
Returns:
|
| 236 |
+
过滤后的文章列表
|
| 237 |
+
"""
|
| 238 |
+
if not articles:
|
| 239 |
+
return []
|
| 240 |
+
|
| 241 |
+
cutoff_date = datetime.now().timestamp() - (days * 24 * 60 * 60)
|
| 242 |
+
filtered = []
|
| 243 |
+
|
| 244 |
+
for article in articles:
|
| 245 |
+
publish_date = article.get("publish_date")
|
| 246 |
+
if not publish_date:
|
| 247 |
+
continue
|
| 248 |
+
|
| 249 |
+
try:
|
| 250 |
+
# 尝试解析日期字符串
|
| 251 |
+
if isinstance(publish_date, str):
|
| 252 |
+
# 移除可能的时区信息
|
| 253 |
+
publish_date = publish_date.split('+')[0].strip()
|
| 254 |
+
dt = datetime.strptime(publish_date, "%Y-%m-%d %H:%M:%S")
|
| 255 |
+
article_timestamp = dt.timestamp()
|
| 256 |
+
|
| 257 |
+
if article_timestamp >= cutoff_date:
|
| 258 |
+
filtered.append(article)
|
| 259 |
+
except (ValueError, TypeError):
|
| 260 |
+
# 如果日期解析失败,保留文章
|
| 261 |
+
filtered.append(article)
|
| 262 |
+
|
| 263 |
+
logger.info(f"时间过滤: 从 {len(articles)} 篇文章中过滤出 {len(filtered)} 篇最近 {days} 天的文章")
|
| 264 |
+
return filtered
|
| 265 |
+
|
| 266 |
+
@staticmethod
|
| 267 |
+
def sort_by_date(articles: List[Dict[str, Any]], reverse: bool = True) -> List[Dict[str, Any]]:
|
| 268 |
+
"""
|
| 269 |
+
按发布时间排序
|
| 270 |
+
|
| 271 |
+
Args:
|
| 272 |
+
articles: 文章列表
|
| 273 |
+
reverse: True表示从新到旧,False表示从旧到新
|
| 274 |
+
|
| 275 |
+
Returns:
|
| 276 |
+
排序后的文章列表
|
| 277 |
+
"""
|
| 278 |
+
def get_timestamp(article):
|
| 279 |
+
publish_date = article.get("publish_date")
|
| 280 |
+
if not publish_date:
|
| 281 |
+
return 0
|
| 282 |
+
|
| 283 |
+
try:
|
| 284 |
+
if isinstance(publish_date, str):
|
| 285 |
+
publish_date = publish_date.split('+')[0].strip()
|
| 286 |
+
dt = datetime.strptime(publish_date, "%Y-%m-%d %H:%M:%S")
|
| 287 |
+
return dt.timestamp()
|
| 288 |
+
except (ValueError, TypeError):
|
| 289 |
+
pass
|
| 290 |
+
return 0
|
| 291 |
+
|
| 292 |
+
sorted_articles = sorted(articles, key=get_timestamp, reverse=reverse)
|
| 293 |
+
return sorted_articles
|
| 294 |
+
|
| 295 |
+
@staticmethod
|
| 296 |
+
def limit_results(articles: List[Dict[str, Any]], limit: int) -> List[Dict[str, Any]]:
|
| 297 |
+
"""
|
| 298 |
+
限制结果数量
|
| 299 |
+
|
| 300 |
+
Args:
|
| 301 |
+
articles: 文章列表
|
| 302 |
+
limit: 最大返回数量
|
| 303 |
+
|
| 304 |
+
Returns:
|
| 305 |
+
限制数量后的文章列表
|
| 306 |
+
"""
|
| 307 |
+
if limit <= 0:
|
| 308 |
+
return articles
|
| 309 |
+
return articles[:limit]
|
| 310 |
+
|
| 311 |
+
@staticmethod
|
| 312 |
+
def extract_key_info(article: Dict[str, Any]) -> Dict[str, Any]:
|
| 313 |
+
"""
|
| 314 |
+
提取文章关键信息
|
| 315 |
+
|
| 316 |
+
Args:
|
| 317 |
+
article: 文章字典
|
| 318 |
+
|
| 319 |
+
Returns:
|
| 320 |
+
包含关键信息的字典
|
| 321 |
+
"""
|
| 322 |
+
return {
|
| 323 |
+
"title": article.get("title", ""),
|
| 324 |
+
"summary": article.get("summary", ""),
|
| 325 |
+
"url": article.get("url", ""),
|
| 326 |
+
"publish_date": article.get("publish_date", ""),
|
| 327 |
+
"source": "同花顺问财"
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
@staticmethod
|
| 331 |
+
def save_to_csv(articles: List[Dict[str, Any]], filepath: str) -> None:
|
| 332 |
+
"""
|
| 333 |
+
保存文章到CSV文件
|
| 334 |
+
|
| 335 |
+
Args:
|
| 336 |
+
articles: 文章列表
|
| 337 |
+
filepath: CSV文件路径
|
| 338 |
+
"""
|
| 339 |
+
if not articles:
|
| 340 |
+
logger.warning("没有文章数据可保存到CSV")
|
| 341 |
+
return
|
| 342 |
+
|
| 343 |
+
# 提取关键信息
|
| 344 |
+
processed_articles = [NewsProcessor.extract_key_info(article) for article in articles]
|
| 345 |
+
|
| 346 |
+
# 确保目录存在
|
| 347 |
+
Path(filepath).parent.mkdir(parents=True, exist_ok=True)
|
| 348 |
+
|
| 349 |
+
with open(filepath, 'w', newline='', encoding='utf-8-sig') as f:
|
| 350 |
+
if processed_articles:
|
| 351 |
+
fieldnames = list(processed_articles[0].keys())
|
| 352 |
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
| 353 |
+
writer.writeheader()
|
| 354 |
+
writer.writerows(processed_articles)
|
| 355 |
+
|
| 356 |
+
logger.info(f"已保存 {len(processed_articles)} 篇文章到 {filepath}")
|
| 357 |
+
|
| 358 |
+
@staticmethod
|
| 359 |
+
def save_to_json(articles: List[Dict[str, Any]], filepath: str) -> None:
|
| 360 |
+
"""
|
| 361 |
+
保存文章到JSON文件
|
| 362 |
+
|
| 363 |
+
Args:
|
| 364 |
+
articles: 文章列表
|
| 365 |
+
filepath: JSON文件路径
|
| 366 |
+
"""
|
| 367 |
+
if not articles:
|
| 368 |
+
logger.warning("没有文章数据可保存到JSON")
|
| 369 |
+
return
|
| 370 |
+
|
| 371 |
+
# 提取关键信息
|
| 372 |
+
processed_articles = [NewsProcessor.extract_key_info(article) for article in articles]
|
| 373 |
+
|
| 374 |
+
# 确保目录存在
|
| 375 |
+
Path(filepath).parent.mkdir(parents=True, exist_ok=True)
|
| 376 |
+
|
| 377 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 378 |
+
json.dump(processed_articles, f, ensure_ascii=False, indent=2)
|
| 379 |
+
|
| 380 |
+
logger.info(f"已保存 {len(processed_articles)} 篇文章到 {filepath}")
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
class QueryProcessor:
|
| 384 |
+
"""查询处理类,负责思维链拆解"""
|
| 385 |
+
|
| 386 |
+
@staticmethod
|
| 387 |
+
def split_complex_query(query: str) -> List[str]:
|
| 388 |
+
"""
|
| 389 |
+
拆解复杂查询
|
| 390 |
+
|
| 391 |
+
Args:
|
| 392 |
+
query: 原始查询
|
| 393 |
+
|
| 394 |
+
Returns:
|
| 395 |
+
拆解后的查询列表
|
| 396 |
+
"""
|
| 397 |
+
# 常见的连接词
|
| 398 |
+
connectors = ["和", "与", "及", "以及", "还有", "并且", "同时"]
|
| 399 |
+
|
| 400 |
+
# 检查是否需要拆解
|
| 401 |
+
needs_split = False
|
| 402 |
+
for connector in connectors:
|
| 403 |
+
if connector in query:
|
| 404 |
+
needs_split = True
|
| 405 |
+
break
|
| 406 |
+
|
| 407 |
+
if not needs_split:
|
| 408 |
+
return [query]
|
| 409 |
+
|
| 410 |
+
# 简单的拆解逻辑
|
| 411 |
+
sub_queries = []
|
| 412 |
+
current_query = query
|
| 413 |
+
|
| 414 |
+
for connector in connectors:
|
| 415 |
+
if connector in current_query:
|
| 416 |
+
parts = current_query.split(connector)
|
| 417 |
+
# 处理每个部分
|
| 418 |
+
for part in parts:
|
| 419 |
+
part = part.strip()
|
| 420 |
+
if part:
|
| 421 |
+
# 进一步检查是否还有连接词
|
| 422 |
+
sub_queries.extend(QueryProcessor.split_complex_query(part))
|
| 423 |
+
break
|
| 424 |
+
|
| 425 |
+
# 如果没有拆解成功,返回原始查询
|
| 426 |
+
if not sub_queries:
|
| 427 |
+
sub_queries = [query]
|
| 428 |
+
|
| 429 |
+
# 去重
|
| 430 |
+
unique_queries = []
|
| 431 |
+
for q in sub_queries:
|
| 432 |
+
if q not in unique_queries:
|
| 433 |
+
unique_queries.append(q)
|
| 434 |
+
|
| 435 |
+
logger.info(f"查询拆解: '{query}' -> {unique_queries}")
|
| 436 |
+
return unique_queries
|
| 437 |
+
|
| 438 |
+
@staticmethod
|
| 439 |
+
def evaluate_results(articles: List[Dict[str, Any]], min_articles: int = 3) -> bool:
|
| 440 |
+
"""
|
| 441 |
+
评估搜索结果是否足够
|
| 442 |
+
|
| 443 |
+
Args:
|
| 444 |
+
articles: 文章列表
|
| 445 |
+
min_articles: 最小文章数量要求
|
| 446 |
+
|
| 447 |
+
Returns:
|
| 448 |
+
是否足够
|
| 449 |
+
"""
|
| 450 |
+
if len(articles) >= min_articles:
|
| 451 |
+
logger.info(f"搜索结果评估: 足够 ({len(articles)} 篇文章)")
|
| 452 |
+
return True
|
| 453 |
+
else:
|
| 454 |
+
logger.warning(f"搜索结果评估: 不足 (只有 {len(articles)} 篇文章,需要至少 {min_articles} 篇)")
|
| 455 |
+
return False
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def main():
|
| 459 |
+
"""主函数,处理命令行参数"""
|
| 460 |
+
parser = argparse.ArgumentParser(
|
| 461 |
+
description="财经新闻搜索工具 - 调用同花顺问财的财经资讯搜索接口(遵守问财OpenAPI网关规范)",
|
| 462 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 463 |
+
epilog="""
|
| 464 |
+
示例:
|
| 465 |
+
%(prog)s -q "人工智能"
|
| 466 |
+
%(prog)s -q "人工智能和芯片行业" -o results.csv -f csv
|
| 467 |
+
%(prog)s -i queries.txt -o output/ -f json
|
| 468 |
+
echo "人工智能" | %(prog)s -f text
|
| 469 |
+
|
| 470 |
+
问财OpenAPI网关规范要求:
|
| 471 |
+
1. 所有请求必须包含X-Claw-* HTTP头(X-Claw-Call-Type, X-Claw-Skill-Id, X-Claw-Skill-Version等)
|
| 472 |
+
2. 使用Bearer Token认证,从IWENCAI_API_KEY环境变量读取API密钥
|
| 473 |
+
3. 每次请求生成64字符全局唯一Trace ID
|
| 474 |
+
4. 返回结果完全透明传递,不对API响应做任何修改、过滤或重组
|
| 475 |
+
|
| 476 |
+
curl示例(脱敏,请替换占位符):
|
| 477 |
+
Unix/Linux/macOS:
|
| 478 |
+
export IWENCAI_API_KEY="your_api_key_here"
|
| 479 |
+
curl -X POST https://openapi.iwencai.com/v1/comprehensive/search \
|
| 480 |
+
-H "Content-Type: application/json" \
|
| 481 |
+
-H "Authorization: Bearer $IWENCAI_API_KEY" \
|
| 482 |
+
-H "X-Claw-Call-Type: normal" \
|
| 483 |
+
-H "X-Claw-Skill-Id: news-search" \
|
| 484 |
+
-H "X-Claw-Skill-Version: 1.0.0" \
|
| 485 |
+
-H "X-Claw-Plugin-Id: none" \
|
| 486 |
+
-H "X-Claw-Plugin-Version: none" \
|
| 487 |
+
-H "X-Claw-Trace-Id: $(openssl rand -hex 32)" \
|
| 488 |
+
-d '{"channels":["news"],"app_id":"AIME_SKILL","query":"人工智能"}'
|
| 489 |
+
|
| 490 |
+
Windows PowerShell:
|
| 491 |
+
$env:IWENCAI_API_KEY="your_api_key_here"
|
| 492 |
+
$traceId = -join ((48..57)+(65..70)+(97..102) | Get-Random -Count 64 | ForEach-Object { [char]$_ })
|
| 493 |
+
curl.exe -X POST https://openapi.iwencai.com/v1/comprehensive/search `
|
| 494 |
+
-H "Content-Type: application/json" `
|
| 495 |
+
-H "Authorization: Bearer $env:IWENCAI_API_KEY" `
|
| 496 |
+
-H "X-Claw-Call-Type: normal" `
|
| 497 |
+
-H "X-Claw-Skill-Id: news-search" `
|
| 498 |
+
-H "X-Claw-Skill-Version: 1.0.0" `
|
| 499 |
+
-H "X-Claw-Plugin-Id: none" `
|
| 500 |
+
-H "X-Claw-Plugin-Version: none" `
|
| 501 |
+
-H "X-Claw-Trace-Id: $traceId" `
|
| 502 |
+
-d '{\"channels\":[\"news\"],\"app_id\":\"AIME_SKILL\",\"query\":\"人工智能\"}'
|
| 503 |
+
|
| 504 |
+
环境变量设置(跨平台):
|
| 505 |
+
Unix/Linux/macOS (bash/zsh):
|
| 506 |
+
export IWENCAI_API_KEY="your_api_key_here"
|
| 507 |
+
|
| 508 |
+
Windows PowerShell:
|
| 509 |
+
$env:IWENCAI_API_KEY="your_api_key_here"
|
| 510 |
+
|
| 511 |
+
Windows CMD:
|
| 512 |
+
set IWENCAI_API_KEY=your_api_key_here
|
| 513 |
+
|
| 514 |
+
获取API密钥:
|
| 515 |
+
1. 访问同花顺i问财SkillHub: https://www.iwencai.com/skillhub
|
| 516 |
+
2. 登录账号
|
| 517 |
+
3. 点击具体技能,在安装方式-Agent用户中查找您的IWENCAI_API_KEY并复制
|
| 518 |
+
4. 按照上述方式设置环境变量
|
| 519 |
+
"""
|
| 520 |
+
)
|
| 521 |
+
|
| 522 |
+
# 输入参数
|
| 523 |
+
input_group = parser.add_mutually_exclusive_group(required=True)
|
| 524 |
+
input_group.add_argument(
|
| 525 |
+
"-q", "--query",
|
| 526 |
+
help="搜索关键词,支持中文"
|
| 527 |
+
)
|
| 528 |
+
input_group.add_argument(
|
| 529 |
+
"-i", "--input",
|
| 530 |
+
help="输入文件路径,每行一个查询词(批量处理)"
|
| 531 |
+
)
|
| 532 |
+
|
| 533 |
+
# 输出参数
|
| 534 |
+
parser.add_argument(
|
| 535 |
+
"-o", "--output",
|
| 536 |
+
help="输出文件路径或目录"
|
| 537 |
+
)
|
| 538 |
+
parser.add_argument(
|
| 539 |
+
"-f", "--format",
|
| 540 |
+
choices=["csv", "json", "text"],
|
| 541 |
+
default="text",
|
| 542 |
+
help="输出格式 (默认: text)"
|
| 543 |
+
)
|
| 544 |
+
|
| 545 |
+
# 搜索参数
|
| 546 |
+
parser.add_argument(
|
| 547 |
+
"-l", "--limit",
|
| 548 |
+
type=int,
|
| 549 |
+
default=10,
|
| 550 |
+
help="每查询返回的最大文章数量 (默认: 10)"
|
| 551 |
+
)
|
| 552 |
+
parser.add_argument(
|
| 553 |
+
"-d", "--days",
|
| 554 |
+
type=int,
|
| 555 |
+
default=30,
|
| 556 |
+
help="搜索最近多少天内的文章 (默认: 30)"
|
| 557 |
+
)
|
| 558 |
+
parser.add_argument(
|
| 559 |
+
"--api-key",
|
| 560 |
+
help="API密钥,如果不提供则从环境变量 IWENCAI_API_KEY 获取"
|
| 561 |
+
)
|
| 562 |
+
|
| 563 |
+
# 其他参数
|
| 564 |
+
parser.add_argument(
|
| 565 |
+
"--debug",
|
| 566 |
+
action="store_true",
|
| 567 |
+
help="启用调试模式"
|
| 568 |
+
)
|
| 569 |
+
|
| 570 |
+
args = parser.parse_args()
|
| 571 |
+
|
| 572 |
+
# 设置日志级别
|
| 573 |
+
if args.debug:
|
| 574 |
+
logging.getLogger().setLevel(logging.DEBUG)
|
| 575 |
+
logger.debug("调试模式已启用")
|
| 576 |
+
|
| 577 |
+
try:
|
| 578 |
+
# 初始化API客户端
|
| 579 |
+
api = NewsSearchAPI(api_key=args.api_key)
|
| 580 |
+
processor = NewsProcessor()
|
| 581 |
+
query_processor = QueryProcessor()
|
| 582 |
+
|
| 583 |
+
# 处理输入
|
| 584 |
+
if args.input:
|
| 585 |
+
# 批量处理模式
|
| 586 |
+
with open(args.input, 'r', encoding='utf-8') as f:
|
| 587 |
+
queries = [line.strip() for line in f if line.strip()]
|
| 588 |
+
|
| 589 |
+
if not queries:
|
| 590 |
+
logger.error("输入文件为空")
|
| 591 |
+
sys.exit(1)
|
| 592 |
+
|
| 593 |
+
logger.info(f"批量处理 {len(queries)} 个查询")
|
| 594 |
+
all_results = {}
|
| 595 |
+
|
| 596 |
+
for query in queries:
|
| 597 |
+
# 拆解复杂查询
|
| 598 |
+
sub_queries = query_processor.split_complex_query(query)
|
| 599 |
+
query_results = []
|
| 600 |
+
|
| 601 |
+
for sub_query in sub_queries:
|
| 602 |
+
try:
|
| 603 |
+
# 获取透明传递的API响应
|
| 604 |
+
response = api.search(sub_query)
|
| 605 |
+
|
| 606 |
+
# 检查是否为错误响应
|
| 607 |
+
if "error" in response:
|
| 608 |
+
# 应用层可以选择性展示关键错误信息
|
| 609 |
+
error_type = response.get("error", "unknown")
|
| 610 |
+
status_code = response.get("status_code", 0)
|
| 611 |
+
raw_response = response.get("raw_response", "")
|
| 612 |
+
|
| 613 |
+
# 对于常见错误(如401),只显示raw_response
|
| 614 |
+
if status_code == 401 and raw_response:
|
| 615 |
+
print(f"\n查询 '{query}' - 子查询 '{sub_query}' API错误 (状态码: {status_code}):")
|
| 616 |
+
print(raw_response)
|
| 617 |
+
else:
|
| 618 |
+
# 其他错误显示完整信息
|
| 619 |
+
print(f"\n查询 '{query}' - 子查询 '{sub_query}' API错误响应:")
|
| 620 |
+
print(json.dumps(response, indent=2, ensure_ascii=False))
|
| 621 |
+
continue
|
| 622 |
+
|
| 623 |
+
# 从响应中提取文章数据(在应用层处理,不在API层)
|
| 624 |
+
articles = response.get("data", [])
|
| 625 |
+
query_results.extend(articles)
|
| 626 |
+
except Exception as e:
|
| 627 |
+
logger.error(f"查询 '{sub_query}' 失败: {str(e)}")
|
| 628 |
+
|
| 629 |
+
all_results[query] = query_results
|
| 630 |
+
|
| 631 |
+
# 处理输出
|
| 632 |
+
if args.output:
|
| 633 |
+
output_dir = Path(args.output)
|
| 634 |
+
if output_dir.suffix: # 是文件
|
| 635 |
+
if args.format == "csv":
|
| 636 |
+
# 合并所有结果到一个CSV文件
|
| 637 |
+
all_articles = []
|
| 638 |
+
for query, articles in all_results.items():
|
| 639 |
+
for article in articles:
|
| 640 |
+
article["original_query"] = query
|
| 641 |
+
all_articles.append(article)
|
| 642 |
+
|
| 643 |
+
processor.save_to_csv(all_articles, str(output_dir))
|
| 644 |
+
elif args.format == "json":
|
| 645 |
+
# 保存为JSON
|
| 646 |
+
output_data = {}
|
| 647 |
+
for query, articles in all_results.items():
|
| 648 |
+
output_data[query] = [
|
| 649 |
+
processor.extract_key_info(article) for article in articles
|
| 650 |
+
]
|
| 651 |
+
|
| 652 |
+
output_dir.parent.mkdir(parents=True, exist_ok=True)
|
| 653 |
+
with open(output_dir, 'w', encoding='utf-8') as f:
|
| 654 |
+
json.dump(output_data, f, ensure_ascii=False, indent=2)
|
| 655 |
+
logger.info(f"已保存批量结果到 {output_dir}")
|
| 656 |
+
else: # 是目录
|
| 657 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 658 |
+
for query, articles in all_results.items():
|
| 659 |
+
# 创建安全的文件名
|
| 660 |
+
safe_filename = "".join(c for c in query if c.isalnum() or c in (' ', '-', '_')).rstrip()
|
| 661 |
+
safe_filename = safe_filename[:50] # 限制文件名长度
|
| 662 |
+
|
| 663 |
+
if args.format == "csv":
|
| 664 |
+
filepath = output_dir / f"{safe_filename}.csv"
|
| 665 |
+
processor.save_to_csv(articles, str(filepath))
|
| 666 |
+
elif args.format == "json":
|
| 667 |
+
filepath = output_dir / f"{safe_filename}.json"
|
| 668 |
+
processor.save_to_json(articles, str(filepath))
|
| 669 |
+
|
| 670 |
+
# 文本输出
|
| 671 |
+
if args.format == "text" or not args.output:
|
| 672 |
+
for query, articles in all_results.items():
|
| 673 |
+
print(f"\n{'='*60}")
|
| 674 |
+
print(f"查询: {query}")
|
| 675 |
+
print(f"{'='*60}")
|
| 676 |
+
|
| 677 |
+
if not articles:
|
| 678 |
+
print("未找到相关文章")
|
| 679 |
+
continue
|
| 680 |
+
|
| 681 |
+
# 处理文章
|
| 682 |
+
articles = processor.filter_by_date(articles, args.days)
|
| 683 |
+
articles = processor.sort_by_date(articles)
|
| 684 |
+
articles = processor.limit_results(articles, args.limit)
|
| 685 |
+
|
| 686 |
+
for i, article in enumerate(articles, 1):
|
| 687 |
+
print(f"\n{i}. {article.get('title', '无标题')}")
|
| 688 |
+
print(f" 摘要: {article.get('summary', '无摘要')}")
|
| 689 |
+
print(f" 发布时间: {article.get('publish_date', '未知时间')}")
|
| 690 |
+
print(f" 链接: {article.get('url', '无链接')}")
|
| 691 |
+
|
| 692 |
+
else:
|
| 693 |
+
# 单查询模式
|
| 694 |
+
query = args.query
|
| 695 |
+
|
| 696 |
+
# 检查是否从管道读取
|
| 697 |
+
if not sys.stdin.isatty() and not query:
|
| 698 |
+
query = sys.stdin.read().strip()
|
| 699 |
+
|
| 700 |
+
if not query:
|
| 701 |
+
logger.error("未提供查询关键词")
|
| 702 |
+
parser.print_help()
|
| 703 |
+
sys.exit(1)
|
| 704 |
+
|
| 705 |
+
logger.info(f"处理查询: {query}")
|
| 706 |
+
|
| 707 |
+
# 拆解复杂查询
|
| 708 |
+
sub_queries = query_processor.split_complex_query(query)
|
| 709 |
+
all_articles = []
|
| 710 |
+
|
| 711 |
+
for sub_query in sub_queries:
|
| 712 |
+
try:
|
| 713 |
+
# 获取透明传递的API响应
|
| 714 |
+
response = api.search(sub_query)
|
| 715 |
+
|
| 716 |
+
# 检查是否为错误响应
|
| 717 |
+
if "error" in response:
|
| 718 |
+
# 应用层可以选择性展示关键错误信息
|
| 719 |
+
error_type = response.get("error", "unknown")
|
| 720 |
+
status_code = response.get("status_code", 0)
|
| 721 |
+
raw_response = response.get("raw_response", "")
|
| 722 |
+
|
| 723 |
+
# 对于常见错误(如401),只显示raw_response
|
| 724 |
+
if status_code == 401 and raw_response:
|
| 725 |
+
print(f"\n{'='*60}")
|
| 726 |
+
print(f"API错误 (状态码: {status_code}):")
|
| 727 |
+
print(f"{'='*60}")
|
| 728 |
+
print(raw_response)
|
| 729 |
+
print(f"{'='*60}")
|
| 730 |
+
else:
|
| 731 |
+
# 其他错误显示完整信息
|
| 732 |
+
print(f"\n{'='*60}")
|
| 733 |
+
print(f"API错误响应(透明传递):")
|
| 734 |
+
print(f"{'='*60}")
|
| 735 |
+
print(json.dumps(response, indent=2, ensure_ascii=False))
|
| 736 |
+
print(f"{'='*60}")
|
| 737 |
+
continue
|
| 738 |
+
|
| 739 |
+
# 从响应中提取文章数据(在应用层处理,不在API层)
|
| 740 |
+
articles = response.get("data", [])
|
| 741 |
+
all_articles.extend(articles)
|
| 742 |
+
logger.info(f"子查询 '{sub_query}' 找到 {len(articles)} 篇文章")
|
| 743 |
+
except Exception as e:
|
| 744 |
+
logger.error(f"子查询 '{sub_query}' 失败: {str(e)}")
|
| 745 |
+
|
| 746 |
+
# 评估结果
|
| 747 |
+
if not query_processor.evaluate_results(all_articles):
|
| 748 |
+
logger.warning("搜索结果可能不足以完整回答问题")
|
| 749 |
+
|
| 750 |
+
# 处理文章
|
| 751 |
+
all_articles = processor.filter_by_date(all_articles, args.days)
|
| 752 |
+
all_articles = processor.sort_by_date(all_articles)
|
| 753 |
+
all_articles = processor.limit_results(all_articles, args.limit)
|
| 754 |
+
|
| 755 |
+
# 处理输出
|
| 756 |
+
if args.output:
|
| 757 |
+
if args.format == "csv":
|
| 758 |
+
processor.save_to_csv(all_articles, args.output)
|
| 759 |
+
elif args.format == "json":
|
| 760 |
+
processor.save_to_json(all_articles, args.output)
|
| 761 |
+
|
| 762 |
+
# 文本输出
|
| 763 |
+
if args.format == "text" or not args.output:
|
| 764 |
+
print(f"\n{'='*60}")
|
| 765 |
+
print(f"查询: {query}")
|
| 766 |
+
print(f"找到 {len(all_articles)} 篇文章 (最近 {args.days} 天)")
|
| 767 |
+
print(f"{'='*60}")
|
| 768 |
+
|
| 769 |
+
if not all_articles:
|
| 770 |
+
print("未找到相关文章")
|
| 771 |
+
else:
|
| 772 |
+
for i, article in enumerate(all_articles, 1):
|
| 773 |
+
print(f"\n{i}. {article.get('title', '无标题')}")
|
| 774 |
+
print(f" 摘要: {article.get('summary', '无摘要')}")
|
| 775 |
+
print(f" 发布时间: {article.get('publish_date', '未知时间')}")
|
| 776 |
+
print(f" 链接: {article.get('url', '无链接')}")
|
| 777 |
+
|
| 778 |
+
logger.info("新闻搜索完成")
|
| 779 |
+
|
| 780 |
+
except ValueError as e:
|
| 781 |
+
logger.error(f"参数错误: {str(e)}")
|
| 782 |
+
sys.exit(1)
|
| 783 |
+
except Exception as e:
|
| 784 |
+
logger.error(f"程序执行错误: {str(e)}")
|
| 785 |
+
if args.debug:
|
| 786 |
+
import traceback
|
| 787 |
+
traceback.print_exc()
|
| 788 |
+
sys.exit(1)
|
| 789 |
+
|
| 790 |
+
|
| 791 |
+
if __name__ == "__main__":
|
| 792 |
+
main()
|
Skills/news-search/scripts/requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 新闻搜索技能依赖
|
| 2 |
+
requests>=2.25.0
|
Skills/news-search/scripts/setup.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
新闻搜索技能安装脚本
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from setuptools import setup, find_packages
|
| 8 |
+
|
| 9 |
+
with open("README.md", "r", encoding="utf-8") as fh:
|
| 10 |
+
long_description = fh.read()
|
| 11 |
+
|
| 12 |
+
setup(
|
| 13 |
+
name="news-search-skill",
|
| 14 |
+
version="1.0.0",
|
| 15 |
+
author="News Search Skill",
|
| 16 |
+
author_email="",
|
| 17 |
+
description="财经领域资讯搜索引擎,调用同花顺问财的财经资讯搜索接口",
|
| 18 |
+
long_description=long_description,
|
| 19 |
+
long_description_content_type="text/markdown",
|
| 20 |
+
url="",
|
| 21 |
+
packages=find_packages(),
|
| 22 |
+
classifiers=[
|
| 23 |
+
"Programming Language :: Python :: 3",
|
| 24 |
+
"Programming Language :: Python :: 3.7",
|
| 25 |
+
"Programming Language :: Python :: 3.8",
|
| 26 |
+
"Programming Language :: Python :: 3.9",
|
| 27 |
+
"Programming Language :: Python :: 3.10",
|
| 28 |
+
"Programming Language :: Python :: 3.11",
|
| 29 |
+
"License :: OSI Approved :: MIT License",
|
| 30 |
+
"Operating System :: OS Independent",
|
| 31 |
+
],
|
| 32 |
+
python_requires=">=3.7",
|
| 33 |
+
install_requires=[
|
| 34 |
+
"requests>=2.25.0",
|
| 35 |
+
],
|
| 36 |
+
entry_points={
|
| 37 |
+
"console_scripts": [
|
| 38 |
+
"news-search=.trae.skills.新闻搜索.news_search:main",
|
| 39 |
+
],
|
| 40 |
+
},
|
| 41 |
+
)
|
Skills/news-search/scripts/test_basic.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
新闻搜索技能基本测试
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import json
|
| 10 |
+
import tempfile
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
# 添加当前目录到Python路径
|
| 14 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 15 |
+
|
| 16 |
+
from news_search import NewsSearchAPI, NewsProcessor, QueryProcessor
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def test_query_processor():
|
| 20 |
+
"""测试查询处理器"""
|
| 21 |
+
print("测试查询处理器...")
|
| 22 |
+
|
| 23 |
+
processor = QueryProcessor()
|
| 24 |
+
|
| 25 |
+
# 测试简单查询
|
| 26 |
+
simple_query = "人工智能"
|
| 27 |
+
result = processor.split_complex_query(simple_query)
|
| 28 |
+
print(f"简单查询 '{simple_query}' -> {result}")
|
| 29 |
+
assert result == ["人工智能"], f"期望 ['人工智能'],得到 {result}"
|
| 30 |
+
|
| 31 |
+
# 测试复杂查询
|
| 32 |
+
complex_query = "人工智能和芯片行业最新动态"
|
| 33 |
+
result = processor.split_complex_query(complex_query)
|
| 34 |
+
print(f"复杂查询 '{complex_query}' -> {result}")
|
| 35 |
+
assert len(result) >= 2, f"期望至少2个子查询,得到 {len(result)}"
|
| 36 |
+
|
| 37 |
+
# 测试更多连接词
|
| 38 |
+
test_cases = [
|
| 39 |
+
("A与B", ["A", "B"]),
|
| 40 |
+
("A及B", ["A", "B"]),
|
| 41 |
+
("A以及B", ["A", "B"]),
|
| 42 |
+
("A还有B", ["A", "B"]),
|
| 43 |
+
("A并且B", ["A", "B"]),
|
| 44 |
+
("A同时B", ["A", "B"]),
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
for query, expected_prefix in test_cases:
|
| 48 |
+
result = processor.split_complex_query(query)
|
| 49 |
+
print(f"连接词测试 '{query}' -> {result}")
|
| 50 |
+
assert len(result) >= 2, f"期望至少2个子查询,得到 {len(result)}"
|
| 51 |
+
|
| 52 |
+
print("查询处理器测试通过!\n")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_news_processor():
|
| 56 |
+
"""测试新闻处理器"""
|
| 57 |
+
print("测试新闻处理器...")
|
| 58 |
+
|
| 59 |
+
processor = NewsProcessor()
|
| 60 |
+
|
| 61 |
+
# 测试数据 - 使用当前日期附近的日期
|
| 62 |
+
from datetime import datetime, timedelta
|
| 63 |
+
now = datetime.now()
|
| 64 |
+
|
| 65 |
+
test_articles = [
|
| 66 |
+
{
|
| 67 |
+
"title": "文章1",
|
| 68 |
+
"summary": "摘要1",
|
| 69 |
+
"url": "http://example.com/1",
|
| 70 |
+
"publish_date": (now - timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S")
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"title": "文章2",
|
| 74 |
+
"summary": "摘要2",
|
| 75 |
+
"url": "http://example.com/2",
|
| 76 |
+
"publish_date": (now - timedelta(days=5)).strftime("%Y-%m-%d %H:%M:%S")
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"title": "文章3",
|
| 80 |
+
"summary": "摘要3",
|
| 81 |
+
"url": "http://example.com/3",
|
| 82 |
+
"publish_date": (now - timedelta(days=40)).strftime("%Y-%m-%d %H:%M:%S")
|
| 83 |
+
}
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
# 测试时间过滤
|
| 87 |
+
filtered = processor.filter_by_date(test_articles, days=30)
|
| 88 |
+
print(f"时间过滤: 从 {len(test_articles)} 篇文章中过滤出 {len(filtered)} 篇最近30天的文章")
|
| 89 |
+
assert len(filtered) >= 2, f"期望至少2篇文章,得到 {len(filtered)}"
|
| 90 |
+
|
| 91 |
+
# 测试排序
|
| 92 |
+
sorted_articles = processor.sort_by_date(test_articles, reverse=True)
|
| 93 |
+
print("排序测试: 文章按时间从新到旧排序")
|
| 94 |
+
|
| 95 |
+
# 测试数量限制
|
| 96 |
+
limited = processor.limit_results(test_articles, 2)
|
| 97 |
+
print(f"数量限制: 限制为2篇文章,得到 {len(limited)} 篇")
|
| 98 |
+
assert len(limited) == 2, f"期望2篇文章,得到 {len(limited)}"
|
| 99 |
+
|
| 100 |
+
# 测试关键信息提取
|
| 101 |
+
key_info = processor.extract_key_info(test_articles[0])
|
| 102 |
+
print(f"关键信息提取: {key_info}")
|
| 103 |
+
assert "title" in key_info
|
| 104 |
+
assert "source" in key_info
|
| 105 |
+
assert key_info["source"] == "同花顺问财"
|
| 106 |
+
|
| 107 |
+
# 测试文件保存
|
| 108 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 109 |
+
# 测试CSV保存
|
| 110 |
+
csv_path = Path(tmpdir) / "test.csv"
|
| 111 |
+
processor.save_to_csv(test_articles, str(csv_path))
|
| 112 |
+
print(f"CSV保存测试: 文件已保存到 {csv_path}")
|
| 113 |
+
assert csv_path.exists()
|
| 114 |
+
|
| 115 |
+
# 测试JSON保存
|
| 116 |
+
json_path = Path(tmpdir) / "test.json"
|
| 117 |
+
processor.save_to_json(test_articles, str(json_path))
|
| 118 |
+
print(f"JSON保存测试: 文件已保存到 {json_path}")
|
| 119 |
+
assert json_path.exists()
|
| 120 |
+
|
| 121 |
+
# 验证JSON内容
|
| 122 |
+
with open(json_path, 'r', encoding='utf-8') as f:
|
| 123 |
+
loaded_data = json.load(f)
|
| 124 |
+
assert len(loaded_data) == len(test_articles)
|
| 125 |
+
assert loaded_data[0]["source"] == "同花顺问财"
|
| 126 |
+
|
| 127 |
+
print("新闻处理器测试通过!\n")
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def test_api_client_structure():
|
| 131 |
+
"""测试API客户端结构(不实际调用API)"""
|
| 132 |
+
print("测试API客户端结构...")
|
| 133 |
+
|
| 134 |
+
# 测试初始化
|
| 135 |
+
try:
|
| 136 |
+
# 应该抛出错误,因为没有API密钥
|
| 137 |
+
api = NewsSearchAPI()
|
| 138 |
+
print("警告: API客户端初始化应该失败(无API密钥)")
|
| 139 |
+
except ValueError as e:
|
| 140 |
+
print(f"预期错误: {str(e)}")
|
| 141 |
+
|
| 142 |
+
# 测试带API密钥的初始化
|
| 143 |
+
test_api_key = "test_key_123"
|
| 144 |
+
api = NewsSearchAPI(api_key=test_api_key)
|
| 145 |
+
|
| 146 |
+
# 检查属性
|
| 147 |
+
assert api.base_url == "https://openapi.iwencai.com"
|
| 148 |
+
assert api.endpoint == "/v1/comprehensive/search"
|
| 149 |
+
assert api.api_key == test_api_key
|
| 150 |
+
|
| 151 |
+
print("API客户端结构测试通过!\n")
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def test_config_module():
|
| 155 |
+
"""测试配置模块"""
|
| 156 |
+
print("测试配置模块...")
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
+
from config import Config, get_config
|
| 160 |
+
|
| 161 |
+
# 测试默认配置
|
| 162 |
+
config = Config()
|
| 163 |
+
|
| 164 |
+
# 检查默认值
|
| 165 |
+
api_config = config.get_api_config()
|
| 166 |
+
assert api_config["base_url"] == "https://openapi.iwencai.com"
|
| 167 |
+
assert api_config["timeout"] == 30
|
| 168 |
+
|
| 169 |
+
search_config = config.get_search_config()
|
| 170 |
+
assert search_config["default_limit"] == 10
|
| 171 |
+
assert search_config["default_days"] == 30
|
| 172 |
+
|
| 173 |
+
print("配置模块测试通过!")
|
| 174 |
+
|
| 175 |
+
except ImportError as e:
|
| 176 |
+
print(f"配置模块导入失败: {str(e)}")
|
| 177 |
+
print("跳过配置模块测试")
|
| 178 |
+
|
| 179 |
+
print()
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def run_all_tests():
|
| 183 |
+
"""运行所有测试"""
|
| 184 |
+
print("=" * 60)
|
| 185 |
+
print("新闻搜索技能测试套件")
|
| 186 |
+
print("=" * 60)
|
| 187 |
+
print()
|
| 188 |
+
|
| 189 |
+
tests = [
|
| 190 |
+
test_query_processor,
|
| 191 |
+
test_news_processor,
|
| 192 |
+
test_api_client_structure,
|
| 193 |
+
test_config_module,
|
| 194 |
+
]
|
| 195 |
+
|
| 196 |
+
passed = 0
|
| 197 |
+
failed = 0
|
| 198 |
+
|
| 199 |
+
for test_func in tests:
|
| 200 |
+
try:
|
| 201 |
+
test_func()
|
| 202 |
+
passed += 1
|
| 203 |
+
except AssertionError as e:
|
| 204 |
+
print(f"测试失败: {str(e)}")
|
| 205 |
+
failed += 1
|
| 206 |
+
except Exception as e:
|
| 207 |
+
print(f"测试异常: {type(e).__name__}: {str(e)}")
|
| 208 |
+
failed += 1
|
| 209 |
+
|
| 210 |
+
print("=" * 60)
|
| 211 |
+
print(f"测试结果: 通过 {passed},失败 {failed}")
|
| 212 |
+
print("=" * 60)
|
| 213 |
+
|
| 214 |
+
if failed == 0:
|
| 215 |
+
print("所有测试通过!")
|
| 216 |
+
return 0
|
| 217 |
+
else:
|
| 218 |
+
print(f"{failed} 个测试失败")
|
| 219 |
+
return 1
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
if __name__ == "__main__":
|
| 223 |
+
sys.exit(run_all_tests())
|
Skills/news-search/scripts/test_queries.txt
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
人工智能
|
| 2 |
+
芯片行业
|
| 3 |
+
新能源汽车
|
| 4 |
+
央行货币政策
|
| 5 |
+
腾讯公司
|
| 6 |
+
阿里巴巴
|
| 7 |
+
美团
|
| 8 |
+
京东
|
| 9 |
+
拼多多
|
| 10 |
+
字节跳动
|
| 11 |
+
百度
|
| 12 |
+
华为
|
| 13 |
+
小米
|
| 14 |
+
苹果公司
|
| 15 |
+
特斯拉
|
| 16 |
+
宁德时代
|
| 17 |
+
比亚迪
|
| 18 |
+
隆基绿能
|
| 19 |
+
药明康德
|
| 20 |
+
恒瑞医药
|
| 21 |
+
贵州茅台
|
| 22 |
+
五粮液
|
| 23 |
+
中国平安
|
| 24 |
+
招商银行
|
| 25 |
+
工商银行
|
| 26 |
+
建设银行
|
| 27 |
+
农业银行
|
| 28 |
+
中国银行
|
| 29 |
+
中信证券
|
| 30 |
+
海通证券
|
| 31 |
+
国泰君安
|
| 32 |
+
东方财富
|
| 33 |
+
同花顺
|
| 34 |
+
大智慧
|
| 35 |
+
万得资讯
|
| 36 |
+
金融科技
|
| 37 |
+
数字货币
|
| 38 |
+
区块链
|
| 39 |
+
元宇宙
|
| 40 |
+
5G技术
|
| 41 |
+
6G技术
|
| 42 |
+
云计算
|
| 43 |
+
大数据
|
| 44 |
+
物联网
|
| 45 |
+
工业互联网
|
| 46 |
+
智能制造
|
| 47 |
+
机器人
|
| 48 |
+
自动驾驶
|
| 49 |
+
无人机
|
| 50 |
+
航空航天
|
| 51 |
+
生物医药
|
| 52 |
+
医疗器械
|
| 53 |
+
医疗服务
|
| 54 |
+
健康管理
|
| 55 |
+
养老服务
|
| 56 |
+
教育培训
|
| 57 |
+
在线教育
|
| 58 |
+
职业教育
|
| 59 |
+
文化旅游
|
| 60 |
+
体育产业
|
| 61 |
+
电影产业
|
| 62 |
+
游戏产业
|
| 63 |
+
动漫产业
|
| 64 |
+
短视频
|
| 65 |
+
直播电商
|
| 66 |
+
社交电商
|
| 67 |
+
跨境电商
|
| 68 |
+
物流快递
|
| 69 |
+
共享经济
|
| 70 |
+
新能源
|
| 71 |
+
光伏发电
|
| 72 |
+
风力发电
|
| 73 |
+
水力发电
|
| 74 |
+
核电技术
|
| 75 |
+
储能技术
|
| 76 |
+
氢能源
|
| 77 |
+
碳交易
|
| 78 |
+
碳中和
|
| 79 |
+
环境保护
|
| 80 |
+
污染治理
|
| 81 |
+
垃圾分类
|
| 82 |
+
水资源
|
| 83 |
+
农业科技
|
| 84 |
+
粮食安全
|
| 85 |
+
乡村振兴
|
| 86 |
+
城镇化
|
| 87 |
+
房地产
|
| 88 |
+
基础设施建设
|
| 89 |
+
交通运输
|
| 90 |
+
铁路建设
|
| 91 |
+
公路建设
|
| 92 |
+
机场建设
|
| 93 |
+
港口建设
|
| 94 |
+
一带一路
|
| 95 |
+
京津冀
|
| 96 |
+
长三角
|
| 97 |
+
粤港澳大湾区
|
| 98 |
+
成渝经济圈
|
| 99 |
+
长江经济带
|
| 100 |
+
黄河流域
|
| 101 |
+
东北振兴
|
| 102 |
+
西部开发
|
| 103 |
+
中部崛起
|
| 104 |
+
东部率先
|
| 105 |
+
自由贸易区
|
| 106 |
+
经济特区
|
| 107 |
+
开发区
|
| 108 |
+
高新区
|
| 109 |
+
经开区
|
| 110 |
+
自贸试验区
|
| 111 |
+
综合保税区
|
| 112 |
+
口岸经济
|
| 113 |
+
边境贸易
|
| 114 |
+
国际贸易
|
| 115 |
+
进出口
|
| 116 |
+
外商投资
|
| 117 |
+
对外投资
|
| 118 |
+
国际合作
|
| 119 |
+
全球治理
|
| 120 |
+
世界经济
|
| 121 |
+
国际金融
|
| 122 |
+
国际货币
|
| 123 |
+
汇率变动
|
| 124 |
+
利率政策
|
| 125 |
+
货币政策
|
| 126 |
+
财政政策
|
| 127 |
+
税收政策
|
| 128 |
+
产业政策
|
| 129 |
+
科技政策
|
| 130 |
+
人才政策
|
| 131 |
+
就业政策
|
| 132 |
+
社保政策
|
| 133 |
+
医保政策
|
| 134 |
+
教育政策
|
| 135 |
+
住房政策
|
| 136 |
+
养老政策
|
| 137 |
+
生育政策
|
| 138 |
+
人口政策
|
| 139 |
+
户籍政策
|
| 140 |
+
土地政策
|
| 141 |
+
环保政策
|
| 142 |
+
能源政策
|
| 143 |
+
交通政策
|
| 144 |
+
通信政策
|
| 145 |
+
互联网政策
|
| 146 |
+
数据政策
|
| 147 |
+
人工智能政策
|
| 148 |
+
芯片政策
|
| 149 |
+
新能源汽车政策
|
| 150 |
+
生物医药政策
|
| 151 |
+
金融监管
|
| 152 |
+
证券监管
|
| 153 |
+
银行监管
|
| 154 |
+
保险监管
|
| 155 |
+
支付监管
|
| 156 |
+
反垄断
|
| 157 |
+
反不正当竞争
|
| 158 |
+
消费者权益
|
| 159 |
+
知识产权
|
| 160 |
+
专利保护
|
| 161 |
+
商标保护
|
| 162 |
+
著作权保护
|
| 163 |
+
商业秘密
|
| 164 |
+
网络安全
|
| 165 |
+
数据安全
|
| 166 |
+
个人信息保护
|
| 167 |
+
国家安全
|
| 168 |
+
国防建设
|
| 169 |
+
军队改革
|
| 170 |
+
军民融合
|
| 171 |
+
外交政策
|
| 172 |
+
国际关系
|
| 173 |
+
中美关系
|
| 174 |
+
中欧关系
|
| 175 |
+
中日关系
|
| 176 |
+
中俄关系
|
| 177 |
+
中国东盟关系
|
| 178 |
+
一带一路合作
|
| 179 |
+
金砖国家
|
| 180 |
+
上合组织
|
| 181 |
+
G20峰会
|
| 182 |
+
APEC会议
|
| 183 |
+
达沃斯论坛
|
| 184 |
+
博鳌论坛
|
| 185 |
+
联合国
|
| 186 |
+
世贸组织
|
| 187 |
+
国际货币基金组织
|
| 188 |
+
世界银行
|
| 189 |
+
亚洲开发银行
|
| 190 |
+
亚投行
|
| 191 |
+
新开发银行
|
| 192 |
+
气候变化
|
| 193 |
+
环境保护
|
| 194 |
+
生物多样性
|
| 195 |
+
可持续发展
|
| 196 |
+
减贫脱贫
|
| 197 |
+
共同富裕
|
| 198 |
+
小康社会
|
| 199 |
+
社会主义现代化
|
| 200 |
+
中华民族伟大复兴
|
| 201 |
+
中国梦
|
| 202 |
+
中国共产党
|
| 203 |
+
中国政府
|
| 204 |
+
中国人民
|
| 205 |
+
中国特色社会主义
|
| 206 |
+
改革开放
|
| 207 |
+
市场经济
|
| 208 |
+
计划经济
|
| 209 |
+
混合所有制
|
| 210 |
+
国有企业
|
| 211 |
+
民营企业
|
| 212 |
+
外资企业
|
| 213 |
+
中小企业
|
| 214 |
+
个体工商户
|
| 215 |
+
农民专业合作社
|
| 216 |
+
家庭农场
|
| 217 |
+
农业企业
|
| 218 |
+
工业企业
|
| 219 |
+
服务业企业
|
| 220 |
+
高科技企业
|
| 221 |
+
创新企业
|
| 222 |
+
创业企业
|
| 223 |
+
上市公司
|
| 224 |
+
新三板
|
| 225 |
+
科创板
|
| 226 |
+
创业板
|
| 227 |
+
主板市场
|
| 228 |
+
债券市场
|
| 229 |
+
期货市场
|
| 230 |
+
外汇市场
|
| 231 |
+
黄金市场
|
| 232 |
+
大宗商品
|
| 233 |
+
原油价格
|
| 234 |
+
铁矿石价格
|
| 235 |
+
铜价
|
| 236 |
+
铝价
|
| 237 |
+
锌价
|
| 238 |
+
镍价
|
| 239 |
+
锂价
|
| 240 |
+
钴价
|
| 241 |
+
稀土价格
|
| 242 |
+
粮食价格
|
| 243 |
+
猪肉价格
|
| 244 |
+
蔬菜价格
|
| 245 |
+
水果价格
|
| 246 |
+
房价
|
| 247 |
+
地价
|
| 248 |
+
租金
|
| 249 |
+
工资
|
| 250 |
+
物价
|
| 251 |
+
CPI
|
| 252 |
+
PPI
|
| 253 |
+
GDP
|
| 254 |
+
GNP
|
| 255 |
+
GNI
|
| 256 |
+
财政收入
|
| 257 |
+
财政支出
|
| 258 |
+
财政赤字
|
| 259 |
+
国债
|
| 260 |
+
地方债
|
| 261 |
+
企业债
|
| 262 |
+
公司债
|
| 263 |
+
金融债
|
| 264 |
+
资产证券化
|
| 265 |
+
REITs
|
| 266 |
+
ETF
|
| 267 |
+
LOF
|
| 268 |
+
基金
|
| 269 |
+
股票
|
| 270 |
+
债券
|
| 271 |
+
期货
|
| 272 |
+
期权
|
| 273 |
+
外汇
|
| 274 |
+
黄金
|
| 275 |
+
原油
|
| 276 |
+
数字货币
|
| 277 |
+
比特币
|
| 278 |
+
以太坊
|
| 279 |
+
区块链
|
| 280 |
+
DeFi
|
| 281 |
+
NFT
|
| 282 |
+
Web3
|
| 283 |
+
元宇宙
|
| 284 |
+
人工智能
|
| 285 |
+
机器学习
|
| 286 |
+
深度学习
|
| 287 |
+
自然语言处理
|
| 288 |
+
计算机视觉
|
| 289 |
+
语音识别
|
| 290 |
+
机器人技术
|
| 291 |
+
自动驾驶
|
| 292 |
+
无人机
|
| 293 |
+
智能家居
|
| 294 |
+
智能医疗
|
| 295 |
+
智能教育
|
| 296 |
+
智能交通
|
| 297 |
+
智能城市
|
| 298 |
+
智能制造
|
| 299 |
+
工业互联网
|
| 300 |
+
物联网
|
| 301 |
+
5G
|
| 302 |
+
6G
|
| 303 |
+
卫星互联网
|
| 304 |
+
量子计算
|
| 305 |
+
量子通信
|
| 306 |
+
量子传感
|
| 307 |
+
生物技术
|
| 308 |
+
基因编辑
|
| 309 |
+
细胞治疗
|
| 310 |
+
合成生物学
|
| 311 |
+
生物制药
|
| 312 |
+
医疗器械
|
| 313 |
+
数字医疗
|
| 314 |
+
远程医疗
|
| 315 |
+
互联网医疗
|
| 316 |
+
健康管理
|
| 317 |
+
养老服务
|
| 318 |
+
教育培训
|
| 319 |
+
在线教育
|
| 320 |
+
职业教育
|
| 321 |
+
技能培训
|
| 322 |
+
文化旅游
|
| 323 |
+
体育产业
|
| 324 |
+
电影产业
|
| 325 |
+
游戏产业
|
| 326 |
+
动漫产业
|
| 327 |
+
短视频
|
| 328 |
+
直播
|
| 329 |
+
社交
|
| 330 |
+
电商
|
| 331 |
+
物流
|
| 332 |
+
快递
|
| 333 |
+
外卖
|
| 334 |
+
出行
|
| 335 |
+
住宿
|
| 336 |
+
餐饮
|
| 337 |
+
零售
|
| 338 |
+
批发
|
| 339 |
+
制造
|
| 340 |
+
建筑
|
| 341 |
+
房地产
|
| 342 |
+
金融
|
| 343 |
+
保险
|
| 344 |
+
证券
|
| 345 |
+
银行
|
| 346 |
+
信托
|
| 347 |
+
租赁
|
| 348 |
+
担保
|
| 349 |
+
典当
|
| 350 |
+
拍卖
|
| 351 |
+
评估
|
| 352 |
+
会计
|
| 353 |
+
审计
|
| 354 |
+
税务
|
| 355 |
+
法律
|
| 356 |
+
咨询
|
| 357 |
+
设计
|
| 358 |
+
研发
|
| 359 |
+
测试
|
| 360 |
+
运维
|
| 361 |
+
运营
|
| 362 |
+
市场
|
| 363 |
+
销售
|
| 364 |
+
客服
|
| 365 |
+
人力资源
|
| 366 |
+
行政
|
| 367 |
+
财务
|
| 368 |
+
采购
|
| 369 |
+
生产
|
| 370 |
+
质量
|
| 371 |
+
安全
|
| 372 |
+
环保
|
| 373 |
+
能源
|
| 374 |
+
交通
|
| 375 |
+
通信
|
| 376 |
+
IT
|
| 377 |
+
互联网
|
| 378 |
+
软件
|
| 379 |
+
硬件
|
| 380 |
+
芯片
|
| 381 |
+
半导体
|
| 382 |
+
显示面板
|
| 383 |
+
电池
|
| 384 |
+
电机
|
| 385 |
+
电控
|
| 386 |
+
汽车
|
| 387 |
+
飞机
|
| 388 |
+
船舶
|
| 389 |
+
火车
|
| 390 |
+
地铁
|
| 391 |
+
公交
|
| 392 |
+
出租车
|
| 393 |
+
共享单车
|
| 394 |
+
共享汽车
|
| 395 |
+
物流车
|
| 396 |
+
货车
|
| 397 |
+
客车
|
| 398 |
+
轿车
|
| 399 |
+
SUV
|
| 400 |
+
MPV
|
| 401 |
+
电动车
|
| 402 |
+
混动车
|
| 403 |
+
燃油车
|
| 404 |
+
氢燃料车
|
| 405 |
+
太阳能车
|
| 406 |
+
飞行汽车
|
| 407 |
+
无人机配送
|
| 408 |
+
机器人配送
|
| 409 |
+
智���仓储
|
| 410 |
+
智能物流
|
| 411 |
+
智慧港口
|
| 412 |
+
智慧机场
|
| 413 |
+
智慧车站
|
| 414 |
+
智慧公路
|
| 415 |
+
智慧铁路
|
| 416 |
+
智慧地铁
|
| 417 |
+
智慧公交
|
| 418 |
+
智慧出租车
|
| 419 |
+
智慧停车
|
| 420 |
+
智慧交通
|
| 421 |
+
智慧城市
|
| 422 |
+
智慧社区
|
| 423 |
+
智慧家庭
|
| 424 |
+
智慧办公
|
| 425 |
+
智慧工厂
|
| 426 |
+
智慧农场
|
| 427 |
+
智慧医院
|
| 428 |
+
智慧学校
|
| 429 |
+
智慧政府
|
| 430 |
+
智慧法院
|
| 431 |
+
智慧检察院
|
| 432 |
+
智慧公安
|
| 433 |
+
智慧消防
|
| 434 |
+
智慧应急
|
| 435 |
+
智慧环保
|
| 436 |
+
智慧能源
|
| 437 |
+
智慧水利
|
| 438 |
+
智慧林业
|
| 439 |
+
智慧草原
|
| 440 |
+
智慧海洋
|
| 441 |
+
智慧天空
|
| 442 |
+
智慧地球
|
| 443 |
+
数字地球
|
| 444 |
+
元宇宙地球
|
| 445 |
+
虚拟现实
|
| 446 |
+
增强现实
|
| 447 |
+
混合现实
|
| 448 |
+
扩展现实
|
| 449 |
+
脑机接口
|
| 450 |
+
人机交互
|
| 451 |
+
语音交互
|
| 452 |
+
手势交互
|
| 453 |
+
眼动交互
|
| 454 |
+
脑电交互
|
| 455 |
+
肌电交互
|
| 456 |
+
心电交互
|
| 457 |
+
血压监测
|
| 458 |
+
血糖监测
|
| 459 |
+
血氧监测
|
| 460 |
+
体温监测
|
| 461 |
+
呼吸监测
|
| 462 |
+
心率监测
|
| 463 |
+
睡眠监测
|
| 464 |
+
运动监测
|
| 465 |
+
饮食监测
|
| 466 |
+
情绪监测
|
| 467 |
+
压力监测
|
| 468 |
+
疲劳监测
|
| 469 |
+
健康评估
|
| 470 |
+
疾病预测
|
| 471 |
+
早期筛查
|
| 472 |
+
精准诊断
|
| 473 |
+
个性化治疗
|
| 474 |
+
康复训练
|
| 475 |
+
长期照护
|
| 476 |
+
临终关怀
|
| 477 |
+
生命科学
|
| 478 |
+
医学研究
|
| 479 |
+
药物研发
|
| 480 |
+
临床试验
|
| 481 |
+
药品生产
|
| 482 |
+
药品流通
|
| 483 |
+
药品使用
|
| 484 |
+
药品监管
|
| 485 |
+
医疗器械研发
|
| 486 |
+
医疗器械生产
|
| 487 |
+
医疗器械流通
|
| 488 |
+
医疗器械使用
|
| 489 |
+
医疗器械监管
|
| 490 |
+
医疗服务提供
|
| 491 |
+
医疗服务支付
|
| 492 |
+
医疗服务监管
|
| 493 |
+
医疗数据管理
|
| 494 |
+
医疗信息安全
|
| 495 |
+
医疗人工智能
|
| 496 |
+
医疗机器人
|
| 497 |
+
医疗3D打印
|
| 498 |
+
医疗VR/AR
|
| 499 |
+
医疗物联网
|
| 500 |
+
医疗区块链
|
| 501 |
+
医疗云计算
|
| 502 |
+
医疗大数据
|
| 503 |
+
医疗5G
|
| 504 |
+
远程手术
|
| 505 |
+
远程会诊
|
| 506 |
+
远程监护
|
| 507 |
+
远程康复
|
| 508 |
+
互联网医院
|
| 509 |
+
数字医院
|
| 510 |
+
智慧医院
|
| 511 |
+
未来医院
|
| 512 |
+
健康中国
|
| 513 |
+
美丽中国
|
| 514 |
+
平安中国
|
| 515 |
+
法治中国
|
| 516 |
+
数字中国
|
| 517 |
+
创新中国
|
| 518 |
+
科技强国
|
| 519 |
+
制造强国
|
| 520 |
+
质量强国
|
| 521 |
+
网络强国
|
| 522 |
+
交通强国
|
| 523 |
+
海洋强国
|
| 524 |
+
航天强国
|
| 525 |
+
文化强国
|
| 526 |
+
教育强国
|
| 527 |
+
人才强国
|
| 528 |
+
体育强国
|
| 529 |
+
健康强国
|
| 530 |
+
平安强国
|
| 531 |
+
法治强国
|
| 532 |
+
数字强国
|
| 533 |
+
创新强国
|
| 534 |
+
科技兴国
|
| 535 |
+
制造兴国
|
| 536 |
+
质量兴国
|
| 537 |
+
网络兴国
|
| 538 |
+
交通兴国
|
| 539 |
+
海洋兴国
|
| 540 |
+
航天兴国
|
| 541 |
+
文化兴国
|
| 542 |
+
教育兴国
|
| 543 |
+
人才兴国
|
| 544 |
+
体育兴国
|
| 545 |
+
健康兴国
|
| 546 |
+
平安兴国
|
| 547 |
+
法治兴国
|
| 548 |
+
数字兴国
|
| 549 |
+
创新兴国
|
Skills/report-search/README.md
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 研报搜索技能
|
| 2 |
+
|
| 3 |
+
财经研究报告搜索引擎,调用同花顺问财的财经资讯搜索接口,专门搜索和分析主流投研机构发布的研究报告。
|
| 4 |
+
|
| 5 |
+
## 功能特点
|
| 6 |
+
|
| 7 |
+
- **研究报告搜索**: 专门搜索各类财经研究报告和分析文章
|
| 8 |
+
- **专业查询处理**: 自动拆解复杂查询为多个专业查询,生成标准化关键词
|
| 9 |
+
- **数据质量评估**: 自动评估搜索结果的专业性和完整性
|
| 10 |
+
- **关键信息提取**: 提取分析逻辑、投资评级、目标价等关键信息
|
| 11 |
+
- **多种输出格式**: 支持CSV、JSON、Markdown等多种输出格式
|
| 12 |
+
- **批量处理**: 支持从文件读取多个查询并批量处理
|
| 13 |
+
- **错误处理**: 完善的错误处理和重试机制
|
| 14 |
+
- **详细日志**: 详细的运行日志和调试信息
|
| 15 |
+
- **跨平台支持**: 支持Windows、macOS、Linux操作系统
|
| 16 |
+
- **规范遵守**: 符合iwencai-skill-creator所有规范要求
|
| 17 |
+
- **响应透明传递**: 严格遵循iwencai-gateway-spec规范,API响应透明传递给调用方
|
| 18 |
+
|
| 19 |
+
## 响应透明传递规范
|
| 20 |
+
|
| 21 |
+
本技能严格遵循iwencai-gateway-spec规范,特别是**响应透明传递要求**:
|
| 22 |
+
|
| 23 |
+
### 核心原则
|
| 24 |
+
- **禁止对API响应进行任何修改**:不得对网关返回的`data`、`result`、`response`等字段进行二次解析、清洗、重组
|
| 25 |
+
- **直接透传完整响应**:API客户端返回完整的HTTP响应体,原封不动传递给调用方
|
| 26 |
+
- **错误透明传递**:API返回的错误状态码与错误Body也应完整传递,不得替换为自定义错误信息
|
| 27 |
+
|
| 28 |
+
### 使用说明
|
| 29 |
+
1. **AI代理调用时**:必须使用`api_client.py`模块,保持响应透明传递
|
| 30 |
+
2. **CLI工具使用时**:可以使用`data_processor.py`进行用户界面友好的后处理
|
| 31 |
+
3. **代码生成时**:优先展示透明传递的API调用方式
|
| 32 |
+
|
| 33 |
+
### 违规示例(禁止)
|
| 34 |
+
```python
|
| 35 |
+
# ❌ 违规:对API响应做了二次组装
|
| 36 |
+
def call_api(...):
|
| 37 |
+
resp = requests.post(url, headers=headers, json=payload)
|
| 38 |
+
data = resp.json()
|
| 39 |
+
result = {"code": 0, "data": data["result"], "msg": "success"} # 禁止:自行包装
|
| 40 |
+
return result
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
### 合规示例(推荐)
|
| 44 |
+
```python
|
| 45 |
+
# ✅ 合规:透明传递API响应
|
| 46 |
+
def call_api(...):
|
| 47 |
+
resp = requests.post(url, headers=headers, json=payload)
|
| 48 |
+
return resp # 或 return resp.json() 直接返回原始响应
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
## 安装要求
|
| 52 |
+
|
| 53 |
+
- Python 3.7+
|
| 54 |
+
- requests库(通过requirements.txt安装)
|
| 55 |
+
|
| 56 |
+
## 快速开始
|
| 57 |
+
|
| 58 |
+
### 1. 获取API密钥
|
| 59 |
+
|
| 60 |
+
所有技能都需要 IWENCAI_API_KEY 环境变量才能使用。如果用户尚未配置,按以下步骤引导:
|
| 61 |
+
|
| 62 |
+
步骤 1:获取 API Key
|
| 63 |
+
在浏览器内打同花顺i问财SkillHub页面:https://www.iwencai.com/skillhub
|
| 64 |
+
|
| 65 |
+
步骤 2:登录
|
| 66 |
+
|
| 67 |
+
步骤 3:点击具体的Skill,打开弹窗查看详情,在安装方式-Agent用户-找到您的IWENCAI_API_KEY这一段,复制
|
| 68 |
+
|
| 69 |
+
步骤 4:配置环境变量
|
| 70 |
+
获取到 API Key 后,直接复制指引文字发送给AI助手,或手动设置环境变量:
|
| 71 |
+
|
| 72 |
+
### 2. 设置环境变量
|
| 73 |
+
|
| 74 |
+
#### Unix/Linux/macOS (bash/zsh)
|
| 75 |
+
```bash
|
| 76 |
+
export IWENCAI_API_KEY="your_api_key_here"
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
#### Windows PowerShell
|
| 80 |
+
```powershell
|
| 81 |
+
$env:IWENCAI_API_KEY="your_api_key_here"
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
#### Windows CMD
|
| 85 |
+
```cmd
|
| 86 |
+
set IWENCAI_API_KEY=your_api_key_here
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### 3. 安装依赖
|
| 90 |
+
```bash
|
| 91 |
+
pip install -r requirements.txt
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
### 4. 基本使用
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
# 搜索研究报告
|
| 98 |
+
python research_report_search.py -q "人工智能行业研究报告"
|
| 99 |
+
|
| 100 |
+
# 搜索最近30天的研究报告
|
| 101 |
+
python research_report_search.py -q "芯片行业" -d 30
|
| 102 |
+
|
| 103 |
+
# 限制返回结果数量
|
| 104 |
+
python research_report_search.py -q "新能源汽车" -l 5
|
| 105 |
+
|
| 106 |
+
# 导出为CSV格式
|
| 107 |
+
python research_report_search.py -q "人工智能" -o results.csv -f csv
|
| 108 |
+
|
| 109 |
+
# 导出为JSON格式
|
| 110 |
+
python research_report_search.py -q "人工智能" -o results.json -f json
|
| 111 |
+
|
| 112 |
+
# 导出为Markdown报告格式
|
| 113 |
+
python research_report_search.py -q "医药行业" -o report.md -f markdown
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
### 5. 测试API连接
|
| 117 |
+
```bash
|
| 118 |
+
python research_report_search.py --test
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
### 6. curl示例
|
| 122 |
+
```bash
|
| 123 |
+
# 使用环境变量中的 API Key
|
| 124 |
+
curl -X POST "https://openapi.iwencai.com/v1/comprehensive/search" \
|
| 125 |
+
-H "Content-Type: application/json" \
|
| 126 |
+
-H "Authorization: Bearer $IWENCAI_API_KEY" \
|
| 127 |
+
-H "X-Claw-Call-Type: normal" \
|
| 128 |
+
-H "X-Claw-Skill-Id: report-search" \
|
| 129 |
+
-H "X-Claw-Skill-Version: 2.0.0" \
|
| 130 |
+
-H "X-Claw-Plugin-Id: none" \
|
| 131 |
+
-H "X-Claw-Plugin-Version: none" \
|
| 132 |
+
-H "X-Claw-Trace-Id: $(python -c 'import secrets; print(secrets.token_hex(32))')" \
|
| 133 |
+
-d '{
|
| 134 |
+
"channels": ["report"],
|
| 135 |
+
"app_id": "AIME_SKILL",
|
| 136 |
+
"query": "人工智能行业研究报告"
|
| 137 |
+
}'
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
## 批量处理
|
| 141 |
+
|
| 142 |
+
```bash
|
| 143 |
+
# 从文件读取多个查询并批量处理
|
| 144 |
+
python research_report_search.py -i queries.txt -o ./results
|
| 145 |
+
|
| 146 |
+
# 指定输出格式为JSON
|
| 147 |
+
python research_report_search.py -i queries.txt -o ./results -f json
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
## 时间范围搜索
|
| 151 |
+
|
| 152 |
+
```bash
|
| 153 |
+
# 搜索指定时间范围的研究报告
|
| 154 |
+
python research_report_search.py -q "新能源车" --date-from "2024-01-01" --date-to "2024-03-31"
|
| 155 |
+
|
| 156 |
+
# 搜索最近7天的研究报告
|
| 157 |
+
python research_report_search.py -q "人工智能" -d 7
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
## 命令行参数
|
| 161 |
+
|
| 162 |
+
### 基本搜索参数
|
| 163 |
+
- `-q, --query`: 搜索关键词(支持中文)
|
| 164 |
+
- `-o, --output`: 输出文件路径
|
| 165 |
+
- `-f, --format`: 输出格式(csv, json, text, markdown)
|
| 166 |
+
- `-l, --limit`: 结果数量限制(默认10)
|
| 167 |
+
|
| 168 |
+
### 批量处理参数
|
| 169 |
+
- `-i, --input`: 输入文件路径(支持批量查询)
|
| 170 |
+
- `--input-format`: 输入文件格式(txt, csv, json)
|
| 171 |
+
- `--output-dir`: 输出目录(批量处理时使用)
|
| 172 |
+
|
| 173 |
+
### 过滤与排序参数
|
| 174 |
+
- `--date-from`: 开始日期(YYYY-MM-DD)
|
| 175 |
+
- `--date-to`: 结束日期(YYYY-MM-DD)
|
| 176 |
+
- `-d, --days`: 最近N天(与date-from/date-to互斥)
|
| 177 |
+
- `--sort-by`: 排序字段(date, relevance)
|
| 178 |
+
- `--sort-order`: 排序顺序(asc, desc)
|
| 179 |
+
|
| 180 |
+
### 其他参数
|
| 181 |
+
- `-v, --verbose`: 详细输出模式
|
| 182 |
+
- `--debug`: 调试模式
|
| 183 |
+
- `--test`: 测试API连接
|
| 184 |
+
- `--config`: 配置文件路径
|
| 185 |
+
- `-h, --help`: 显示帮助信息
|
| 186 |
+
|
| 187 |
+
## 配置文件
|
| 188 |
+
|
| 189 |
+
技能使用 `config.example.json` 作为配置文件示例,实际配置应通过环境变量或自定义配置文件设置。
|
| 190 |
+
|
| 191 |
+
### 配置示例 (config.example.json)
|
| 192 |
+
```json
|
| 193 |
+
{
|
| 194 |
+
"api": {
|
| 195 |
+
"base_url": "https://openapi.iwencai.com",
|
| 196 |
+
"endpoint": "/v1/comprehensive/search",
|
| 197 |
+
"timeout": 30,
|
| 198 |
+
"max_retries": 3,
|
| 199 |
+
"retry_delay": 1.0
|
| 200 |
+
},
|
| 201 |
+
"search": {
|
| 202 |
+
"channels": ["report"],
|
| 203 |
+
"app_id": "AIME_SKILL",
|
| 204 |
+
"default_limit": 10,
|
| 205 |
+
"default_days": 30,
|
| 206 |
+
"min_articles_for_sufficient": 3
|
| 207 |
+
},
|
| 208 |
+
"output": {
|
| 209 |
+
"default_format": "text",
|
| 210 |
+
"csv_encoding": "utf-8-sig",
|
| 211 |
+
"json_indent": 2
|
| 212 |
+
},
|
| 213 |
+
"logging": {
|
| 214 |
+
"level": "INFO",
|
| 215 |
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 216 |
+
},
|
| 217 |
+
"注意": "API密钥应从环境变量 IWENCAI_API_KEY 获取,不要在此配置文件中硬编码"
|
| 218 |
+
}
|
| 219 |
+
```
|
| 220 |
+
|
| 221 |
+
## 使用示例
|
| 222 |
+
|
| 223 |
+
### 示例1:基本搜索
|
| 224 |
+
```bash
|
| 225 |
+
python research_report_search.py --query "人工智能行业研究报告" --output ai_reports.csv --format csv
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
### 示例2:批量处理
|
| 229 |
+
```bash
|
| 230 |
+
# queries.txt 内容:
|
| 231 |
+
# 人工智能
|
| 232 |
+
# 芯片行业
|
| 233 |
+
# 新能源汽车
|
| 234 |
+
# 医药行业
|
| 235 |
+
|
| 236 |
+
python research_report_search.py --input queries.txt --output-dir ./reports --format json
|
| 237 |
+
```
|
| 238 |
+
|
| 239 |
+
### 示例3:专业分析报告
|
| 240 |
+
```bash
|
| 241 |
+
python research_report_search.py --query "特斯拉投资评级目标价" --output tesla_analysis.md --format markdown --limit 5
|
| 242 |
+
```
|
| 243 |
+
|
| 244 |
+
### 示例4:时间范围搜索
|
| 245 |
+
```bash
|
| 246 |
+
python research_report_search.py --query "央行货币政策" --date-from "2024-01-01" --date-to "2024-03-31" --output monetary_policy.json --format json
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
## 接口规范
|
| 250 |
+
|
| 251 |
+
本技能严格遵守iwencai-skill-creator规范,所有发往问财 OpenAPI 网关的请求包含以下 Header:
|
| 252 |
+
|
| 253 |
+
| Header | 取值说明 |
|
| 254 |
+
|--------|----------|
|
| 255 |
+
| `X-Claw-Call-Type` | `normal`:正常请求;`retry`:失败后的重试 |
|
| 256 |
+
| `X-Claw-Skill-Id` | 技能标识:`report-search` |
|
| 257 |
+
| `X-Claw-Skill-Version` | 技能版本:`2.0.0` |
|
| 258 |
+
| `X-Claw-Plugin-Id` | 插件 ID:`none` |
|
| 259 |
+
| `X-Claw-Plugin-Version` | 插件版本:`none` |
|
| 260 |
+
| `X-Claw-Trace-Id` | **每次请求必须新生成**的**全局唯一**追踪 ID;**长度为 64 个字符** |
|
| 261 |
+
|
| 262 |
+
## 注意事项
|
| 263 |
+
|
| 264 |
+
1. **API密钥安全**: API密钥必须通过环境变量设置,不要硬编码在代码中
|
| 265 |
+
2. **认证方式**: 必须使用Bearer Token认证方式
|
| 266 |
+
3. **规范遵守**: 必须遵守iwencai-skill-creator的所有规范要求
|
| 267 |
+
4. **使用限制**: 遵守接口调用频率限制
|
| 268 |
+
5. **错误处理**: 技能包含完善的错误处理和重试机制
|
| 269 |
+
7. **跨平台支持**: 支持Windows、macOS、Linux操作系统
|
| 270 |
+
|
| 271 |
+
## 技术支持
|
| 272 |
+
|
| 273 |
+
如有问题,请参考:
|
| 274 |
+
- `SKILL.md`: 技能详细说明文档
|
| 275 |
+
- `references/api.md`: 接口文档
|
| 276 |
+
- 示例代码:`scripts/example_usage.py`
|
| 277 |
+
|
| 278 |
+
---
|
| 279 |
+
版本:2.0.0
|
| 280 |
+
更新日期:2026-04-17
|
Skills/report-search/SKILL.md
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: report-search
|
| 3 |
+
description: 收录了主流投研机构发布的研究报告,帮你快速获取专业、深度的分析逻辑、投资评级、目标价等重要投研决策信息。
|
| 4 |
+
version: 1.0.0
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# 研报搜索技能
|
| 8 |
+
|
| 9 |
+
## 版本
|
| 10 |
+
当前技能版本:1.0.0(与X-Claw-Skill-Version头一致)
|
| 11 |
+
|
| 12 |
+
## 首次使用 - 获取 API Key
|
| 13 |
+
所有技能都需要 IWENCAI_API_KEY 环境变量才能使用。 如果用户尚未配置,按以下步骤引导:
|
| 14 |
+
|
| 15 |
+
步骤 1:获取 API Key
|
| 16 |
+
在浏览器内打同花顺i问财SkillHub页面:https://www.iwencai.com/skillhub
|
| 17 |
+
|
| 18 |
+
步骤 2:登录
|
| 19 |
+
|
| 20 |
+
步骤 3:点击具体的Skill,打开弹窗查看详情,在安装方式-Agent用户-找到您的IWENCAI_API_KEY这一段,复制
|
| 21 |
+
|
| 22 |
+
步骤 4:配置环境变量
|
| 23 |
+
获取到 API Key 后,直接复制指引文字发送给AI助手,或手动设置环境变量:
|
| 24 |
+
|
| 25 |
+
## 技能概述
|
| 26 |
+
|
| 27 |
+
本技能是一个财经研究报告搜索引擎,通过调用同花顺问财的财经资讯搜索接口,专门搜索和分析主流投研机构发布的研究报告,帮助用户获取专业的分析逻辑、投资评级、目标价等重要投研决策信息。本技能符合iwencai-skill-creator规范,确保所有API调用遵循问财OpenAPI网关的标准要求。
|
| 28 |
+
|
| 29 |
+
## 技能功能
|
| 30 |
+
|
| 31 |
+
### 1. 研究报告搜索
|
| 32 |
+
- 搜索各类财经研究报告和分析文章
|
| 33 |
+
- 覆盖主流投研机构、证券公司、研究机构等
|
| 34 |
+
- 支持中文关键词搜索,专注于研究报告类型
|
| 35 |
+
- 符合iwencai-gateway-spec规范,包含完整的X-Claw-* Header
|
| 36 |
+
|
| 37 |
+
### 2. 智能查询处理能力
|
| 38 |
+
- 自动拆解复杂查询为多个专业查询
|
| 39 |
+
- 示例:用户问"人工智能和芯片行业的研究报告"可以拆分为"人工智能行业研究报告"和"芯片行业研究报告"
|
| 40 |
+
- 根据查询复杂度决定调用接口的次数
|
| 41 |
+
- 生成标准化的专业查询关键词
|
| 42 |
+
|
| 43 |
+
### 3. 数据质量评估与扩展
|
| 44 |
+
- 自动评估搜索结果的专业性和相关性
|
| 45 |
+
- 检查研究报告是否包含分析逻辑、投资评级、目标价等关键信息
|
| 46 |
+
- 如有必要,可调用其他技能或工具扩展数据源
|
| 47 |
+
- 对搜索结果进行专业质量评估
|
| 48 |
+
|
| 49 |
+
### 4. 专业数据处理与返回
|
| 50 |
+
- 对研究搜索结果进行专业排序、过滤和摘要
|
| 51 |
+
- 提取关键信息:分析逻辑、投资评级、目标价、风险提示等
|
| 52 |
+
- **⚠️ 重要警告:根据问财OpenAPI网关规范条件六,API原始响应必须透明传递**
|
| 53 |
+
- **必须遵守**:不得对API响应进行二次解析、清洗、重组或再加工
|
| 54 |
+
- **透明传递要求**:
|
| 55 |
+
- 直接返回API原始响应JSON,不做任何包装
|
| 56 |
+
- 错误响应也必须原样传递,不得替换为自定义错误信息
|
| 57 |
+
- 网络层错误(超时、连接失败等)可提供技术性错误信息
|
| 58 |
+
- 将透明传递的响应数据返回给大模型进行处理
|
| 59 |
+
- 大模型负责生成专业、深度的回答格式
|
| 60 |
+
|
| 61 |
+
## 接口规范
|
| 62 |
+
|
| 63 |
+
### HTTP Header 要求
|
| 64 |
+
所有发往问财 OpenAPI 网关的请求必须包含以下 Header:
|
| 65 |
+
|
| 66 |
+
| Header | 取值说明 |
|
| 67 |
+
|--------|----------|
|
| 68 |
+
| `X-Claw-Call-Type` | `normal`:正常请求;`retry`:失败后的重试。按实际调用场景二选一。 |
|
| 69 |
+
| `X-Claw-Skill-Id` | 技能标识,固定为 `report-search`。 |
|
| 70 |
+
| `X-Claw-Skill-Version` | 当前技能版本号,固定为 `1.0.0`。 |
|
| 71 |
+
| `X-Claw-Plugin-Id` | 插件 ID,固定为 `none`。 |
|
| 72 |
+
| `X-Claw-Plugin-Version` | 插件版本,固定为 `none`。 |
|
| 73 |
+
| `X-Claw-Trace-Id` | **每次请求必须新生成**的**全局唯一**追踪 ID;**长度为 64 个字符**(使用 64 位十六进制字符串)。 |
|
| 74 |
+
|
| 75 |
+
### 基础信息
|
| 76 |
+
- **Base URL**: `https://openapi.iwencai.com`
|
| 77 |
+
- **接口路径**: `/v1/comprehensive/search`
|
| 78 |
+
- **请求方式**: POST(优先使用 POST)
|
| 79 |
+
- **认证方式**: API Key (Bearer Token)
|
| 80 |
+
|
| 81 |
+
### 认证要求
|
| 82 |
+
在请求头中需要携带API Key进行认证:
|
| 83 |
+
```
|
| 84 |
+
Authorization: Bearer {IWENCAI_API_KEY}
|
| 85 |
+
```
|
| 86 |
+
其中 `IWENCAI_API_KEY` 是用户申请的有效API密钥,需要设置为环境变量。
|
| 87 |
+
|
| 88 |
+
### 请求参数
|
| 89 |
+
```json
|
| 90 |
+
{
|
| 91 |
+
"channels": ["report"],
|
| 92 |
+
"app_id": "AIME_SKILL",
|
| 93 |
+
"query": "搜索关键词"
|
| 94 |
+
}
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
**重要参数说明**:
|
| 98 |
+
- `channels`: 固定为 `["report"]`,表示搜索研究报告类型
|
| 99 |
+
- `app_id`: 固定为 `AIME_SKILL`
|
| 100 |
+
- `query`: 用户搜索关键词,支持中文
|
| 101 |
+
|
| 102 |
+
### 响应透明传递要求(Non-Negotiable)
|
| 103 |
+
**核心原则:Skill 生成的代码必须透明传递 API 响应,不得对返回内容做任何修改、过滤、重组或再加工后再交付给调用方。**
|
| 104 |
+
|
| 105 |
+
1. **禁止行为**:
|
| 106 |
+
- 不得对网关返回的 `data`、`result`、`response` 等字段进行二次解析、清洗、重组;
|
| 107 |
+
- 不得自行添加、删除、修改返回结果的任何键值或结构;
|
| 108 |
+
- 不得在 Skill 生成的代码中将 API 原始响应包装成另一套 `result` / `output` / `data` 等结构再返回;
|
| 109 |
+
- 不得在返回前对响应内容做任何「业务逻辑层」的处理(如字段映射、类型转换、格式化等),这些应由调用方决定如何处理。
|
| 110 |
+
|
| 111 |
+
2. **要求行为**:
|
| 112 |
+
- **直接透传**:对网关返回的完整 HTTP 响应体(Body),应在获取后**原封不动**地传递给调用方(或返回给 LLM);
|
| 113 |
+
- **透明返回**:若使用 Python 等语言实现,返回值应为对 API 响应的直接赋值或简单的 `return response`,不做任何中间 transformation;
|
| 114 |
+
- **错误传递**:API 返回的错误状态码与错误 Body 也应完整传递,不得替换为自定义错误信息(除非是网络层超时、连接失败等技术性错误)。
|
| 115 |
+
|
| 116 |
+
3. **正确实现示例**:
|
| 117 |
+
```python
|
| 118 |
+
# ✅ 正确:直接返回API响应
|
| 119 |
+
def search_reports(query: str):
|
| 120 |
+
response = requests.post(url, headers=headers, json=payload)
|
| 121 |
+
# 直接返回API响应,不做任何处理
|
| 122 |
+
return response.json()
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
4. **错误实现示例**:
|
| 126 |
+
```python
|
| 127 |
+
# ❌ 错误:对API响应做了二次组装
|
| 128 |
+
def search_reports(query: str):
|
| 129 |
+
resp = requests.post(url, headers=headers, json=payload)
|
| 130 |
+
data = resp.json()
|
| 131 |
+
result = {"code": 0, "data": data["result"], "msg": "success"} # 禁止:自行包装
|
| 132 |
+
return result
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
## 使用说明
|
| 136 |
+
|
| 137 |
+
### 环境变量配置
|
| 138 |
+
|
| 139 |
+
#### Unix/Linux/macOS (bash/zsh)
|
| 140 |
+
```bash
|
| 141 |
+
export IWENCAI_API_KEY="your_api_key_here"
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
#### Windows PowerShell
|
| 145 |
+
```powershell
|
| 146 |
+
$env:IWENCAI_API_KEY="your_api_key_here"
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
#### Windows CMD
|
| 150 |
+
```cmd
|
| 151 |
+
set IWENCAI_API_KEY=your_api_key_here
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
### 命令行使用
|
| 155 |
+
```bash
|
| 156 |
+
# 基本搜索
|
| 157 |
+
python research_report_search.py -q "人工智能行业研究报告"
|
| 158 |
+
|
| 159 |
+
# 限制结果数量
|
| 160 |
+
python research_report_search.py -q "芯片行业" -l 5
|
| 161 |
+
|
| 162 |
+
# 导出为CSV格式
|
| 163 |
+
python research_report_search.py -q "新能源汽车" -o results.csv -f csv
|
| 164 |
+
|
| 165 |
+
# 批量处理
|
| 166 |
+
python research_report_search.py -i queries.txt -o ./results -f json
|
| 167 |
+
|
| 168 |
+
# 时间范围搜索
|
| 169 |
+
python research_report_search.py -q "医药行业" --date-from "2024-01-01" --date-to "2024-03-31"
|
| 170 |
+
|
| 171 |
+
# 获取帮助
|
| 172 |
+
python research_report_search.py -h
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
### curl 示例
|
| 176 |
+
```bash
|
| 177 |
+
# 生成64位十六进制Trace ID
|
| 178 |
+
TRACE_ID=$(python3 -c "import secrets; print(secrets.token_hex(32))")
|
| 179 |
+
|
| 180 |
+
# 使用环境变量中的 API Key
|
| 181 |
+
curl -X POST "https://openapi.iwencai.com/v1/comprehensive/search" \
|
| 182 |
+
-H "Content-Type: application/json" \
|
| 183 |
+
-H "Authorization: Bearer $IWENCAI_API_KEY" \
|
| 184 |
+
-H "X-Claw-Call-Type: normal" \
|
| 185 |
+
-H "X-Claw-Skill-Id: report-search" \
|
| 186 |
+
-H "X-Claw-Skill-Version: 1.0.0" \
|
| 187 |
+
-H "X-Claw-Plugin-Id: none" \
|
| 188 |
+
-H "X-Claw-Plugin-Version: none" \
|
| 189 |
+
-H "X-Claw-Trace-Id: $TRACE_ID" \
|
| 190 |
+
-d '{
|
| 191 |
+
"channels": ["report"],
|
| 192 |
+
"app_id": "AIME_SKILL",
|
| 193 |
+
"query": "人工智能行业研究报告"
|
| 194 |
+
}'
|
| 195 |
+
```
|
| 196 |
+
|
| 197 |
+
**Windows PowerShell 示例:**
|
| 198 |
+
```powershell
|
| 199 |
+
# 生成64位十六进制Trace ID
|
| 200 |
+
$TRACE_ID = python -c "import secrets; print(secrets.token_hex(32))"
|
| 201 |
+
|
| 202 |
+
# 调用研报搜索接口
|
| 203 |
+
$headers = @{
|
| 204 |
+
"Content-Type" = "application/json"
|
| 205 |
+
"Authorization" = "Bearer $env:IWENCAI_API_KEY"
|
| 206 |
+
"X-Claw-Call-Type" = "normal"
|
| 207 |
+
"X-Claw-Skill-Id" = "report-search"
|
| 208 |
+
"X-Claw-Skill-Version" = "1.0.0"
|
| 209 |
+
"X-Claw-Plugin-Id" = "none"
|
| 210 |
+
"X-Claw-Plugin-Version" = "none"
|
| 211 |
+
"X-Claw-Trace-Id" = $TRACE_ID
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
$body = @{
|
| 215 |
+
channels = @("report")
|
| 216 |
+
app_id = "AIME_SKILL"
|
| 217 |
+
query = "人工智能行业研究报告"
|
| 218 |
+
} | ConvertTo-Json
|
| 219 |
+
|
| 220 |
+
Invoke-RestMethod -Uri "https://openapi.iwencai.com/v1/comprehensive/search" -Method Post -Headers $headers -Body $body
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
## 使用场景
|
| 224 |
+
|
| 225 |
+
### 何时调用本技能
|
| 226 |
+
1. **行业研究报告搜索**: 当用户需要了解特定行业的专业研究报告时
|
| 227 |
+
2. **公司分析报告查询**: 当用户需要获取特定公司的深度分析报告时
|
| 228 |
+
3. **投资评级查询**: 当用户需要了解投资评级和目标价信息时
|
| 229 |
+
4. **趋势分析报告**: 当用户需要获取行业趋势和专业分析时
|
| 230 |
+
5. **投研决策支持**: 当用户需要专业投研信息支持决策时
|
| 231 |
+
|
| 232 |
+
### 调用示例
|
| 233 |
+
1. 用户问:"人工智能行业的最新研究报告有哪些?"
|
| 234 |
+
2. 用户问:"特斯拉的投资评级和目标价是多少?"
|
| 235 |
+
3. 用户问:"芯片行业的深度分析报告有哪些?"
|
| 236 |
+
4. 用户问:"新能源车行业的投资前景分析报告?"
|
| 237 |
+
5. 用户问:"医药行业的研究报告和投资建议?"
|
| 238 |
+
|
| 239 |
+
## 技能内部逻辑
|
| 240 |
+
|
| 241 |
+
### 查询处理流程
|
| 242 |
+
1. **接收用户查询**: 获取用户的专业搜索需求
|
| 243 |
+
2. **查询拆解分析**: 分析查询复杂度,决定是否需要拆分为多个专业子查询
|
| 244 |
+
3. **专业查询生成**: 生成标准化的专业查询关键词,优化搜索效果
|
| 245 |
+
4. **API调用执行**: 生成并执行API调用代码,使用Bearer Token认证和X-Claw-* Header
|
| 246 |
+
5. **数据质量评估**: 检查返回的研究报告是否专业、完整,能否回答用户问题
|
| 247 |
+
6. **专业数据处理**: 对搜索结果进行专业排序、过滤、摘要和关键信息提取
|
| 248 |
+
7. **结果整合返回**: 将处理后的专业结果返回给大模型,生成深度回答
|
| 249 |
+
|
| 250 |
+
### 代码生成要求
|
| 251 |
+
- 生成完整的API调用代码,包括Bearer Token认证、X-Claw-* Header、64字符Trace ID
|
| 252 |
+
- 处理网络异常和接口错误,实现专业重试机制
|
| 253 |
+
- **严格遵守响应透明传递要求**:返回完整的API响应,不得对响应内容做任何修改、过滤、重组
|
| 254 |
+
- 确保代码符合Python最佳实践,可读性和可维护性高
|
| 255 |
+
- **注意**:仅在用户明确要求进行数据处理时,才在返回给用户前进行适当的格式化展示,但API调用本身必须保持透明传递
|
| 256 |
+
|
| 257 |
+
## 技术实现
|
| 258 |
+
|
| 259 |
+
### Python代码要求
|
| 260 |
+
- 使用Python 3.7+版本
|
| 261 |
+
- 优先使用Python标准库和官方包
|
| 262 |
+
- 常用库允许使用:requests, pandas, numpy等
|
| 263 |
+
- 尽量减少第三方库依赖
|
| 264 |
+
- 代码结构清晰,模块化设计
|
| 265 |
+
|
| 266 |
+
### 配置文件要求
|
| 267 |
+
- 必须包含 `config.example.json` 配置文件示例
|
| 268 |
+
- 必须实现 `config.py` 配置管理模块
|
| 269 |
+
- API密钥必须从环境变量 `IWENCAI_API_KEY` 获取,不得硬编码
|
| 270 |
+
|
| 271 |
+
### 目录结构要求
|
| 272 |
+
```
|
| 273 |
+
研报搜索/
|
| 274 |
+
├── README.md # 技能说明文档(中文)
|
| 275 |
+
├── SKILL.md # 技能主文档(包含YAML frontmatter)
|
| 276 |
+
├── references/ # 参考文档目录
|
| 277 |
+
│ └── api.md # 接口文档副本
|
| 278 |
+
└── scripts/ # 源代码目录
|
| 279 |
+
├── __main__.py # 命令行入口点
|
| 280 |
+
├── config.py # 配置管理模块
|
| 281 |
+
├── config.example.json # 配置文件示例
|
| 282 |
+
├── requirements.txt # Python依赖文件
|
| 283 |
+
├── setup.py # 安装配置
|
| 284 |
+
├── research_report_search.py # 主程序文件
|
| 285 |
+
├── api_client.py # API客户端模块
|
| 286 |
+
├── data_processor.py # 数据处理模块
|
| 287 |
+
├── cli.py # CLI接口模块
|
| 288 |
+
├── example_usage.py # 使用示例
|
| 289 |
+
└── test_basic.py # 基础测试
|
| 290 |
+
```
|
| 291 |
+
|
| 292 |
+
### CLI接口要求
|
| 293 |
+
- 支持 `python research_report_search.py` 调用方式
|
| 294 |
+
- 支持 `python -m scripts.research_report_search` 调用方式
|
| 295 |
+
- 提供完整的命令行参数和帮助文档
|
| 296 |
+
- 支持多种输出格式:csv, json, text, markdown
|
| 297 |
+
|
| 298 |
+
### API透明传递与CLI数据处理区分说明
|
| 299 |
+
1. **API客户端模块 (api_client.py)**:
|
| 300 |
+
- 严格遵守响应透明传递要求,返回完整的API响应
|
| 301 |
+
- 不进行任何数据清洗、重组或业务逻辑处理
|
| 302 |
+
- 仅处理网络层错误和认证问题
|
| 303 |
+
|
| 304 |
+
2. **CLI工具与数据处理模块 (cli.py, data_processor.py)**:
|
| 305 |
+
- 这些模块仅用于命令行工具的后处理功能
|
| 306 |
+
- 当使用CLI工具时,可以对API响应进行格式化、过滤等用户界面友好的处理
|
| 307 |
+
- 但在技能被AI调用时,必须使用API客户端模块的透明传递方式
|
| 308 |
+
|
| 309 |
+
3. **重要原则**:
|
| 310 |
+
- 当技能被AI代理调用时,必须生成使用api_client.py的代码,并保持响应透明传递
|
| 311 |
+
- 当用户直接使用CLI工具时,可以使用data_processor.py进行后处理
|
| 312 |
+
- SKILL.md中的代码示例应优先展示透明传递的API调用方式
|
| 313 |
+
|
| 314 |
+
## 注意事项
|
| 315 |
+
|
| 316 |
+
1. **数据专业性**: 专注于研究报告类型,确保返回结果的深度和专业性
|
| 317 |
+
2. **认证合规**: 必须使用Bearer Token认证方式,符合公网访问要求
|
| 318 |
+
3. **错误处理**: 实现完善的错误处理和重试机制
|
| 319 |
+
4. **性能优化**: 考虑接口调用频率限制,实现合理的请求间隔
|
| 320 |
+
5. **用户体验**: CLI接口要直观易用,提供清晰的专业反馈
|
| 321 |
+
6. **规范遵守**: 必须遵守iwencai-skill-creator的所有规范要求
|
Skills/report-search/references/api.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 财经报告搜索接口文档
|
| 2 |
+
|
| 3 |
+
## 接口概述
|
| 4 |
+
|
| 5 |
+
- **接口名称**: 财经报告搜索接口
|
| 6 |
+
- **接口说明**: 搜索财经领域的报告文章,返回相关的文章信息列表
|
| 7 |
+
- **接口版本**: v1
|
| 8 |
+
|
| 9 |
+
## 基础信息
|
| 10 |
+
|
| 11 |
+
- **Base URL**: `https://openapi.iwencai.com`
|
| 12 |
+
- **接口路径**: `/v1/comprehensive/search`
|
| 13 |
+
- **请求方式**: POST
|
| 14 |
+
- **Content-Type**: `application/json`
|
| 15 |
+
- **API Key 环境变量**: `IWENCAI_API_KEY`
|
| 16 |
+
|
| 17 |
+
## 请求说明
|
| 18 |
+
|
| 19 |
+
### 请求头
|
| 20 |
+
|
| 21 |
+
```
|
| 22 |
+
Content-Type: application/json
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
### 请求参数
|
| 26 |
+
|
| 27 |
+
请求体为 JSON 格式,包含以下参数:
|
| 28 |
+
|
| 29 |
+
#### 固定参数
|
| 30 |
+
|
| 31 |
+
| 参数名 | 类型 | 说明 | 值 |
|
| 32 |
+
|--------|------|------|-----|
|
| 33 |
+
| channels | LIST | 搜索渠道类型 | `["report"]` |
|
| 34 |
+
| app_id | STRING | 应用ID | `AIME_SKILL` |
|
| 35 |
+
|
| 36 |
+
#### 可变参数
|
| 37 |
+
|
| 38 |
+
| 参数名 | 类型 | 说明 | 必填 |
|
| 39 |
+
|--------|------|------|------|
|
| 40 |
+
| query | STRING | 用户问句,即搜索关键词 | 是 |
|
| 41 |
+
|
| 42 |
+
### 请求示例
|
| 43 |
+
|
| 44 |
+
```json
|
| 45 |
+
{
|
| 46 |
+
"channels": ["report"],
|
| 47 |
+
"app_id": "AIME_SKILL",
|
| 48 |
+
"query": "人工智能行业研究报告"
|
| 49 |
+
}
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## 响应说明
|
| 53 |
+
|
| 54 |
+
### 响应格式
|
| 55 |
+
|
| 56 |
+
响应为 JSON 格式,主要包含 `data` 字段,该字段是一个文章信息列表。
|
| 57 |
+
|
| 58 |
+
### 响应结构
|
| 59 |
+
|
| 60 |
+
```json
|
| 61 |
+
{
|
| 62 |
+
"data": [
|
| 63 |
+
{
|
| 64 |
+
"title": "文章标题",
|
| 65 |
+
"summary": "文章摘要",
|
| 66 |
+
"url": "文章网址",
|
| 67 |
+
"publish_date": "文章发布时间"
|
| 68 |
+
}
|
| 69 |
+
]
|
| 70 |
+
}
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### 字段说明
|
| 74 |
+
|
| 75 |
+
#### data 字段
|
| 76 |
+
|
| 77 |
+
| 字段名 | 类型 | 说明 | 格式 |
|
| 78 |
+
|--------|------|------|------|
|
| 79 |
+
| title | STRING | 文章标题 | - |
|
| 80 |
+
| summary | STRING | 文章摘要 | - |
|
| 81 |
+
| url | STRING | 文章网址 | URL格式 |
|
| 82 |
+
| publish_date | STRING | 文章发布时间 | `YYYY-MM-DD HH:MM:SS` |
|
| 83 |
+
|
| 84 |
+
### 响应示例
|
| 85 |
+
|
| 86 |
+
```json
|
| 87 |
+
{
|
| 88 |
+
"data": [
|
| 89 |
+
{
|
| 90 |
+
"title": "2024年人工智能行业发展趋势报告",
|
| 91 |
+
"summary": "本报告分析了2024年人工智能行业的发展趋势,包括技术突破、应用场景、市场规模等方面的内容...",
|
| 92 |
+
"url": "https://example.com/reports/ai-trends-2024",
|
| 93 |
+
"publish_date": "2024-01-15 09:30:00"
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"title": "金融科技AI应用研究报告",
|
| 97 |
+
"summary": "报告详细介绍了人工智能在金融科技领域的应用现状和未来发展方向...",
|
| 98 |
+
"url": "https://example.com/reports/fintech-ai-applications",
|
| 99 |
+
"publish_date": "2024-01-14 14:20:00"
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"title": "智能制造AI解决方案分析",
|
| 103 |
+
"summary": "本报告分析了人工智能在智能制造领域的解决方案和应用案例...",
|
| 104 |
+
"url": "https://example.com/reports/smart-manufacturing-ai",
|
| 105 |
+
"publish_date": "2024-01-13 11:15"
|
| 106 |
+
}
|
| 107 |
+
]
|
| 108 |
+
}
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
## 使用说明
|
| 112 |
+
|
| 113 |
+
### 环境变量设置
|
| 114 |
+
|
| 115 |
+
在使用此接口前,需要设置 API Key 环境变量:
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
export IWENCAI_API_KEY="your_api_key_here"
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
### 调用示例(Python)
|
| 122 |
+
|
| 123 |
+
```python
|
| 124 |
+
import os
|
| 125 |
+
import requests
|
| 126 |
+
import json
|
| 127 |
+
|
| 128 |
+
# API配置
|
| 129 |
+
BASE_URL = "https://openapi.iwencai.com"
|
| 130 |
+
ENDPOINT = "/v1/comprehensive/search"
|
| 131 |
+
API_KEY = os.getenv("IWENCAI_API_KEY")
|
| 132 |
+
|
| 133 |
+
# 请求头
|
| 134 |
+
headers = {
|
| 135 |
+
"Content-Type": "application/json",
|
| 136 |
+
"Authorization": f"Bearer {API_KEY}"
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
# 请求体
|
| 140 |
+
payload = {
|
| 141 |
+
"channels": ["report"],
|
| 142 |
+
"app_id": "AIME_SKILL",
|
| 143 |
+
"query": "人工智能行业研究报告"
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
# 发送请求
|
| 147 |
+
response = requests.post(
|
| 148 |
+
f"{BASE_URL}{ENDPOINT}",
|
| 149 |
+
headers=headers,
|
| 150 |
+
json=payload
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
# 处理响应
|
| 154 |
+
if response.status_code == 200:
|
| 155 |
+
data = response.json()
|
| 156 |
+
articles = data.get("data", [])
|
| 157 |
+
for article in articles:
|
| 158 |
+
print(f"标题: {article['title']}")
|
| 159 |
+
print(f"摘要: {article['summary']}")
|
| 160 |
+
print(f"链接: {article['url']}")
|
| 161 |
+
print(f"发布时间: {article['publish_date']}")
|
| 162 |
+
print("---")
|
| 163 |
+
else:
|
| 164 |
+
print(f"请求失败: {response.status_code}")
|
| 165 |
+
print(response.text)
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
### 调用示例(JavaScript/Node.js)
|
| 169 |
+
|
| 170 |
+
```javascript
|
| 171 |
+
const axios = require('axios');
|
| 172 |
+
|
| 173 |
+
// API配置
|
| 174 |
+
const BASE_URL = 'https://openapi.iwencai.com';
|
| 175 |
+
const ENDPOINT = '/v1/comprehensive/search';
|
| 176 |
+
const API_KEY = process.env.IWENCAI_API_KEY;
|
| 177 |
+
|
| 178 |
+
// 请求头
|
| 179 |
+
const headers = {
|
| 180 |
+
'Content-Type': 'application/json',
|
| 181 |
+
'Authorization': `Bearer ${API_KEY}`
|
| 182 |
+
};
|
| 183 |
+
|
| 184 |
+
// 请求体
|
| 185 |
+
const payload = {
|
| 186 |
+
channels: ['report'],
|
| 187 |
+
app_id: 'AIME_SKILL',
|
| 188 |
+
query: '人工智能行业研究报告'
|
| 189 |
+
};
|
| 190 |
+
|
| 191 |
+
// 发送请求
|
| 192 |
+
axios.post(`${BASE_URL}${ENDPOINT}`, payload, { headers })
|
| 193 |
+
.then(response => {
|
| 194 |
+
const articles = response.data.data || [];
|
| 195 |
+
articles.forEach(article => {
|
| 196 |
+
console.log(`标题: ${article.title}`);
|
| 197 |
+
console.log(`摘要: ${article.summary}`);
|
| 198 |
+
console.log(`链接: ${article.url}`);
|
| 199 |
+
console.log(`发布时间: ${article.publish_date}`);
|
| 200 |
+
console.log('---');
|
| 201 |
+
});
|
| 202 |
+
})
|
| 203 |
+
.catch(error => {
|
| 204 |
+
console.error('请求失败:', error.response?.status || error.message);
|
| 205 |
+
console.error('错误信息:', error.response?.data || error.message);
|
| 206 |
+
});
|
| 207 |
+
```
|
| 208 |
+
|
| 209 |
+
## 注意事项
|
| 210 |
+
|
| 211 |
+
1. **API Key**: 需要申请有效的 API Key 并设置为 `IWENCAI_API_KEY` 环境变量
|
| 212 |
+
2. **参数固定值**: `channels` 参数必须为 `["report"]`,`app_id` 参数必须为 `AIME_SKILL`
|
| 213 |
+
3. **搜索关键词**: `query` 参数支持中文关键词,建议使用具体的搜索词以获得更准确的结果
|
| 214 |
+
4. **响应数据**: 返回的文章数据按发布时间倒序排列
|
| 215 |
+
5. **错误处理**: 如果请求失败,请检查网络连接、API Key 和请求参数
|
| 216 |
+
|
| 217 |
+
## 常见问题
|
| 218 |
+
|
| 219 |
+
### Q: 如何获取 API Key?
|
| 220 |
+
A: 需要向接口提供方申请有效的 API Key。
|
| 221 |
+
|
| 222 |
+
### Q: 搜索不到结果怎么办?
|
| 223 |
+
A: 可以尝试调整搜索关键词,使用更具体或更通用的词汇。
|
| 224 |
+
|
| 225 |
+
### Q: 返回的文章数量有限制吗?
|
| 226 |
+
A: 接口文档未明确说明返回数量限制,实际返回数量可能受搜索条件和系统限制影响。
|
| 227 |
+
|
| 228 |
+
### Q: publish_date 字段的时区是什么?
|
| 229 |
+
A: 文档未明确说明时区,通常为北京时间(UTC+8)。
|
| 230 |
+
|
| 231 |
+
---
|
| 232 |
+
|
| 233 |
+
**文档版本**: 1.0
|
| 234 |
+
**最后更新**: 基于 api_prompt.txt 生成
|
| 235 |
+
**接口状态**: 正常
|
Skills/report-search/scripts/__main__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
研报搜索技能命令行入口点
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
# 添加当前目录到Python路径
|
| 11 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 12 |
+
|
| 13 |
+
from research_report_search import main
|
| 14 |
+
|
| 15 |
+
if __name__ == "__main__":
|
| 16 |
+
sys.exit(main())
|
Skills/report-search/scripts/api_client.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
研报搜索技能API客户端模块
|
| 5 |
+
符合iwencai-skill-creator规范
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import time
|
| 10 |
+
import logging
|
| 11 |
+
import secrets
|
| 12 |
+
from typing import Dict, List, Any, Optional, Tuple
|
| 13 |
+
import requests
|
| 14 |
+
from requests.exceptions import RequestException, Timeout, ConnectionError
|
| 15 |
+
|
| 16 |
+
from config import get_config
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class APIError(Exception):
|
| 20 |
+
"""API错误异常类"""
|
| 21 |
+
pass
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class APIClient:
|
| 25 |
+
"""API客户端类"""
|
| 26 |
+
|
| 27 |
+
def __init__(self, config_file: Optional[str] = None):
|
| 28 |
+
"""
|
| 29 |
+
初始化API客户端
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
config_file: 配置文件路径
|
| 33 |
+
"""
|
| 34 |
+
self.config = get_config(config_file)
|
| 35 |
+
self.api_url = self.config.get_api_url()
|
| 36 |
+
self.api_key = self.config.get_api_key()
|
| 37 |
+
self.timeout = self.config.get("api.timeout")
|
| 38 |
+
self.max_retries = self.config.get("api.max_retries")
|
| 39 |
+
self.retry_delay = self.config.get("api.retry_delay")
|
| 40 |
+
self.skill_id = "report-search"
|
| 41 |
+
self.skill_version = "2.0.0"
|
| 42 |
+
|
| 43 |
+
# 设置日志
|
| 44 |
+
self.config.setup_logging()
|
| 45 |
+
self.logger = logging.getLogger(__name__)
|
| 46 |
+
|
| 47 |
+
def _generate_trace_id(self) -> str:
|
| 48 |
+
"""生成64字符的Trace ID"""
|
| 49 |
+
return secrets.token_hex(32)
|
| 50 |
+
|
| 51 |
+
def _get_headers(self, call_type: str = "normal") -> Dict[str, str]:
|
| 52 |
+
"""获取请求头(包含X-Claw-* Header)"""
|
| 53 |
+
headers = {
|
| 54 |
+
"Content-Type": "application/json",
|
| 55 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 56 |
+
"X-Claw-Call-Type": call_type,
|
| 57 |
+
"X-Claw-Skill-Id": self.skill_id,
|
| 58 |
+
"X-Claw-Skill-Version": self.skill_version,
|
| 59 |
+
"X-Claw-Plugin-Id": "none",
|
| 60 |
+
"X-Claw-Plugin-Version": "none",
|
| 61 |
+
"X-Claw-Trace-Id": self._generate_trace_id()
|
| 62 |
+
}
|
| 63 |
+
return headers
|
| 64 |
+
|
| 65 |
+
def _prepare_payload(self, query: str, limit: Optional[int] = None) -> Dict[str, Any]:
|
| 66 |
+
"""准备请求体"""
|
| 67 |
+
payload = {
|
| 68 |
+
"channels": self.config.get("search.channels"),
|
| 69 |
+
"app_id": self.config.get("search.app_id"),
|
| 70 |
+
"query": query
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
# 可以添加其他参数
|
| 74 |
+
if limit:
|
| 75 |
+
# 注意:实际接口可能不支持limit参数,这里只是示例
|
| 76 |
+
pass
|
| 77 |
+
|
| 78 |
+
return payload
|
| 79 |
+
|
| 80 |
+
def _make_request_raw(self, payload: Dict[str, Any], call_type: str = "normal") -> Dict[str, Any]:
|
| 81 |
+
"""
|
| 82 |
+
发送请求(带重试机制,完全透明传递响应)
|
| 83 |
+
|
| 84 |
+
根据问财OpenAPI网关规范条件六,此方法必须透明传递所有API响应,
|
| 85 |
+
包括错误响应,不做任何修改、过滤或重组。
|
| 86 |
+
"""
|
| 87 |
+
for attempt in range(self.max_retries + 1):
|
| 88 |
+
try:
|
| 89 |
+
headers = self._get_headers(call_type)
|
| 90 |
+
|
| 91 |
+
self.logger.debug(f"发送API请求 (尝试 {attempt + 1}/{self.max_retries + 1}): {payload}")
|
| 92 |
+
self.logger.debug(f"Trace ID: {headers['X-Claw-Trace-Id']}")
|
| 93 |
+
|
| 94 |
+
response = requests.post(
|
| 95 |
+
self.api_url,
|
| 96 |
+
headers=headers,
|
| 97 |
+
json=payload,
|
| 98 |
+
timeout=self.timeout
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
self.logger.debug(f"API响应状态码: {response.status_code}")
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
return response.json()
|
| 105 |
+
except ValueError:
|
| 106 |
+
self.logger.warning(f"API返回非JSON响应,状态码: {response.status_code}")
|
| 107 |
+
return {
|
| 108 |
+
"error": "invalid_json_response",
|
| 109 |
+
"raw_response": response.text,
|
| 110 |
+
"status_code": response.status_code,
|
| 111 |
+
"headers": dict(response.headers)
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
except Timeout:
|
| 115 |
+
self.logger.warning(f"请求超时 (尝试 {attempt + 1}/{self.max_retries + 1})")
|
| 116 |
+
if attempt < self.max_retries:
|
| 117 |
+
wait_time = self.retry_delay * (attempt + 1)
|
| 118 |
+
self.logger.info(f"等待 {wait_time} 秒后重试...")
|
| 119 |
+
time.sleep(wait_time)
|
| 120 |
+
else:
|
| 121 |
+
raise
|
| 122 |
+
|
| 123 |
+
except ConnectionError:
|
| 124 |
+
self.logger.warning(f"连接错误 (尝试 {attempt + 1}/{self.max_retries + 1})")
|
| 125 |
+
if attempt < self.max_retries:
|
| 126 |
+
self.logger.info(f"等待 {self.retry_delay} 秒后重试...")
|
| 127 |
+
time.sleep(self.retry_delay)
|
| 128 |
+
else:
|
| 129 |
+
raise
|
| 130 |
+
|
| 131 |
+
except RequestException as e:
|
| 132 |
+
self.logger.error(f"请求异常: {e}")
|
| 133 |
+
if attempt < self.max_retries:
|
| 134 |
+
self.logger.info(f"等待 {self.retry_delay} 秒后重试...")
|
| 135 |
+
time.sleep(self.retry_delay)
|
| 136 |
+
else:
|
| 137 |
+
raise
|
| 138 |
+
|
| 139 |
+
raise Exception(f"请求失败,已达到最大重试次数: {self.max_retries + 1}")
|
| 140 |
+
|
| 141 |
+
def search_reports(self, query: str, limit: Optional[int] = None, call_type: str = "normal") -> Dict[str, Any]:
|
| 142 |
+
"""
|
| 143 |
+
搜索研究报告(原始响应,完全透明传递)
|
| 144 |
+
|
| 145 |
+
Args:
|
| 146 |
+
query: 搜索关键词
|
| 147 |
+
limit: 结果数量限制(注意:此参数仅用于向后兼容,实际限制应由调用方处理)
|
| 148 |
+
call_type: 调用类型,normal或retry
|
| 149 |
+
|
| 150 |
+
Returns:
|
| 151 |
+
完整的API响应数据(符合响应透明传递要求)
|
| 152 |
+
|
| 153 |
+
Note:
|
| 154 |
+
根据问财OpenAPI网关规范条件六,此方法必须透明传递所有API响应,
|
| 155 |
+
包括错误响应,不做任何修改、过滤或重组。
|
| 156 |
+
调用方需要自己处理响应数据。这是推荐的使用方式。
|
| 157 |
+
"""
|
| 158 |
+
try:
|
| 159 |
+
self.logger.info(f"搜索研究报告: {query}")
|
| 160 |
+
|
| 161 |
+
payload = self._prepare_payload(query, limit)
|
| 162 |
+
|
| 163 |
+
response_data = self._make_request_raw(payload, call_type)
|
| 164 |
+
|
| 165 |
+
self.logger.info(f"API响应接收完成")
|
| 166 |
+
return response_data
|
| 167 |
+
|
| 168 |
+
except Exception as e:
|
| 169 |
+
self.logger.error(f"搜索研究报告时发生错误: {e}")
|
| 170 |
+
raise
|
| 171 |
+
|
| 172 |
+
def batch_search(self, queries: List[str], limit_per_query: Optional[int] = None, call_type: str = "normal") -> Dict[str, Dict[str, Any]]:
|
| 173 |
+
"""
|
| 174 |
+
批量搜索研究报告(透明传递版本)
|
| 175 |
+
|
| 176 |
+
Args:
|
| 177 |
+
queries: 搜索关键词列表
|
| 178 |
+
limit_per_query: 每个查询的结果数量限制(注意:此参数仅用于向后兼容)
|
| 179 |
+
call_type: 调用类型,normal或retry
|
| 180 |
+
|
| 181 |
+
Returns:
|
| 182 |
+
字典,键为查询词,值为对应的透明传递的API响应
|
| 183 |
+
|
| 184 |
+
Note:
|
| 185 |
+
此方法完全透明传递API响应,符合问财规范条件六。
|
| 186 |
+
调用方需要自己处理响应数据。
|
| 187 |
+
"""
|
| 188 |
+
results = {}
|
| 189 |
+
|
| 190 |
+
for i, query in enumerate(queries):
|
| 191 |
+
try:
|
| 192 |
+
self.logger.info(f"批量搜索 [{i+1}/{len(queries)}]: {query}")
|
| 193 |
+
response_data = self.search_reports(query, limit_per_query, call_type=call_type)
|
| 194 |
+
results[query] = response_data
|
| 195 |
+
|
| 196 |
+
if i < len(queries) - 1:
|
| 197 |
+
time.sleep(0.5)
|
| 198 |
+
|
| 199 |
+
except Exception as e:
|
| 200 |
+
self.logger.error(f"查询 '{query}' 搜索失败: {str(e)}")
|
| 201 |
+
results[query] = {"error": "search_failed", "message": str(e)}
|
| 202 |
+
|
| 203 |
+
return results
|
| 204 |
+
|
| 205 |
+
def test_connection(self) -> bool:
|
| 206 |
+
"""
|
| 207 |
+
测试API连接
|
| 208 |
+
|
| 209 |
+
Returns:
|
| 210 |
+
连接是否成功
|
| 211 |
+
"""
|
| 212 |
+
try:
|
| 213 |
+
self.logger.info("测试API连接...")
|
| 214 |
+
|
| 215 |
+
test_query = "测试"
|
| 216 |
+
test_payload = self._prepare_payload(test_query)
|
| 217 |
+
|
| 218 |
+
response = self._make_request_raw(test_payload)
|
| 219 |
+
|
| 220 |
+
if "error" not in response:
|
| 221 |
+
self.logger.info("API连接测试成功")
|
| 222 |
+
return True
|
| 223 |
+
else:
|
| 224 |
+
self.logger.warning(f"API连接测试失败: {response}")
|
| 225 |
+
return False
|
| 226 |
+
|
| 227 |
+
except Exception as e:
|
| 228 |
+
self.logger.error(f"API连接测试异常: {e}")
|
| 229 |
+
return False
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
if __name__ == "__main__":
|
| 233 |
+
import sys
|
| 234 |
+
|
| 235 |
+
if not os.getenv("IWENCAI_API_KEY"):
|
| 236 |
+
print("请设置环境变量 IWENCAI_API_KEY")
|
| 237 |
+
sys.exit(1)
|
| 238 |
+
|
| 239 |
+
try:
|
| 240 |
+
client = APIClient()
|
| 241 |
+
|
| 242 |
+
if client.test_connection():
|
| 243 |
+
print("API连接测试成功")
|
| 244 |
+
else:
|
| 245 |
+
print("API连接测试失败")
|
| 246 |
+
sys.exit(1)
|
| 247 |
+
|
| 248 |
+
test_queries = ["人工智能", "芯片行业"]
|
| 249 |
+
for query in test_queries:
|
| 250 |
+
print(f"\n搜索: {query}")
|
| 251 |
+
try:
|
| 252 |
+
response = client.search_reports(query, limit=3)
|
| 253 |
+
print(f"API响应类型: {type(response)}")
|
| 254 |
+
if isinstance(response, dict):
|
| 255 |
+
print(f"响应键: {list(response.keys())}")
|
| 256 |
+
except Exception as e:
|
| 257 |
+
print(f"搜索失败: {e}")
|
| 258 |
+
|
| 259 |
+
except Exception as e:
|
| 260 |
+
print(f"测试过程中发生错误: {e}")
|
| 261 |
+
sys.exit(1)
|
Skills/report-search/scripts/cli.py
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
研报搜索技能命令行接口模块
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import argparse
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
import logging
|
| 12 |
+
from typing import List, Optional, Dict, Any
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
from api_client import APIClient, APIError
|
| 16 |
+
from data_processor import DataProcessor
|
| 17 |
+
from config import get_config
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class ResearchReportCLI:
|
| 21 |
+
"""研报搜索命令行接口类"""
|
| 22 |
+
|
| 23 |
+
def __init__(self):
|
| 24 |
+
"""初始化CLI"""
|
| 25 |
+
self.config = get_config()
|
| 26 |
+
self.config.setup_logging()
|
| 27 |
+
self.logger = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
+
self.api_client = None
|
| 30 |
+
self.data_processor = None
|
| 31 |
+
|
| 32 |
+
def initialize(self, config_file: Optional[str] = None) -> None:
|
| 33 |
+
"""初始化组件"""
|
| 34 |
+
try:
|
| 35 |
+
self.api_client = APIClient(config_file)
|
| 36 |
+
self.data_processor = DataProcessor(config_file)
|
| 37 |
+
self.logger.info("CLI初始化完成")
|
| 38 |
+
except Exception as e:
|
| 39 |
+
self.logger.error(f"CLI初始化失败: {e}")
|
| 40 |
+
raise
|
| 41 |
+
|
| 42 |
+
def parse_arguments(self) -> argparse.Namespace:
|
| 43 |
+
"""解析命令行参数"""
|
| 44 |
+
parser = argparse.ArgumentParser(
|
| 45 |
+
description="研报搜索技能 - 搜索和分析财经研究报告",
|
| 46 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 47 |
+
epilog="""
|
| 48 |
+
使用示例:
|
| 49 |
+
# 基本搜索
|
| 50 |
+
python research_report_search.py -q "人工智能行业研究报告"
|
| 51 |
+
|
| 52 |
+
# 限制结果数量
|
| 53 |
+
python research_report_search.py -q "芯片行业" -l 5
|
| 54 |
+
|
| 55 |
+
# 导出为CSV格式
|
| 56 |
+
python research_report_search.py -q "新能源汽车" -o results.csv -f csv
|
| 57 |
+
|
| 58 |
+
# 批量处理
|
| 59 |
+
python research_report_search.py -i queries.txt -o ./results -f json
|
| 60 |
+
|
| 61 |
+
# 时间范围搜索
|
| 62 |
+
python research_report_search.py -q "医药行业" --date-from "2024-01-01" --date-to "2024-03-31"
|
| 63 |
+
|
| 64 |
+
# 获取帮助
|
| 65 |
+
python research_report_search.py -h
|
| 66 |
+
"""
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# 基本搜索参数
|
| 70 |
+
parser.add_argument(
|
| 71 |
+
"-q", "--query",
|
| 72 |
+
help="搜索关键词(支持中文)",
|
| 73 |
+
type=str
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
parser.add_argument(
|
| 77 |
+
"-o", "--output",
|
| 78 |
+
help="输出文件路径",
|
| 79 |
+
type=str
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
parser.add_argument(
|
| 83 |
+
"-f", "--format",
|
| 84 |
+
help="输出格式(csv, json, text, markdown)",
|
| 85 |
+
choices=["csv", "json", "text", "markdown"],
|
| 86 |
+
default="text"
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
parser.add_argument(
|
| 90 |
+
"-l", "--limit",
|
| 91 |
+
help="结果数量限制",
|
| 92 |
+
type=int,
|
| 93 |
+
default=10
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# 批量处理参数
|
| 97 |
+
parser.add_argument(
|
| 98 |
+
"-i", "--input",
|
| 99 |
+
help="输入文件路径(支持批量查询)",
|
| 100 |
+
type=str
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
parser.add_argument(
|
| 104 |
+
"--input-format",
|
| 105 |
+
help="输入文件格式(txt, csv, json)",
|
| 106 |
+
choices=["txt", "csv", "json"],
|
| 107 |
+
default="txt"
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
parser.add_argument(
|
| 111 |
+
"--output-dir",
|
| 112 |
+
help="输出目录(批量处理时使用)",
|
| 113 |
+
type=str
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
# 过滤与排序参数
|
| 117 |
+
parser.add_argument(
|
| 118 |
+
"--date-from",
|
| 119 |
+
help="开始日期(YYYY-MM-DD)",
|
| 120 |
+
type=str
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
parser.add_argument(
|
| 124 |
+
"--date-to",
|
| 125 |
+
help="结束日期(YYYY-MM-DD)",
|
| 126 |
+
type=str
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
parser.add_argument(
|
| 130 |
+
"-d", "--days",
|
| 131 |
+
help="最近N天",
|
| 132 |
+
type=int
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
parser.add_argument(
|
| 136 |
+
"--sort-by",
|
| 137 |
+
help="排序字段",
|
| 138 |
+
choices=["date", "relevance"],
|
| 139 |
+
default="date"
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
parser.add_argument(
|
| 143 |
+
"--sort-order",
|
| 144 |
+
help="排序顺序",
|
| 145 |
+
choices=["asc", "desc"],
|
| 146 |
+
default="desc"
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
# 其他参数
|
| 150 |
+
parser.add_argument(
|
| 151 |
+
"-v", "--verbose",
|
| 152 |
+
help="详细输出模式",
|
| 153 |
+
action="store_true"
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
parser.add_argument(
|
| 157 |
+
"--debug",
|
| 158 |
+
help="调试模式",
|
| 159 |
+
action="store_true"
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
parser.add_argument(
|
| 163 |
+
"--config",
|
| 164 |
+
help="配置文件路径",
|
| 165 |
+
type=str
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
parser.add_argument(
|
| 169 |
+
"--test",
|
| 170 |
+
help="测试API连接",
|
| 171 |
+
action="store_true"
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
return parser.parse_args()
|
| 175 |
+
|
| 176 |
+
def load_queries_from_file(self, filepath: str, file_format: str = "txt") -> List[str]:
|
| 177 |
+
"""从文件加载查询"""
|
| 178 |
+
try:
|
| 179 |
+
queries = []
|
| 180 |
+
|
| 181 |
+
if file_format == "txt":
|
| 182 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 183 |
+
for line in f:
|
| 184 |
+
line = line.strip()
|
| 185 |
+
if line and not line.startswith('#'):
|
| 186 |
+
queries.append(line)
|
| 187 |
+
|
| 188 |
+
elif file_format == "csv":
|
| 189 |
+
import pandas as pd
|
| 190 |
+
df = pd.read_csv(filepath)
|
| 191 |
+
if 'query' in df.columns:
|
| 192 |
+
queries = df['query'].dropna().tolist()
|
| 193 |
+
else:
|
| 194 |
+
# 使用第一列
|
| 195 |
+
queries = df.iloc[:, 0].dropna().tolist()
|
| 196 |
+
|
| 197 |
+
elif file_format == "json":
|
| 198 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 199 |
+
data = json.load(f)
|
| 200 |
+
if isinstance(data, list):
|
| 201 |
+
queries = [str(item) for item in data if item]
|
| 202 |
+
elif isinstance(data, dict) and 'queries' in data:
|
| 203 |
+
queries = [str(q) for q in data['queries'] if q]
|
| 204 |
+
|
| 205 |
+
self.logger.info(f"从文件加载 {len(queries)} 个查询: {filepath}")
|
| 206 |
+
return queries
|
| 207 |
+
|
| 208 |
+
except Exception as e:
|
| 209 |
+
self.logger.error(f"加载查询文件失败 {filepath}: {e}")
|
| 210 |
+
raise
|
| 211 |
+
|
| 212 |
+
def process_single_query(self, args: argparse.Namespace) -> Dict[str, Any]:
|
| 213 |
+
"""处理单个查询"""
|
| 214 |
+
result = {
|
| 215 |
+
"query": args.query,
|
| 216 |
+
"success": False,
|
| 217 |
+
"articles": [],
|
| 218 |
+
"error": None
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
try:
|
| 222 |
+
# 搜索研究报告
|
| 223 |
+
self.logger.info(f"搜索研究报告: {args.query}")
|
| 224 |
+
response = self.api_client.search_reports(args.query, args.limit)
|
| 225 |
+
|
| 226 |
+
# 检查是否为错误响应
|
| 227 |
+
if isinstance(response, dict) and "error" in response:
|
| 228 |
+
# 应用层可以选择性展示关键错误信息
|
| 229 |
+
error_type = response.get("error", "unknown")
|
| 230 |
+
status_code = response.get("status_code", 0)
|
| 231 |
+
raw_response = response.get("raw_response", "")
|
| 232 |
+
|
| 233 |
+
# 对于常见错误(如401),只显示raw_response
|
| 234 |
+
if status_code == 401 and raw_response:
|
| 235 |
+
print(f"\n{'=' * 60}")
|
| 236 |
+
print(f"API错误 (状态码: {status_code}):")
|
| 237 |
+
print(f"{'=' * 60}")
|
| 238 |
+
print(raw_response)
|
| 239 |
+
print(f"{'=' * 60}")
|
| 240 |
+
else:
|
| 241 |
+
# 其他错误显示完整信息
|
| 242 |
+
print(f"\n{'=' * 60}")
|
| 243 |
+
print(f"API错误响应:")
|
| 244 |
+
print(f"{'=' * 60}")
|
| 245 |
+
print(json.dumps(response, indent=2, ensure_ascii=False))
|
| 246 |
+
print(f"{'=' * 60}")
|
| 247 |
+
|
| 248 |
+
result["error"] = response.get("error", "unknown_error")
|
| 249 |
+
result["status_code"] = status_code
|
| 250 |
+
return result
|
| 251 |
+
|
| 252 |
+
# 从API响应中提取文章列表
|
| 253 |
+
articles = []
|
| 254 |
+
if isinstance(response, dict):
|
| 255 |
+
# 优先使用 data 字段,兼容 result 字段
|
| 256 |
+
if "data" in response:
|
| 257 |
+
articles = response["data"]
|
| 258 |
+
result["total"] = response.get("total", len(articles))
|
| 259 |
+
elif "result" in response:
|
| 260 |
+
articles = response["result"]
|
| 261 |
+
result["total"] = response.get("total", len(articles))
|
| 262 |
+
elif isinstance(response, list):
|
| 263 |
+
articles = response
|
| 264 |
+
|
| 265 |
+
# 数据处理
|
| 266 |
+
if articles:
|
| 267 |
+
# 按日期过滤
|
| 268 |
+
if args.date_from or args.date_to:
|
| 269 |
+
articles = self.data_processor.filter_by_date(
|
| 270 |
+
articles, args.date_from, args.date_to
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
# 按最近N天过滤
|
| 274 |
+
if args.days:
|
| 275 |
+
articles = self.data_processor.filter_by_days(articles, args.days)
|
| 276 |
+
|
| 277 |
+
# 排序
|
| 278 |
+
articles = self.data_processor.sort_articles(
|
| 279 |
+
articles, args.sort_by, args.sort_order
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
# 提取关键信息
|
| 283 |
+
articles = self.data_processor.extract_key_info(articles)
|
| 284 |
+
|
| 285 |
+
result["articles"] = articles
|
| 286 |
+
result["success"] = True
|
| 287 |
+
result["count"] = len(articles)
|
| 288 |
+
|
| 289 |
+
self.logger.info(f"搜索完成: 找到 {len(articles)} 篇研究报告")
|
| 290 |
+
|
| 291 |
+
except APIError as e:
|
| 292 |
+
result["error"] = str(e)
|
| 293 |
+
self.logger.error(f"API错误: {e}")
|
| 294 |
+
except Exception as e:
|
| 295 |
+
result["error"] = str(e)
|
| 296 |
+
self.logger.error(f"处理查询失败: {e}")
|
| 297 |
+
|
| 298 |
+
return result
|
| 299 |
+
|
| 300 |
+
def process_batch_queries(self, args: argparse.Namespace) -> Dict[str, Any]:
|
| 301 |
+
"""处理批量查询"""
|
| 302 |
+
result = {
|
| 303 |
+
"success": False,
|
| 304 |
+
"queries": {},
|
| 305 |
+
"total_articles": 0,
|
| 306 |
+
"error": None
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
try:
|
| 310 |
+
# 加载查询
|
| 311 |
+
queries = self.load_queries_from_file(args.input, args.input_format)
|
| 312 |
+
|
| 313 |
+
if not queries:
|
| 314 |
+
result["error"] = "没有找到有效的查询"
|
| 315 |
+
return result
|
| 316 |
+
|
| 317 |
+
# 批量搜索
|
| 318 |
+
self.logger.info(f"批量处理 {len(queries)} 个查询")
|
| 319 |
+
batch_results = self.api_client.batch_search(queries, args.limit)
|
| 320 |
+
|
| 321 |
+
# 处理每个查询的结果
|
| 322 |
+
processed_results = {}
|
| 323 |
+
total_articles = 0
|
| 324 |
+
|
| 325 |
+
for query, response in batch_results.items():
|
| 326 |
+
# 从API响应中提取文章列表
|
| 327 |
+
articles = []
|
| 328 |
+
if isinstance(response, dict):
|
| 329 |
+
# 优先使用 data 字段,兼容 result 字段
|
| 330 |
+
if "data" in response:
|
| 331 |
+
articles = response["data"]
|
| 332 |
+
elif "result" in response:
|
| 333 |
+
articles = response["result"]
|
| 334 |
+
elif isinstance(response, list):
|
| 335 |
+
articles = response
|
| 336 |
+
|
| 337 |
+
if articles:
|
| 338 |
+
# 数据处理
|
| 339 |
+
if args.date_from or args.date_to:
|
| 340 |
+
articles = self.data_processor.filter_by_date(
|
| 341 |
+
articles, args.date_from, args.date_to
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
if args.days:
|
| 345 |
+
articles = self.data_processor.filter_by_days(articles, args.days)
|
| 346 |
+
|
| 347 |
+
articles = self.data_processor.sort_articles(
|
| 348 |
+
articles, args.sort_by, args.sort_order
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
articles = self.data_processor.extract_key_info(articles)
|
| 352 |
+
|
| 353 |
+
processed_results[query] = {
|
| 354 |
+
"articles": articles,
|
| 355 |
+
"count": len(articles)
|
| 356 |
+
}
|
| 357 |
+
total_articles += len(articles)
|
| 358 |
+
|
| 359 |
+
result["queries"] = processed_results
|
| 360 |
+
result["total_articles"] = total_articles
|
| 361 |
+
result["success"] = True
|
| 362 |
+
|
| 363 |
+
self.logger.info(f"批量处理完成: {len(queries)} 个查询, 共 {total_articles} 篇研究报告")
|
| 364 |
+
|
| 365 |
+
except Exception as e:
|
| 366 |
+
result["error"] = str(e)
|
| 367 |
+
self.logger.error(f"批量处理失败: {e}")
|
| 368 |
+
|
| 369 |
+
return result
|
| 370 |
+
|
| 371 |
+
def save_results(self, result: Dict[str, Any], args: argparse.Namespace) -> None:
|
| 372 |
+
"""保存结果"""
|
| 373 |
+
try:
|
| 374 |
+
if not result.get("success"):
|
| 375 |
+
self.logger.warning("结果不成功,跳过保存")
|
| 376 |
+
return
|
| 377 |
+
|
| 378 |
+
output_format = args.format.lower()
|
| 379 |
+
|
| 380 |
+
# 单个查询结果
|
| 381 |
+
if "query" in result:
|
| 382 |
+
articles = result.get("articles", [])
|
| 383 |
+
|
| 384 |
+
if not articles:
|
| 385 |
+
self.logger.warning("没有研究报告可保存")
|
| 386 |
+
return
|
| 387 |
+
|
| 388 |
+
if args.output:
|
| 389 |
+
output_path = args.output
|
| 390 |
+
|
| 391 |
+
if output_format == "csv":
|
| 392 |
+
self.data_processor.save_to_csv(articles, output_path)
|
| 393 |
+
elif output_format == "json":
|
| 394 |
+
self.data_processor.save_to_json(articles, output_path)
|
| 395 |
+
elif output_format == "markdown":
|
| 396 |
+
self.data_processor.save_to_markdown(articles, output_path)
|
| 397 |
+
elif output_format == "text":
|
| 398 |
+
self._save_to_text(articles, output_path)
|
| 399 |
+
else:
|
| 400 |
+
self.logger.error(f"不支持的输出格式: {output_format}")
|
| 401 |
+
|
| 402 |
+
else:
|
| 403 |
+
# 输出到控制台
|
| 404 |
+
self._print_to_console(articles, output_format)
|
| 405 |
+
|
| 406 |
+
# 批量查询结果
|
| 407 |
+
elif "queries" in result:
|
| 408 |
+
if args.output_dir:
|
| 409 |
+
output_dir = Path(args.output_dir)
|
| 410 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 411 |
+
|
| 412 |
+
for query, query_result in result["queries"].items():
|
| 413 |
+
articles = query_result.get("articles", [])
|
| 414 |
+
|
| 415 |
+
if articles:
|
| 416 |
+
# 创建安全的文件名
|
| 417 |
+
safe_query = "".join(c if c.isalnum() else "_" for c in query)
|
| 418 |
+
if len(safe_query) > 50:
|
| 419 |
+
safe_query = safe_query[:50]
|
| 420 |
+
|
| 421 |
+
if output_format == "csv":
|
| 422 |
+
output_path = output_dir / f"{safe_query}.csv"
|
| 423 |
+
self.data_processor.save_to_csv(articles, str(output_path))
|
| 424 |
+
elif output_format == "json":
|
| 425 |
+
output_path = output_dir / f"{safe_query}.json"
|
| 426 |
+
self.data_processor.save_to_json(articles, str(output_path))
|
| 427 |
+
elif output_format == "markdown":
|
| 428 |
+
output_path = output_dir / f"{safe_query}.md"
|
| 429 |
+
self.data_processor.save_to_markdown(articles, str(output_path))
|
| 430 |
+
elif output_format == "text":
|
| 431 |
+
output_path = output_dir / f"{safe_query}.txt"
|
| 432 |
+
self._save_to_text(articles, str(output_path))
|
| 433 |
+
|
| 434 |
+
# 保存汇总文件
|
| 435 |
+
summary_path = output_dir / f"summary.{output_format}"
|
| 436 |
+
if output_format == "json":
|
| 437 |
+
with open(summary_path, 'w', encoding='utf-8') as f:
|
| 438 |
+
json.dump(result, f, ensure_ascii=False, indent=2)
|
| 439 |
+
|
| 440 |
+
self.logger.info(f"批量结果保存到目录: {output_dir}")
|
| 441 |
+
|
| 442 |
+
else:
|
| 443 |
+
# 输出汇总信息到控制台
|
| 444 |
+
self._print_batch_summary(result)
|
| 445 |
+
|
| 446 |
+
except Exception as e:
|
| 447 |
+
self.logger.error(f"保存结果失败: {e}")
|
| 448 |
+
raise
|
| 449 |
+
|
| 450 |
+
def _save_to_text(self, articles: List[Dict[str, Any]], filepath: str) -> None:
|
| 451 |
+
"""保存到文本文件"""
|
| 452 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 453 |
+
f.write(f"研究报告搜索结果\n")
|
| 454 |
+
f.write(f"搜索时间: {self._get_current_time()}\n")
|
| 455 |
+
f.write(f"报告数量: {len(articles)} 篇\n")
|
| 456 |
+
f.write("=" * 80 + "\n\n")
|
| 457 |
+
|
| 458 |
+
for i, article in enumerate(articles, 1):
|
| 459 |
+
f.write(f"{i}. {article.get('title', '无标题')}\n")
|
| 460 |
+
f.write(f" 发布时间: {article.get('publish_date', '未知')}\n")
|
| 461 |
+
f.write(f" 原文链接: {article.get('url', '')}\n")
|
| 462 |
+
|
| 463 |
+
summary = article.get("summary", "")
|
| 464 |
+
if summary:
|
| 465 |
+
f.write(f" 摘要: {summary[:200]}...\n")
|
| 466 |
+
|
| 467 |
+
# 提取的信息
|
| 468 |
+
if "extracted_info" in article:
|
| 469 |
+
extracted = article["extracted_info"]
|
| 470 |
+
info_parts = []
|
| 471 |
+
|
| 472 |
+
if extracted.get("rating"):
|
| 473 |
+
info_parts.append(f"评级: {extracted['rating']}")
|
| 474 |
+
if extracted.get("target_price"):
|
| 475 |
+
info_parts.append(f"目标价: {extracted['target_price']}元")
|
| 476 |
+
if extracted.get("industry"):
|
| 477 |
+
info_parts.append(f"行业: {extracted['industry']}")
|
| 478 |
+
|
| 479 |
+
if info_parts:
|
| 480 |
+
f.write(f" 提取信息: {', '.join(info_parts)}\n")
|
| 481 |
+
|
| 482 |
+
f.write("\n")
|
| 483 |
+
|
| 484 |
+
self.logger.info(f"保存到文本文件: {len(articles)} 篇研究报告 -> {filepath}")
|
| 485 |
+
|
| 486 |
+
def _print_to_console(self, articles: List[Dict[str, Any]], output_format: str) -> None:
|
| 487 |
+
"""输出到控制台"""
|
| 488 |
+
if output_format == "text":
|
| 489 |
+
print(f"\n研究报告搜索结果")
|
| 490 |
+
print(f"搜索时间: {self._get_current_time()}")
|
| 491 |
+
print(f"报告数量: {len(articles)} 篇")
|
| 492 |
+
print("=" * 80)
|
| 493 |
+
|
| 494 |
+
for i, article in enumerate(articles, 1):
|
| 495 |
+
print(f"\n{i}. {article.get('title', '无标题')}")
|
| 496 |
+
print(f" 发布时间: {article.get('publish_date', '未知')}")
|
| 497 |
+
print(f" 原文链接: {article.get('url', '')}")
|
| 498 |
+
|
| 499 |
+
summary = article.get("summary", "")
|
| 500 |
+
if summary:
|
| 501 |
+
print(f" 摘要: {summary[:150]}...")
|
| 502 |
+
|
| 503 |
+
# 提取的信息
|
| 504 |
+
if "extracted_info" in article:
|
| 505 |
+
extracted = article["extracted_info"]
|
| 506 |
+
info_parts = []
|
| 507 |
+
|
| 508 |
+
if extracted.get("rating"):
|
| 509 |
+
info_parts.append(f"评级: {extracted['rating']}")
|
| 510 |
+
if extracted.get("target_price"):
|
| 511 |
+
info_parts.append(f"目标价: {extracted['target_price']}元")
|
| 512 |
+
if extracted.get("industry"):
|
| 513 |
+
info_parts.append(f"行业: {extracted['industry']}")
|
| 514 |
+
|
| 515 |
+
if info_parts:
|
| 516 |
+
print(f" 提取信息: {', '.join(info_parts)}")
|
| 517 |
+
|
| 518 |
+
print("\n" + "=" * 80)
|
| 519 |
+
|
| 520 |
+
elif output_format == "json":
|
| 521 |
+
print(json.dumps(articles, ensure_ascii=False, indent=2))
|
| 522 |
+
|
| 523 |
+
else:
|
| 524 |
+
# 默��简单输出
|
| 525 |
+
for i, article in enumerate(articles, 1):
|
| 526 |
+
print(f"{i}. {article.get('title', '无标题')}")
|
| 527 |
+
if i >= 10: # 只显示前10条
|
| 528 |
+
print(f"... 还有 {len(articles) - 10} 条结果")
|
| 529 |
+
break
|
| 530 |
+
|
| 531 |
+
def _print_batch_summary(self, result: Dict[str, Any]) -> None:
|
| 532 |
+
"""输出批量处理摘要"""
|
| 533 |
+
print(f"\n批量处理结果摘要")
|
| 534 |
+
print(f"处理时间: {self._get_current_time()}")
|
| 535 |
+
print(f"查询数量: {len(result.get('queries', {}))}")
|
| 536 |
+
print(f"总报告数量: {result.get('total_articles', 0)}")
|
| 537 |
+
print("=" * 80)
|
| 538 |
+
|
| 539 |
+
for query, query_result in result.get("queries", {}).items():
|
| 540 |
+
count = query_result.get("count", 0)
|
| 541 |
+
print(f" {query}: {count} 篇")
|
| 542 |
+
|
| 543 |
+
print("=" * 80)
|
| 544 |
+
|
| 545 |
+
def _get_current_time(self) -> str:
|
| 546 |
+
"""获取当前时间字符串"""
|
| 547 |
+
from datetime import datetime
|
| 548 |
+
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 549 |
+
|
| 550 |
+
def test_connection(self) -> bool:
|
| 551 |
+
"""测试API连接"""
|
| 552 |
+
try:
|
| 553 |
+
self.logger.info("测试API连接...")
|
| 554 |
+
success = self.api_client.test_connection()
|
| 555 |
+
|
| 556 |
+
if success:
|
| 557 |
+
print("API连接测试成功")
|
| 558 |
+
self.logger.info("API连接测试成功")
|
| 559 |
+
else:
|
| 560 |
+
print("API连接测试失败")
|
| 561 |
+
self.logger.warning("API连接测试失败")
|
| 562 |
+
|
| 563 |
+
return success
|
| 564 |
+
|
| 565 |
+
except Exception as e:
|
| 566 |
+
print(f"API连接测试失败: {e}")
|
| 567 |
+
self.logger.error(f"API连接测试失败: {e}")
|
| 568 |
+
return False
|
| 569 |
+
|
| 570 |
+
def run(self) -> int:
|
| 571 |
+
"""运行CLI"""
|
| 572 |
+
try:
|
| 573 |
+
# 解析参数
|
| 574 |
+
args = self.parse_arguments()
|
| 575 |
+
|
| 576 |
+
# 设置日志级别
|
| 577 |
+
if args.debug:
|
| 578 |
+
logging.getLogger().setLevel(logging.DEBUG)
|
| 579 |
+
elif args.verbose:
|
| 580 |
+
logging.getLogger().setLevel(logging.INFO)
|
| 581 |
+
|
| 582 |
+
# 初始化
|
| 583 |
+
self.initialize(args.config)
|
| 584 |
+
|
| 585 |
+
# 测试模式
|
| 586 |
+
if args.test:
|
| 587 |
+
return 0 if self.test_connection() else 1
|
| 588 |
+
|
| 589 |
+
# 检查必要的参数
|
| 590 |
+
if not args.query and not args.input:
|
| 591 |
+
print("错误: 必须指定查询参数 (-q/--query) 或输入文件 (-i/--input)")
|
| 592 |
+
return 1
|
| 593 |
+
|
| 594 |
+
# 处理查询
|
| 595 |
+
if args.input:
|
| 596 |
+
# 批量处理
|
| 597 |
+
result = self.process_batch_queries(args)
|
| 598 |
+
else:
|
| 599 |
+
# 单个查询
|
| 600 |
+
result = self.process_single_query(args)
|
| 601 |
+
|
| 602 |
+
# 保存结果
|
| 603 |
+
if result.get("success"):
|
| 604 |
+
self.save_results(result, args)
|
| 605 |
+
|
| 606 |
+
# 显示成功消息
|
| 607 |
+
if args.input:
|
| 608 |
+
print(f"\n批量处理完成: {result.get('total_articles', 0)} 篇研究报告")
|
| 609 |
+
else:
|
| 610 |
+
print(f"\n搜索完成: {result.get('count', 0)} 篇研究报告")
|
| 611 |
+
|
| 612 |
+
return 0
|
| 613 |
+
else:
|
| 614 |
+
print(f"\n处理失败: {result.get('error', '未知错误')}")
|
| 615 |
+
return 1
|
| 616 |
+
|
| 617 |
+
except KeyboardInterrupt:
|
| 618 |
+
print("\n用户中断")
|
| 619 |
+
return 130
|
| 620 |
+
except Exception as e:
|
| 621 |
+
self.logger.error(f"CLI运行失败: {e}")
|
| 622 |
+
print(f"错误: {e}")
|
| 623 |
+
return 1
|
| 624 |
+
|
| 625 |
+
|
| 626 |
+
def main():
|
| 627 |
+
"""主函数"""
|
| 628 |
+
cli = ResearchReportCLI()
|
| 629 |
+
return cli.run()
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
if __name__ == "__main__":
|
| 633 |
+
sys.exit(main())
|
Skills/report-search/scripts/config.example.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"api": {
|
| 3 |
+
"base_url": "https://openapi.iwencai.com",
|
| 4 |
+
"endpoint": "/v1/comprehensive/search",
|
| 5 |
+
"timeout": 30,
|
| 6 |
+
"max_retries": 3,
|
| 7 |
+
"retry_delay": 1.0
|
| 8 |
+
},
|
| 9 |
+
"search": {
|
| 10 |
+
"channels": ["report"],
|
| 11 |
+
"app_id": "AIME_SKILL",
|
| 12 |
+
"default_limit": 10,
|
| 13 |
+
"default_days": 30,
|
| 14 |
+
"min_articles_for_sufficient": 3
|
| 15 |
+
},
|
| 16 |
+
"output": {
|
| 17 |
+
"default_format": "text",
|
| 18 |
+
"csv_encoding": "utf-8-sig",
|
| 19 |
+
"json_indent": 2
|
| 20 |
+
},
|
| 21 |
+
"logging": {
|
| 22 |
+
"level": "INFO",
|
| 23 |
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 24 |
+
},
|
| 25 |
+
"注意": "API密钥应从环境变量 IWENCAI_API_KEY 获取,不要在此配置文件中硬编码"
|
| 26 |
+
}
|
Skills/report-search/scripts/config.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
研报搜索技能配置管理模块
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import json
|
| 9 |
+
import logging
|
| 10 |
+
from typing import Dict, Any, Optional
|
| 11 |
+
|
| 12 |
+
# 默认配置
|
| 13 |
+
DEFAULT_CONFIG = {
|
| 14 |
+
"api": {
|
| 15 |
+
"base_url": "https://openapi.iwencai.com",
|
| 16 |
+
"endpoint": "/v1/comprehensive/search",
|
| 17 |
+
"timeout": 30,
|
| 18 |
+
"max_retries": 3,
|
| 19 |
+
"retry_delay": 1.0
|
| 20 |
+
},
|
| 21 |
+
"search": {
|
| 22 |
+
"channels": ["report"],
|
| 23 |
+
"app_id": "AIME_SKILL",
|
| 24 |
+
"default_limit": 10,
|
| 25 |
+
"default_days": 30,
|
| 26 |
+
"min_articles_for_sufficient": 3
|
| 27 |
+
},
|
| 28 |
+
"output": {
|
| 29 |
+
"default_format": "text",
|
| 30 |
+
"csv_encoding": "utf-8-sig",
|
| 31 |
+
"json_indent": 2
|
| 32 |
+
},
|
| 33 |
+
"logging": {
|
| 34 |
+
"level": "INFO",
|
| 35 |
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class Config:
|
| 41 |
+
"""配置管理类"""
|
| 42 |
+
|
| 43 |
+
def __init__(self, config_file: Optional[str] = None):
|
| 44 |
+
"""
|
| 45 |
+
初始化配置
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
config_file: 配置文件路径,如果为None则使用默认配置
|
| 49 |
+
"""
|
| 50 |
+
self.config = DEFAULT_CONFIG.copy()
|
| 51 |
+
|
| 52 |
+
# 从配置文件加载配置(如果提供)
|
| 53 |
+
if config_file and os.path.exists(config_file):
|
| 54 |
+
self.load_from_file(config_file)
|
| 55 |
+
|
| 56 |
+
# 从环境变量覆盖配置
|
| 57 |
+
self.load_from_env()
|
| 58 |
+
|
| 59 |
+
# 验证配置
|
| 60 |
+
self.validate()
|
| 61 |
+
|
| 62 |
+
def load_from_file(self, config_file: str) -> None:
|
| 63 |
+
"""从配置文件加载配置"""
|
| 64 |
+
try:
|
| 65 |
+
with open(config_file, 'r', encoding='utf-8') as f:
|
| 66 |
+
file_config = json.load(f)
|
| 67 |
+
self._merge_config(file_config)
|
| 68 |
+
logging.info(f"从文件加载配置: {config_file}")
|
| 69 |
+
except (json.JSONDecodeError, IOError) as e:
|
| 70 |
+
logging.warning(f"加载配置文件失败 {config_file}: {e}, 使用默认配置")
|
| 71 |
+
|
| 72 |
+
def load_from_env(self) -> None:
|
| 73 |
+
"""从环境变量加载配置"""
|
| 74 |
+
# API配置
|
| 75 |
+
api_key = os.getenv("IWENCAI_API_KEY")
|
| 76 |
+
if api_key:
|
| 77 |
+
# API Key存储在环境变量中,不在配置中硬编码
|
| 78 |
+
logging.info("从环境变量加载API Key")
|
| 79 |
+
|
| 80 |
+
# 超时时间
|
| 81 |
+
timeout = os.getenv("API_TIMEOUT")
|
| 82 |
+
if timeout:
|
| 83 |
+
try:
|
| 84 |
+
self.config["api"]["timeout"] = int(timeout)
|
| 85 |
+
except ValueError:
|
| 86 |
+
pass
|
| 87 |
+
|
| 88 |
+
# 日志级别
|
| 89 |
+
log_level = os.getenv("LOG_LEVEL")
|
| 90 |
+
if log_level:
|
| 91 |
+
self.config["logging"]["level"] = log_level.upper()
|
| 92 |
+
|
| 93 |
+
def _merge_config(self, new_config: Dict[str, Any]) -> None:
|
| 94 |
+
"""合并配置"""
|
| 95 |
+
for key, value in new_config.items():
|
| 96 |
+
if key in self.config and isinstance(self.config[key], dict) and isinstance(value, dict):
|
| 97 |
+
# 递归合并字典
|
| 98 |
+
self.config[key].update(value)
|
| 99 |
+
else:
|
| 100 |
+
# 直接覆盖
|
| 101 |
+
self.config[key] = value
|
| 102 |
+
|
| 103 |
+
def validate(self) -> None:
|
| 104 |
+
"""验证配置"""
|
| 105 |
+
# 检查必要的API配置
|
| 106 |
+
if not self.config["api"]["base_url"]:
|
| 107 |
+
raise ValueError("API base_url 不能为空")
|
| 108 |
+
|
| 109 |
+
if not self.config["api"]["endpoint"]:
|
| 110 |
+
raise ValueError("API endpoint 不能为空")
|
| 111 |
+
|
| 112 |
+
# 检查搜索配置
|
| 113 |
+
if not self.config["search"]["channels"]:
|
| 114 |
+
raise ValueError("搜索渠道 channels 不能为空")
|
| 115 |
+
|
| 116 |
+
if not self.config["search"]["app_id"]:
|
| 117 |
+
raise ValueError("应用ID app_id 不能为空")
|
| 118 |
+
|
| 119 |
+
# 检查超时时间
|
| 120 |
+
if self.config["api"]["timeout"] <= 0:
|
| 121 |
+
raise ValueError("API timeout 必须大于0")
|
| 122 |
+
|
| 123 |
+
# 检查重试次数
|
| 124 |
+
if self.config["api"]["max_retries"] < 0:
|
| 125 |
+
raise ValueError("API max_retries 不能为负数")
|
| 126 |
+
|
| 127 |
+
def get(self, key: str, default: Any = None) -> Any:
|
| 128 |
+
"""获取配置值"""
|
| 129 |
+
keys = key.split('.')
|
| 130 |
+
value = self.config
|
| 131 |
+
|
| 132 |
+
for k in keys:
|
| 133 |
+
if isinstance(value, dict) and k in value:
|
| 134 |
+
value = value[k]
|
| 135 |
+
else:
|
| 136 |
+
return default
|
| 137 |
+
|
| 138 |
+
return value
|
| 139 |
+
|
| 140 |
+
def set(self, key: str, value: Any) -> None:
|
| 141 |
+
"""设置配置值"""
|
| 142 |
+
keys = key.split('.')
|
| 143 |
+
config = self.config
|
| 144 |
+
|
| 145 |
+
for i, k in enumerate(keys[:-1]):
|
| 146 |
+
if k not in config:
|
| 147 |
+
config[k] = {}
|
| 148 |
+
config = config[k]
|
| 149 |
+
|
| 150 |
+
config[keys[-1]] = value
|
| 151 |
+
|
| 152 |
+
def get_api_key(self) -> str:
|
| 153 |
+
"""获取API Key(从环境变量)"""
|
| 154 |
+
api_key = os.getenv("IWENCAI_API_KEY")
|
| 155 |
+
if not api_key:
|
| 156 |
+
raise ValueError("请设置环境变量 IWENCAI_API_KEY")
|
| 157 |
+
return api_key
|
| 158 |
+
|
| 159 |
+
def get_api_url(self) -> str:
|
| 160 |
+
"""获取完整的API URL"""
|
| 161 |
+
base_url = self.config["api"]["base_url"].rstrip('/')
|
| 162 |
+
endpoint = self.config["api"]["endpoint"].lstrip('/')
|
| 163 |
+
return f"{base_url}/{endpoint}"
|
| 164 |
+
|
| 165 |
+
def setup_logging(self) -> None:
|
| 166 |
+
"""设置日志"""
|
| 167 |
+
log_level = self.config["logging"]["level"]
|
| 168 |
+
log_format = self.config["logging"]["format"]
|
| 169 |
+
|
| 170 |
+
# 设置日志级别
|
| 171 |
+
numeric_level = getattr(logging, log_level.upper(), None)
|
| 172 |
+
if not isinstance(numeric_level, int):
|
| 173 |
+
numeric_level = logging.INFO
|
| 174 |
+
|
| 175 |
+
logging.basicConfig(
|
| 176 |
+
level=numeric_level,
|
| 177 |
+
format=log_format,
|
| 178 |
+
datefmt="%Y-%m-%d %H:%M:%S"
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
# 全局配置实例
|
| 183 |
+
_config_instance: Optional[Config] = None
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def get_config(config_file: Optional[str] = None) -> Config:
|
| 187 |
+
"""获取全局配置实例"""
|
| 188 |
+
global _config_instance
|
| 189 |
+
if _config_instance is None:
|
| 190 |
+
_config_instance = Config(config_file)
|
| 191 |
+
return _config_instance
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
if __name__ == "__main__":
|
| 195 |
+
# 测试配置模块
|
| 196 |
+
config = get_config()
|
| 197 |
+
print("API URL:", config.get_api_url())
|
| 198 |
+
print("搜索渠道:", config.get("search.channels"))
|
| 199 |
+
print("默认限制:", config.get("search.default_limit"))
|
| 200 |
+
|
| 201 |
+
# 测试环境变量
|
| 202 |
+
try:
|
| 203 |
+
api_key = config.get_api_key()
|
| 204 |
+
print("API Key:", "已设置" if api_key else "未设置")
|
| 205 |
+
except ValueError as e:
|
| 206 |
+
print(f"API Key错误: {e}")
|
Skills/report-search/scripts/data_processor.py
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
研报搜索技能数据处理模块
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
import csv
|
| 9 |
+
import logging
|
| 10 |
+
from typing import Dict, List, Any, Optional, Tuple
|
| 11 |
+
from datetime import datetime, timedelta
|
| 12 |
+
import pandas as pd
|
| 13 |
+
import numpy as np
|
| 14 |
+
|
| 15 |
+
from config import get_config
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class DataProcessor:
|
| 19 |
+
"""数据处理类"""
|
| 20 |
+
|
| 21 |
+
def __init__(self, config_file: Optional[str] = None):
|
| 22 |
+
"""
|
| 23 |
+
初始化数据处理类
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
config_file: 配置文件路径
|
| 27 |
+
"""
|
| 28 |
+
self.config = get_config(config_file)
|
| 29 |
+
|
| 30 |
+
# 设置日志
|
| 31 |
+
self.config.setup_logging()
|
| 32 |
+
self.logger = logging.getLogger(__name__)
|
| 33 |
+
|
| 34 |
+
# 初始化关键词提取器(简单版本)
|
| 35 |
+
self._init_keyword_extractor()
|
| 36 |
+
|
| 37 |
+
def _init_keyword_extractor(self) -> None:
|
| 38 |
+
"""初始化关键词提取器(简单实现)"""
|
| 39 |
+
# 行业关键词
|
| 40 |
+
self.industry_keywords = [
|
| 41 |
+
"人工智能", "芯片", "半导体", "新能源", "电动汽车", "医药", "医疗", "金融",
|
| 42 |
+
"银行", "保险", "证券", "消费", "零售", "制造", "工业", "科技", "互联网",
|
| 43 |
+
"软件", "硬件", "通信", "5G", "物联网", "云计算", "大数据", "区块链"
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
# 报告类型关键词
|
| 47 |
+
self.report_type_keywords = [
|
| 48 |
+
"研究报告", "分析报告", "行业报告", "深度报告", "投资报告", "市场报告",
|
| 49 |
+
"趋势报告", "前景分析", "投资建议", "评级报告", "目标价"
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
def filter_by_date(self, articles: List[Dict[str, Any]],
|
| 53 |
+
date_from: Optional[str] = None,
|
| 54 |
+
date_to: Optional[str] = None) -> List[Dict[str, Any]]:
|
| 55 |
+
"""
|
| 56 |
+
按日期过滤研究报告
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
articles: 研究报告列表
|
| 60 |
+
date_from: 开始日期 (YYYY-MM-DD)
|
| 61 |
+
date_to: 结束日期 (YYYY-MM-DD)
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
过滤后的研究报告列表
|
| 65 |
+
"""
|
| 66 |
+
if not articles:
|
| 67 |
+
return []
|
| 68 |
+
|
| 69 |
+
filtered_articles = []
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
# 解析日期范围
|
| 73 |
+
from_date = None
|
| 74 |
+
to_date = None
|
| 75 |
+
|
| 76 |
+
if date_from:
|
| 77 |
+
from_date = datetime.strptime(date_from, "%Y-%m-%d")
|
| 78 |
+
|
| 79 |
+
if date_to:
|
| 80 |
+
to_date = datetime.strptime(date_to, "%Y-%m-%d")
|
| 81 |
+
|
| 82 |
+
# 过滤文章
|
| 83 |
+
for article in articles:
|
| 84 |
+
publish_date_str = article.get("publish_date", "")
|
| 85 |
+
|
| 86 |
+
if not publish_date_str:
|
| 87 |
+
# 如果没有发布日期,默认保留
|
| 88 |
+
filtered_articles.append(article)
|
| 89 |
+
continue
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
# 尝试解析发布日期
|
| 93 |
+
# 格式可能是 "YYYY-MM-DD HH:MM:SS" 或 "YYYY-MM-DD"
|
| 94 |
+
if " " in publish_date_str:
|
| 95 |
+
publish_date = datetime.strptime(publish_date_str, "%Y-%m-%d %H:%M:%S")
|
| 96 |
+
else:
|
| 97 |
+
publish_date = datetime.strptime(publish_date_str, "%Y-%m-%d")
|
| 98 |
+
|
| 99 |
+
# 检查是否在日期范围内
|
| 100 |
+
if from_date and publish_date < from_date:
|
| 101 |
+
continue
|
| 102 |
+
|
| 103 |
+
if to_date and publish_date > to_date:
|
| 104 |
+
continue
|
| 105 |
+
|
| 106 |
+
filtered_articles.append(article)
|
| 107 |
+
|
| 108 |
+
except ValueError:
|
| 109 |
+
# 日期格式解析失败,保留文章
|
| 110 |
+
filtered_articles.append(article)
|
| 111 |
+
|
| 112 |
+
self.logger.info(f"按日期过滤: 从 {len(articles)} 篇过滤到 {len(filtered_articles)} 篇")
|
| 113 |
+
|
| 114 |
+
except Exception as e:
|
| 115 |
+
self.logger.error(f"按日期过滤失败: {e}")
|
| 116 |
+
# 出错时返回原始数据
|
| 117 |
+
return articles
|
| 118 |
+
|
| 119 |
+
return filtered_articles
|
| 120 |
+
|
| 121 |
+
def filter_by_days(self, articles: List[Dict[str, Any]], days: int) -> List[Dict[str, Any]]:
|
| 122 |
+
"""
|
| 123 |
+
按最近N天过滤研究报告
|
| 124 |
+
|
| 125 |
+
Args:
|
| 126 |
+
articles: 研究报告列表
|
| 127 |
+
days: 最近N天
|
| 128 |
+
|
| 129 |
+
Returns:
|
| 130 |
+
过滤后的研究报告列表
|
| 131 |
+
"""
|
| 132 |
+
if not articles or days <= 0:
|
| 133 |
+
return articles
|
| 134 |
+
|
| 135 |
+
try:
|
| 136 |
+
# 计算截止日期
|
| 137 |
+
cutoff_date = datetime.now() - timedelta(days=days)
|
| 138 |
+
|
| 139 |
+
filtered_articles = []
|
| 140 |
+
for article in articles:
|
| 141 |
+
publish_date_str = article.get("publish_date", "")
|
| 142 |
+
|
| 143 |
+
if not publish_date_str:
|
| 144 |
+
# 如果没有发布日期,默认保留
|
| 145 |
+
filtered_articles.append(article)
|
| 146 |
+
continue
|
| 147 |
+
|
| 148 |
+
try:
|
| 149 |
+
# 尝试解析发布日期
|
| 150 |
+
if " " in publish_date_str:
|
| 151 |
+
publish_date = datetime.strptime(publish_date_str, "%Y-%m-%d %H:%M:%S")
|
| 152 |
+
else:
|
| 153 |
+
publish_date = datetime.strptime(publish_date_str, "%Y-%m-%d")
|
| 154 |
+
|
| 155 |
+
# 检查是否在最近N天内
|
| 156 |
+
if publish_date >= cutoff_date:
|
| 157 |
+
filtered_articles.append(article)
|
| 158 |
+
|
| 159 |
+
except ValueError:
|
| 160 |
+
# 日期格式解析失败,保留文章
|
| 161 |
+
filtered_articles.append(article)
|
| 162 |
+
|
| 163 |
+
self.logger.info(f"按最近 {days} 天过滤: 从 {len(articles)} 篇过滤到 {len(filtered_articles)} 篇")
|
| 164 |
+
|
| 165 |
+
return filtered_articles
|
| 166 |
+
|
| 167 |
+
except Exception as e:
|
| 168 |
+
self.logger.error(f"按天数过滤失败: {e}")
|
| 169 |
+
return articles
|
| 170 |
+
|
| 171 |
+
def sort_articles(self, articles: List[Dict[str, Any]],
|
| 172 |
+
sort_by: str = "date",
|
| 173 |
+
sort_order: str = "desc") -> List[Dict[str, Any]]:
|
| 174 |
+
"""
|
| 175 |
+
排序研究报告
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
articles: 研究报告列表
|
| 179 |
+
sort_by: 排序字段 (date, relevance)
|
| 180 |
+
sort_order: 排序顺序 (asc, desc)
|
| 181 |
+
|
| 182 |
+
Returns:
|
| 183 |
+
排序后的研究报告列表
|
| 184 |
+
"""
|
| 185 |
+
if not articles:
|
| 186 |
+
return []
|
| 187 |
+
|
| 188 |
+
try:
|
| 189 |
+
# 创建可排序的副本
|
| 190 |
+
sorted_articles = articles.copy()
|
| 191 |
+
|
| 192 |
+
if sort_by == "date":
|
| 193 |
+
# 按日期排序
|
| 194 |
+
def get_date(article):
|
| 195 |
+
publish_date_str = article.get("publish_date", "")
|
| 196 |
+
if not publish_date_str:
|
| 197 |
+
return datetime.min
|
| 198 |
+
|
| 199 |
+
try:
|
| 200 |
+
if " " in publish_date_str:
|
| 201 |
+
return datetime.strptime(publish_date_str, "%Y-%m-%d %H:%M:%S")
|
| 202 |
+
else:
|
| 203 |
+
return datetime.strptime(publish_date_str, "%Y-%m-%d")
|
| 204 |
+
except ValueError:
|
| 205 |
+
return datetime.min
|
| 206 |
+
|
| 207 |
+
sorted_articles.sort(key=get_date, reverse=(sort_order == "desc"))
|
| 208 |
+
|
| 209 |
+
elif sort_by == "relevance":
|
| 210 |
+
# 按相关性排序(简单实现:按标题长度和关键词匹配)
|
| 211 |
+
def get_relevance_score(article):
|
| 212 |
+
title = article.get("title", "").lower()
|
| 213 |
+
summary = article.get("summary", "").lower()
|
| 214 |
+
|
| 215 |
+
score = 0
|
| 216 |
+
|
| 217 |
+
# 标题长度(适中为好)
|
| 218 |
+
title_len = len(title)
|
| 219 |
+
if 10 <= title_len <= 50:
|
| 220 |
+
score += 1
|
| 221 |
+
|
| 222 |
+
# 包含关键词
|
| 223 |
+
for keyword in self.industry_keywords + self.report_type_keywords:
|
| 224 |
+
if keyword in title or keyword in summary:
|
| 225 |
+
score += 2
|
| 226 |
+
|
| 227 |
+
# 包含数字(可能表示目标价或日期)
|
| 228 |
+
if any(char.isdigit() for char in title):
|
| 229 |
+
score += 1
|
| 230 |
+
|
| 231 |
+
return score
|
| 232 |
+
|
| 233 |
+
sorted_articles.sort(key=get_relevance_score, reverse=(sort_order == "desc"))
|
| 234 |
+
|
| 235 |
+
self.logger.info(f"按 {sort_by} 排序 ({sort_order}): 排序 {len(sorted_articles)} 篇研究报告")
|
| 236 |
+
|
| 237 |
+
return sorted_articles
|
| 238 |
+
|
| 239 |
+
except Exception as e:
|
| 240 |
+
self.logger.error(f"排序失败: {e}")
|
| 241 |
+
return articles
|
| 242 |
+
|
| 243 |
+
def extract_key_info(self, articles: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
| 244 |
+
"""
|
| 245 |
+
提取关键信息
|
| 246 |
+
|
| 247 |
+
Args:
|
| 248 |
+
articles: 研究报告列表
|
| 249 |
+
|
| 250 |
+
Returns:
|
| 251 |
+
包含提取信息的研究报告列表
|
| 252 |
+
"""
|
| 253 |
+
if not articles:
|
| 254 |
+
return []
|
| 255 |
+
|
| 256 |
+
processed_articles = []
|
| 257 |
+
|
| 258 |
+
for article in articles:
|
| 259 |
+
processed_article = article.copy()
|
| 260 |
+
|
| 261 |
+
# 提取标题中的关键信息
|
| 262 |
+
title = article.get("title", "")
|
| 263 |
+
summary = article.get("summary", "")
|
| 264 |
+
|
| 265 |
+
# 提取投资评级
|
| 266 |
+
rating_keywords = ["买入", "增持", "持有", "减持", "卖出", "推荐", "谨慎推荐"]
|
| 267 |
+
rating = None
|
| 268 |
+
for keyword in rating_keywords:
|
| 269 |
+
if keyword in title or keyword in summary:
|
| 270 |
+
rating = keyword
|
| 271 |
+
break
|
| 272 |
+
|
| 273 |
+
# 提取目标价
|
| 274 |
+
target_price = None
|
| 275 |
+
import re
|
| 276 |
+
price_patterns = [
|
| 277 |
+
r"目标价\s*[::]?\s*(\d+(?:\.\d+)?)\s*元",
|
| 278 |
+
r"(\d+(?:\.\d+)?)\s*元\s*目标价",
|
| 279 |
+
r"目标价\s*(\d+(?:\.\d+)?)"
|
| 280 |
+
]
|
| 281 |
+
|
| 282 |
+
for pattern in price_patterns:
|
| 283 |
+
match = re.search(pattern, title + " " + summary)
|
| 284 |
+
if match:
|
| 285 |
+
target_price = match.group(1)
|
| 286 |
+
break
|
| 287 |
+
|
| 288 |
+
# 提取行业
|
| 289 |
+
industry = None
|
| 290 |
+
for keyword in self.industry_keywords:
|
| 291 |
+
if keyword in title or keyword in summary:
|
| 292 |
+
industry = keyword
|
| 293 |
+
break
|
| 294 |
+
|
| 295 |
+
# 添加提取的信息
|
| 296 |
+
processed_article["extracted_info"] = {
|
| 297 |
+
"rating": rating,
|
| 298 |
+
"target_price": target_price,
|
| 299 |
+
"industry": industry,
|
| 300 |
+
"has_analysis": any(keyword in summary for keyword in ["分析", "逻辑", "原因", "因素"])
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
processed_articles.append(processed_article)
|
| 304 |
+
|
| 305 |
+
self.logger.info(f"提取关键信息: 处理 {len(processed_articles)} 篇研究报告")
|
| 306 |
+
|
| 307 |
+
return processed_articles
|
| 308 |
+
|
| 309 |
+
def save_to_csv(self, articles: List[Dict[str, Any]], filepath: str) -> None:
|
| 310 |
+
"""
|
| 311 |
+
保存研究报告到CSV文件
|
| 312 |
+
|
| 313 |
+
Args:
|
| 314 |
+
articles: 研究报告列表
|
| 315 |
+
filepath: 输出文件路径
|
| 316 |
+
"""
|
| 317 |
+
if not articles:
|
| 318 |
+
self.logger.warning("没有研究报告可保存")
|
| 319 |
+
return
|
| 320 |
+
|
| 321 |
+
try:
|
| 322 |
+
# 准备CSV数据
|
| 323 |
+
csv_data = []
|
| 324 |
+
for article in articles:
|
| 325 |
+
row = {
|
| 326 |
+
"title": article.get("title", ""),
|
| 327 |
+
"summary": article.get("summary", ""),
|
| 328 |
+
"url": article.get("url", ""),
|
| 329 |
+
"publish_date": article.get("publish_date", "")
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
# 添加提取的信息
|
| 333 |
+
if "extracted_info" in article:
|
| 334 |
+
extracted = article["extracted_info"]
|
| 335 |
+
row["rating"] = extracted.get("rating", "")
|
| 336 |
+
row["target_price"] = extracted.get("target_price", "")
|
| 337 |
+
row["industry"] = extracted.get("industry", "")
|
| 338 |
+
row["has_analysis"] = extracted.get("has_analysis", False)
|
| 339 |
+
|
| 340 |
+
csv_data.append(row)
|
| 341 |
+
|
| 342 |
+
# 保存到CSV
|
| 343 |
+
df = pd.DataFrame(csv_data)
|
| 344 |
+
df.to_csv(filepath, index=False, encoding='utf-8-sig')
|
| 345 |
+
|
| 346 |
+
self.logger.info(f"保存到CSV: {len(articles)} 篇研究报告 -> {filepath}")
|
| 347 |
+
|
| 348 |
+
except Exception as e:
|
| 349 |
+
self.logger.error(f"保存到CSV失败: {e}")
|
| 350 |
+
raise
|
| 351 |
+
|
| 352 |
+
def save_to_json(self, articles: List[Dict[str, Any]], filepath: str) -> None:
|
| 353 |
+
"""
|
| 354 |
+
保存研究报告到JSON文件
|
| 355 |
+
|
| 356 |
+
Args:
|
| 357 |
+
articles: 研究报告列表
|
| 358 |
+
filepath: 输出文件路径
|
| 359 |
+
"""
|
| 360 |
+
if not articles:
|
| 361 |
+
self.logger.warning("没有研究报告可保存")
|
| 362 |
+
return
|
| 363 |
+
|
| 364 |
+
try:
|
| 365 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 366 |
+
json.dump(articles, f, ensure_ascii=False, indent=2)
|
| 367 |
+
|
| 368 |
+
self.logger.info(f"保存到JSON: {len(articles)} 篇研究报告 -> {filepath}")
|
| 369 |
+
|
| 370 |
+
except Exception as e:
|
| 371 |
+
self.logger.error(f"保存到JSON失败: {e}")
|
| 372 |
+
raise
|
| 373 |
+
|
| 374 |
+
def save_to_markdown(self, articles: List[Dict[str, Any]], filepath: str) -> None:
|
| 375 |
+
"""
|
| 376 |
+
保存研究报告到Markdown文件
|
| 377 |
+
|
| 378 |
+
Args:
|
| 379 |
+
articles: 研究报告列表
|
| 380 |
+
filepath: 输出文件路径
|
| 381 |
+
"""
|
| 382 |
+
if not articles:
|
| 383 |
+
self.logger.warning("没有研究报告可保存")
|
| 384 |
+
return
|
| 385 |
+
|
| 386 |
+
try:
|
| 387 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 388 |
+
f.write("# 研究报告汇总\n\n")
|
| 389 |
+
f.write(f"**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
| 390 |
+
f.write(f"**报告数量**: {len(articles)} 篇\n\n")
|
| 391 |
+
|
| 392 |
+
for i, article in enumerate(articles, 1):
|
| 393 |
+
f.write(f"## {i}. {article.get('title', '无标题')}\n\n")
|
| 394 |
+
|
| 395 |
+
# 基本信息
|
| 396 |
+
f.write(f"**发布时间**: {article.get('publish_date', '未知')}\n\n")
|
| 397 |
+
f.write(f"**原文链接**: [{article.get('url', '')}]({article.get('url', '')})\n\n")
|
| 398 |
+
|
| 399 |
+
# 摘要
|
| 400 |
+
summary = article.get("summary", "")
|
| 401 |
+
if summary:
|
| 402 |
+
f.write(f"**摘要**: {summary}\n\n")
|
| 403 |
+
|
| 404 |
+
# 提取的信息
|
| 405 |
+
if "extracted_info" in article:
|
| 406 |
+
extracted = article["extracted_info"]
|
| 407 |
+
|
| 408 |
+
info_lines = []
|
| 409 |
+
if extracted.get("rating"):
|
| 410 |
+
info_lines.append(f"- **投资评级**: {extracted['rating']}")
|
| 411 |
+
if extracted.get("target_price"):
|
| 412 |
+
info_lines.append(f"- **目标价**: {extracted['target_price']}元")
|
| 413 |
+
if extracted.get("industry"):
|
| 414 |
+
info_lines.append(f"- **所属行业**: {extracted['industry']}")
|
| 415 |
+
if extracted.get("has_analysis"):
|
| 416 |
+
info_lines.append(f"- **包含分析**: 是")
|
| 417 |
+
|
| 418 |
+
if info_lines:
|
| 419 |
+
f.write("**提取信息**:\n")
|
| 420 |
+
for line in info_lines:
|
| 421 |
+
f.write(f"{line}\n")
|
| 422 |
+
f.write("\n")
|
| 423 |
+
|
| 424 |
+
f.write("---\n\n")
|
| 425 |
+
|
| 426 |
+
self.logger.info(f"保存到Markdown: {len(articles)} 篇研究报告 -> {filepath}")
|
| 427 |
+
|
| 428 |
+
except Exception as e:
|
| 429 |
+
self.logger.error(f"保存到Markdown失败: {e}")
|
| 430 |
+
raise
|
| 431 |
+
|
| 432 |
+
def analyze_articles(self, articles: List[Dict[str, Any]]) -> Dict[str, Any]:
|
| 433 |
+
"""
|
| 434 |
+
分析研究报告
|
| 435 |
+
|
| 436 |
+
Args:
|
| 437 |
+
articles: 研究报告列表
|
| 438 |
+
|
| 439 |
+
Returns:
|
| 440 |
+
分析结果
|
| 441 |
+
"""
|
| 442 |
+
if not articles:
|
| 443 |
+
return {"error": "没有研究报告可分析"}
|
| 444 |
+
|
| 445 |
+
try:
|
| 446 |
+
analysis = {
|
| 447 |
+
"total_count": len(articles),
|
| 448 |
+
"date_range": None,
|
| 449 |
+
"industry_distribution": {},
|
| 450 |
+
"rating_distribution": {},
|
| 451 |
+
"avg_summary_length": 0,
|
| 452 |
+
"analysis_coverage": 0
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
# 分析日期范围
|
| 456 |
+
dates = []
|
| 457 |
+
for article in articles:
|
| 458 |
+
publish_date_str = article.get("publish_date", "")
|
| 459 |
+
if publish_date_str:
|
| 460 |
+
try:
|
| 461 |
+
if " " in publish_date_str:
|
| 462 |
+
date = datetime.strptime(publish_date_str, "%Y-%m-%d %H:%M:%S")
|
| 463 |
+
else:
|
| 464 |
+
date = datetime.strptime(publish_date_str, "%Y-%m-%d")
|
| 465 |
+
dates.append(date)
|
| 466 |
+
except ValueError:
|
| 467 |
+
pass
|
| 468 |
+
|
| 469 |
+
if dates:
|
| 470 |
+
min_date = min(dates)
|
| 471 |
+
max_date = max(dates)
|
| 472 |
+
analysis["date_range"] = {
|
| 473 |
+
"start": min_date.strftime("%Y-%m-%d"),
|
| 474 |
+
"end": max_date.strftime("%Y-%m-%d"),
|
| 475 |
+
"days": (max_date - min_date).days
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
# 分析行业分布
|
| 479 |
+
for article in articles:
|
| 480 |
+
if "extracted_info" in article:
|
| 481 |
+
industry = article["extracted_info"].get("industry")
|
| 482 |
+
if industry:
|
| 483 |
+
analysis["industry_distribution"][industry] = \
|
| 484 |
+
analysis["industry_distribution"].get(industry, 0) + 1
|
| 485 |
+
|
| 486 |
+
# 分析评级分布
|
| 487 |
+
for article in articles:
|
| 488 |
+
if "extracted_info" in article:
|
| 489 |
+
rating = article["extracted_info"].get("rating")
|
| 490 |
+
if rating:
|
| 491 |
+
analysis["rating_distribution"][rating] = \
|
| 492 |
+
analysis["rating_distribution"].get(rating, 0) + 1
|
| 493 |
+
|
| 494 |
+
# 分析摘要长度
|
| 495 |
+
summary_lengths = []
|
| 496 |
+
for article in articles:
|
| 497 |
+
summary = article.get("summary", "")
|
| 498 |
+
if summary:
|
| 499 |
+
summary_lengths.append(len(summary))
|
| 500 |
+
|
| 501 |
+
if summary_lengths:
|
| 502 |
+
analysis["avg_summary_length"] = sum(summary_lengths) / len(summary_lengths)
|
| 503 |
+
|
| 504 |
+
# 分析分析覆盖率
|
| 505 |
+
articles_with_analysis = 0
|
| 506 |
+
for article in articles:
|
| 507 |
+
if "extracted_info" in article:
|
| 508 |
+
if article["extracted_info"].get("has_analysis"):
|
| 509 |
+
articles_with_analysis += 1
|
| 510 |
+
|
| 511 |
+
if articles:
|
| 512 |
+
analysis["analysis_coverage"] = articles_with_analysis / len(articles)
|
| 513 |
+
|
| 514 |
+
self.logger.info(f"分析研究报告: {len(articles)} 篇")
|
| 515 |
+
|
| 516 |
+
return analysis
|
| 517 |
+
|
| 518 |
+
except Exception as e:
|
| 519 |
+
self.logger.error(f"分析研究报告失败: {e}")
|
| 520 |
+
return {"error": f"分析失败: {str(e)}"}
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
if __name__ == "__main__":
|
| 524 |
+
# 测试数据处理模块
|
| 525 |
+
import sys
|
| 526 |
+
|
| 527 |
+
# 测试数据
|
| 528 |
+
test_articles = [
|
| 529 |
+
{
|
| 530 |
+
"title": "人工智能行业研究报告:买入评级,目标价120元",
|
| 531 |
+
"summary": "本报告分析了人工智能行业的发展趋势,包括技术突破、应用场景、市场规模等方面的内容。",
|
| 532 |
+
"url": "https://example.com/reports/ai-2024",
|
| 533 |
+
"publish_date": "2024-01-15 09:30:00"
|
| 534 |
+
},
|
| 535 |
+
{
|
| 536 |
+
"title": "芯片行业分析报告:增持评级",
|
| 537 |
+
"summary": "报告详细介绍了芯片行业的最新发展动态和投资机会。",
|
| 538 |
+
"url": "https://example.com/reports/chip-2024",
|
| 539 |
+
"publish_date": "2024-01-14 14:20:00"
|
| 540 |
+
},
|
| 541 |
+
{
|
| 542 |
+
"title": "新能源汽车行业报告",
|
| 543 |
+
"summary": "分析新能源汽车行业的发展前景和投资建议。",
|
| 544 |
+
"url": "https://example.com/reports/ev-2024",
|
| 545 |
+
"publish_date": "2024-01-13 11:15:00"
|
| 546 |
+
}
|
| 547 |
+
]
|
| 548 |
+
|
| 549 |
+
try:
|
| 550 |
+
processor = DataProcessor()
|
| 551 |
+
|
| 552 |
+
# 测试过滤
|
| 553 |
+
print("测试按日期过滤...")
|
| 554 |
+
filtered = processor.filter_by_date(test_articles, date_from="2024-01-14")
|
| 555 |
+
print(f"过滤后: {len(filtered)} 篇")
|
| 556 |
+
|
| 557 |
+
# 测试排序
|
| 558 |
+
print("\n测试排序...")
|
| 559 |
+
sorted_articles = processor.sort_articles(test_articles, sort_by="date", sort_order="desc")
|
| 560 |
+
print(f"排序后第一篇标题: {sorted_articles[0].get('title')}")
|
| 561 |
+
|
| 562 |
+
# 测试提取关键信息
|
| 563 |
+
print("\n测试提取关键信息...")
|
| 564 |
+
processed = processor.extract_key_info(test_articles)
|
| 565 |
+
for article in processed:
|
| 566 |
+
print(f"标题: {article.get('title')}")
|
| 567 |
+
print(f"评级: {article.get('extracted_info', {}).get('rating')}")
|
| 568 |
+
print(f"目标价: {article.get('extracted_info', {}).get('target_price')}")
|
| 569 |
+
print()
|
| 570 |
+
|
| 571 |
+
# 测试分析
|
| 572 |
+
print("\n测试分析...")
|
| 573 |
+
analysis = processor.analyze_articles(processed)
|
| 574 |
+
print(f"总数量: {analysis.get('total_count')}")
|
| 575 |
+
print(f"行业分布: {analysis.get('industry_distribution')}")
|
| 576 |
+
print(f"评级分布: {analysis.get('rating_distribution')}")
|
| 577 |
+
|
| 578 |
+
# 测试保存
|
| 579 |
+
print("\n测试保存...")
|
| 580 |
+
import tempfile
|
| 581 |
+
import os
|
| 582 |
+
|
| 583 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 584 |
+
csv_path = os.path.join(tmpdir, "test.csv")
|
| 585 |
+
json_path = os.path.join(tmpdir, "test.json")
|
| 586 |
+
md_path = os.path.join(tmpdir, "test.md")
|
| 587 |
+
|
| 588 |
+
processor.save_to_csv(processed, csv_path)
|
| 589 |
+
processor.save_to_json(processed, json_path)
|
| 590 |
+
processor.save_to_markdown(processed, md_path)
|
| 591 |
+
|
| 592 |
+
print(f"CSV文件大小: {os.path.getsize(csv_path)} 字节")
|
| 593 |
+
print(f"JSON文件大小: {os.path.getsize(json_path)} 字节")
|
| 594 |
+
print(f"Markdown文件大小: {os.path.getsize(md_path)} 字节")
|
| 595 |
+
|
| 596 |
+
print("\n测试完成!")
|
| 597 |
+
|
| 598 |
+
except Exception as e:
|
| 599 |
+
print(f"测试失败: {e}")
|
| 600 |
+
sys.exit(1)
|
Skills/report-search/scripts/example_usage.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
研报搜索技能使用示例
|
| 5 |
+
符合iwencai-skill-creator规范
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
import json
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
# 添加当前目录到Python路径
|
| 14 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 15 |
+
|
| 16 |
+
from api_client import APIClient, APIError
|
| 17 |
+
from data_processor import DataProcessor
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def example_basic_search():
|
| 21 |
+
"""示例1: 基本搜索(响应透明传递示例)"""
|
| 22 |
+
print("=" * 80)
|
| 23 |
+
print("示例1: 基本搜索(响应透明传递示例)")
|
| 24 |
+
print("=" * 80)
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
# 初始化API客户端
|
| 28 |
+
client = APIClient()
|
| 29 |
+
|
| 30 |
+
# 搜索研究报告(注意:现在返回完整的API响应)
|
| 31 |
+
query = "人工智能行业研究报告"
|
| 32 |
+
response_data = client.search_reports(query)
|
| 33 |
+
|
| 34 |
+
print(f"搜索关键词: {query}")
|
| 35 |
+
print(f"完整API响应结构: {list(response_data.keys())}")
|
| 36 |
+
|
| 37 |
+
# 从响应中提取数据(调用方决定如何处理)
|
| 38 |
+
articles = response_data.get("data", [])
|
| 39 |
+
print(f"找到 {len(articles)} 篇研究报告:")
|
| 40 |
+
|
| 41 |
+
for i, article in enumerate(articles[:5], 1): # 只显示前5篇
|
| 42 |
+
print(f"\n{i}. {article.get('title', '无标题')}")
|
| 43 |
+
print(f" 摘要: {article.get('summary', '无摘要')[:100]}...")
|
| 44 |
+
print(f" 发布时间: {article.get('publish_date', '未知')}")
|
| 45 |
+
print(f" 原文链接: {article.get('url', '')}")
|
| 46 |
+
|
| 47 |
+
# 显示响应元数据(透明传递的一部分)
|
| 48 |
+
if "metadata" in response_data:
|
| 49 |
+
print(f"\n响应元数据: {response_data.get('metadata', {})}")
|
| 50 |
+
|
| 51 |
+
print("\n" + "=" * 80)
|
| 52 |
+
|
| 53 |
+
except APIError as e:
|
| 54 |
+
print(f"API错误: {e}")
|
| 55 |
+
except Exception as e:
|
| 56 |
+
print(f"未知错误: {e}")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def example_with_data_processing():
|
| 60 |
+
"""示例2: 带数据处理的搜索(CLI工具使用示例)"""
|
| 61 |
+
print("\n" + "=" * 80)
|
| 62 |
+
print("示例2: 带数据处理的搜索(CLI工具使用示例)")
|
| 63 |
+
print("=" * 80)
|
| 64 |
+
print("注意:此示例展示CLI工具的后处理功能,AI代理调用时应使用透明传递方式")
|
| 65 |
+
print("=" * 80)
|
| 66 |
+
|
| 67 |
+
try:
|
| 68 |
+
# 初始化API客户端和数据处理
|
| 69 |
+
client = APIClient()
|
| 70 |
+
processor = DataProcessor()
|
| 71 |
+
|
| 72 |
+
# 搜索研究报告(获取完整API响应)
|
| 73 |
+
query = "芯片行业分析报告"
|
| 74 |
+
response_data = client.search_reports(query)
|
| 75 |
+
|
| 76 |
+
# 从响应中提取数据(CLI工具的后处理)
|
| 77 |
+
articles = response_data.get("data", [])
|
| 78 |
+
|
| 79 |
+
print(f"搜索关键词: {query}")
|
| 80 |
+
print(f"完整API响应接收完成,提取到 {len(articles)} 篇研究报告数据")
|
| 81 |
+
|
| 82 |
+
# 数据处理
|
| 83 |
+
# 1. 按日期排序(最新在前)
|
| 84 |
+
articles = processor.sort_articles(articles, sort_by="date", sort_order="desc")
|
| 85 |
+
|
| 86 |
+
# 2. 提取关键信息
|
| 87 |
+
articles = processor.extract_key_info(articles)
|
| 88 |
+
|
| 89 |
+
# 3. 分析结果
|
| 90 |
+
analysis = processor.analyze_articles(articles)
|
| 91 |
+
|
| 92 |
+
print(f"\n分析结果:")
|
| 93 |
+
print(f" 总数量: {analysis.get('total_count')}")
|
| 94 |
+
print(f" 日期范围: {analysis.get('date_range', {}).get('start')} 到 {analysis.get('date_range', {}).get('end')}")
|
| 95 |
+
print(f" 行业分布: {analysis.get('industry_distribution')}")
|
| 96 |
+
print(f" 评级分布: {analysis.get('rating_distribution')}")
|
| 97 |
+
print(f" 平均摘要长度: {analysis.get('avg_summary_length'):.1f} 字符")
|
| 98 |
+
print(f" 分析覆盖率: {analysis.get('analysis_coverage'):.1%}")
|
| 99 |
+
|
| 100 |
+
# 显示前3篇
|
| 101 |
+
print(f"\n前3篇研究报告:")
|
| 102 |
+
for i, article in enumerate(articles[:3], 1):
|
| 103 |
+
print(f"\n{i}. {article.get('title', '无标题')}")
|
| 104 |
+
print(f" 发布时间: {article.get('publish_date', '未知')}")
|
| 105 |
+
|
| 106 |
+
extracted = article.get('extracted_info', {})
|
| 107 |
+
if extracted.get('rating'):
|
| 108 |
+
print(f" 投资评级: {extracted['rating']}")
|
| 109 |
+
if extracted.get('target_price'):
|
| 110 |
+
print(f" 目标价: {extracted['target_price']}元")
|
| 111 |
+
if extracted.get('industry'):
|
| 112 |
+
print(f" 所属行业: {extracted['industry']}")
|
| 113 |
+
|
| 114 |
+
print("\n" + "=" * 80)
|
| 115 |
+
|
| 116 |
+
except APIError as e:
|
| 117 |
+
print(f"API错误: {e}")
|
| 118 |
+
except Exception as e:
|
| 119 |
+
print(f"未知错误: {e}")
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def example_save_to_files():
|
| 123 |
+
"""示例3: 保存到文件(CLI工具使用示例)"""
|
| 124 |
+
print("\n" + "=" * 80)
|
| 125 |
+
print("示例3: 保存到文件(CLI工具使用示例)")
|
| 126 |
+
print("=" * 80)
|
| 127 |
+
print("注意:此示例展示CLI工具的导出功能,AI代理调用时应使用透明传递方式")
|
| 128 |
+
print("=" * 80)
|
| 129 |
+
|
| 130 |
+
try:
|
| 131 |
+
# 初始化API客户端和数据处理
|
| 132 |
+
client = APIClient()
|
| 133 |
+
processor = DataProcessor()
|
| 134 |
+
|
| 135 |
+
# 搜索研究报告(获取完整API响应)
|
| 136 |
+
query = "新能源汽车投资分析"
|
| 137 |
+
response_data = client.search_reports(query)
|
| 138 |
+
|
| 139 |
+
# 从响应中提取数据(CLI工具的后处理)
|
| 140 |
+
articles = response_data.get("data", [])
|
| 141 |
+
|
| 142 |
+
print(f"搜索关键词: {query}")
|
| 143 |
+
print(f"完整API响应接收完成,提取到 {len(articles)} 篇研究报告数据")
|
| 144 |
+
|
| 145 |
+
# 数据处理
|
| 146 |
+
articles = processor.sort_articles(articles, sort_by="relevance", sort_order="desc")
|
| 147 |
+
articles = processor.extract_key_info(articles)
|
| 148 |
+
|
| 149 |
+
# 创建输出目录
|
| 150 |
+
output_dir = Path("example_output")
|
| 151 |
+
output_dir.mkdir(exist_ok=True)
|
| 152 |
+
|
| 153 |
+
# 保存到不同格式
|
| 154 |
+
# 1. 保存到CSV
|
| 155 |
+
csv_path = output_dir / "research_reports.csv"
|
| 156 |
+
processor.save_to_csv(articles, str(csv_path))
|
| 157 |
+
print(f" 保存到CSV: {csv_path}")
|
| 158 |
+
|
| 159 |
+
# 2. 保存到JSON
|
| 160 |
+
json_path = output_dir / "research_reports.json"
|
| 161 |
+
processor.save_to_json(articles, str(json_path))
|
| 162 |
+
print(f" 保存到JSON: {json_path}")
|
| 163 |
+
|
| 164 |
+
# 3. 保存到Markdown
|
| 165 |
+
md_path = output_dir / "research_reports.md"
|
| 166 |
+
processor.save_to_markdown(articles, str(md_path))
|
| 167 |
+
print(f" 保存到Markdown: {md_path}")
|
| 168 |
+
|
| 169 |
+
# 显示文件大小
|
| 170 |
+
print(f"\n生成的文件:")
|
| 171 |
+
for filepath in [csv_path, json_path, md_path]:
|
| 172 |
+
if filepath.exists():
|
| 173 |
+
size_kb = filepath.stat().st_size / 1024
|
| 174 |
+
print(f" {filepath.name}: {size_kb:.1f} KB")
|
| 175 |
+
|
| 176 |
+
print("\n" + "=" * 80)
|
| 177 |
+
|
| 178 |
+
except APIError as e:
|
| 179 |
+
print(f"API错误: {e}")
|
| 180 |
+
except Exception as e:
|
| 181 |
+
print(f"未知错误: {e}")
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def example_batch_processing():
|
| 185 |
+
"""示例4: 批量处理(响应透明传递示例)"""
|
| 186 |
+
print("\n" + "=" * 80)
|
| 187 |
+
print("示例4: 批量处理(响应透明传递示例)")
|
| 188 |
+
print("=" * 80)
|
| 189 |
+
print("注意:batch_search现在返回完整的API响应字典")
|
| 190 |
+
print("=" * 80)
|
| 191 |
+
|
| 192 |
+
try:
|
| 193 |
+
# 初始化API客户端
|
| 194 |
+
client = APIClient()
|
| 195 |
+
|
| 196 |
+
# 批量查询
|
| 197 |
+
queries = [
|
| 198 |
+
"人工智能行业趋势",
|
| 199 |
+
"芯片技术发展",
|
| 200 |
+
"新能源政策分析",
|
| 201 |
+
"医药创新研究"
|
| 202 |
+
]
|
| 203 |
+
|
| 204 |
+
print(f"批量处理 {len(queries)} 个查询:")
|
| 205 |
+
|
| 206 |
+
# 批量搜索(现在返回完整的API响应字典)
|
| 207 |
+
batch_results = client.batch_search(queries, limit_per_query=3)
|
| 208 |
+
|
| 209 |
+
total_articles = 0
|
| 210 |
+
for query, response_data in batch_results.items():
|
| 211 |
+
# 从每个响应中提取数据
|
| 212 |
+
articles = response_data.get("data", [])
|
| 213 |
+
count = len(articles)
|
| 214 |
+
total_articles += count
|
| 215 |
+
print(f" {query}: {count} 篇研究报告")
|
| 216 |
+
|
| 217 |
+
# 显示每篇的标题
|
| 218 |
+
for i, article in enumerate(articles[:2], 1): # 只显示前2篇
|
| 219 |
+
title = article.get('title', '无标题')
|
| 220 |
+
if len(title) > 50:
|
| 221 |
+
title = title[:47] + "..."
|
| 222 |
+
print(f" {i}. {title}")
|
| 223 |
+
|
| 224 |
+
if count > 2:
|
| 225 |
+
print(f" ... 还有 {count - 2} 篇")
|
| 226 |
+
|
| 227 |
+
print(f"\n总计: {total_articles} 篇研究报告")
|
| 228 |
+
|
| 229 |
+
print("\n" + "=" * 80)
|
| 230 |
+
|
| 231 |
+
except APIError as e:
|
| 232 |
+
print(f"API错误: {e}")
|
| 233 |
+
except Exception as e:
|
| 234 |
+
print(f"未知错误: {e}")
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def example_cli_usage():
|
| 238 |
+
"""示例5: CLI使用方式"""
|
| 239 |
+
print("\n" + "=" * 80)
|
| 240 |
+
print("示例5: CLI使用方式")
|
| 241 |
+
print("=" * 80)
|
| 242 |
+
|
| 243 |
+
print("CLI命令行使用示例:")
|
| 244 |
+
print()
|
| 245 |
+
print("1. 基本搜索:")
|
| 246 |
+
print(" python research_report_search.py -q \"人工智能行业研究报告\"")
|
| 247 |
+
print()
|
| 248 |
+
print("2. 限制结果数量:")
|
| 249 |
+
print(" python research_report_search.py -q \"芯片行业\" -l 5")
|
| 250 |
+
print()
|
| 251 |
+
print("3. 导出为CSV格式:")
|
| 252 |
+
print(" python research_report_search.py -q \"新能源汽车\" -o results.csv -f csv")
|
| 253 |
+
print()
|
| 254 |
+
print("4. 批量处理:")
|
| 255 |
+
print(" python research_report_search.py -i queries.txt -o ./results -f json")
|
| 256 |
+
print()
|
| 257 |
+
print("5. 时间范围搜索:")
|
| 258 |
+
print(" python research_report_search.py -q \"医药行业\" --date-from \"2024-01-01\" --date-to \"2024-03-31\"")
|
| 259 |
+
print()
|
| 260 |
+
print("6. 获取帮助:")
|
| 261 |
+
print(" python research_report_search.py -h")
|
| 262 |
+
print()
|
| 263 |
+
print("7. 测试API连接:")
|
| 264 |
+
print(" python research_report_search.py --test")
|
| 265 |
+
|
| 266 |
+
print("\n" + "=" * 80)
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def example_curl_usage():
|
| 270 |
+
"""示例6: curl使用方式"""
|
| 271 |
+
print("\n" + "=" * 80)
|
| 272 |
+
print("示例6: curl使用方式")
|
| 273 |
+
print("=" * 80)
|
| 274 |
+
|
| 275 |
+
print("curl命令行使用示例:")
|
| 276 |
+
print()
|
| 277 |
+
print("1. 基本搜索 (Unix/Linux/macOS):")
|
| 278 |
+
print(" curl -X POST \"https://openapi.iwencai.com/v1/comprehensive/search\" \\")
|
| 279 |
+
print(" -H \"Content-Type: application/json\" \\")
|
| 280 |
+
print(" -H \"Authorization: Bearer $IWENCAI_API_KEY\" \\")
|
| 281 |
+
print(" -H \"X-Claw-Call-Type: normal\" \\")
|
| 282 |
+
print(" -H \"X-Claw-Skill-Id: report-search\" \\")
|
| 283 |
+
print(" -H \"X-Claw-Skill-Version: 2.0.0\" \\")
|
| 284 |
+
print(" -H \"X-Claw-Plugin-Id: none\" \\")
|
| 285 |
+
print(" -H \"X-Claw-Plugin-Version: none\" \\")
|
| 286 |
+
print(" -H \"X-Claw-Trace-Id: $(python -c 'import secrets; print(secrets.token_hex(32))')\" \\")
|
| 287 |
+
print(" -d '{")
|
| 288 |
+
print(" \"channels\": [\"report\"],")
|
| 289 |
+
print(" \"app_id\": \"AIME_SKILL\",")
|
| 290 |
+
print(" \"query\": \"人工智能行业研究报告\"")
|
| 291 |
+
print(" }'")
|
| 292 |
+
print()
|
| 293 |
+
print("2. Windows PowerShell:")
|
| 294 |
+
print(" $traceId = python -c \"import secrets; print(secrets.token_hex(32))\"")
|
| 295 |
+
print(" curl.exe -X POST \"https://openapi.iwencai.com/v1/comprehensive/search\" \\")
|
| 296 |
+
print(" -H \"Content-Type: application/json\" \\")
|
| 297 |
+
print(" -H \"Authorization: Bearer $env:IWENCAI_API_KEY\" \\")
|
| 298 |
+
print(" -H \"X-Claw-Call-Type: normal\" \\")
|
| 299 |
+
print(" -H \"X-Claw-Skill-Id: report-search\" \\")
|
| 300 |
+
print(" -H \"X-Claw-Skill-Version: 2.0.0\" \\")
|
| 301 |
+
print(" -H \"X-Claw-Plugin-Id: none\" \\")
|
| 302 |
+
print(" -H \"X-Claw-Plugin-Version: none\" \\")
|
| 303 |
+
print(" -H \"X-Claw-Trace-Id: $traceId\" \\")
|
| 304 |
+
print(" -d '{")
|
| 305 |
+
print(" \"channels\": [\"report\"],")
|
| 306 |
+
print(" \"app_id\": \"AIME_SKILL\",")
|
| 307 |
+
print(" \"query\": \"人工智能行业研究报告\"")
|
| 308 |
+
print(" }'")
|
| 309 |
+
print()
|
| 310 |
+
print("注意: 每次请求都会生成新的64字符Trace ID")
|
| 311 |
+
|
| 312 |
+
print("\n" + "=" * 80)
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def main():
|
| 316 |
+
"""主函数"""
|
| 317 |
+
print("研报搜索技能使用示例")
|
| 318 |
+
print("版本: 2.0.0")
|
| 319 |
+
print("规范: 符合iwencai-skill-creator要求")
|
| 320 |
+
print("=" * 80)
|
| 321 |
+
|
| 322 |
+
# 检查API Key
|
| 323 |
+
if not os.getenv("IWENCAI_API_KEY"):
|
| 324 |
+
print("警告: 请先设置环境变量 IWENCAI_API_KEY")
|
| 325 |
+
print("示例: export IWENCAI_API_KEY=\"your_api_key_here\"")
|
| 326 |
+
print()
|
| 327 |
+
print("以下示例将模拟运行,不会实际调用API")
|
| 328 |
+
print("=" * 80)
|
| 329 |
+
|
| 330 |
+
# 运行示例
|
| 331 |
+
example_basic_search()
|
| 332 |
+
example_with_data_processing()
|
| 333 |
+
example_save_to_files()
|
| 334 |
+
example_batch_processing()
|
| 335 |
+
example_cli_usage()
|
| 336 |
+
example_curl_usage()
|
| 337 |
+
|
| 338 |
+
print("\n示例运行完成!")
|
| 339 |
+
print("请参考以上示例使用研报搜索技能。")
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
if __name__ == "__main__":
|
| 343 |
+
main()
|
Skills/report-search/scripts/requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
requests>=2.28.0
|
| 2 |
+
pandas>=1.5.0
|
| 3 |
+
numpy>=1.24.0
|
Skills/report-search/scripts/research_report_search.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
研报搜索技能主程序
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
# 添加当前目录到Python路径
|
| 11 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 12 |
+
|
| 13 |
+
from cli import main
|
| 14 |
+
|
| 15 |
+
if __name__ == "__main__":
|
| 16 |
+
sys.exit(main())
|
Skills/report-search/scripts/setup.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
研报搜索技能安装配置
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
from setuptools import setup, find_packages
|
| 10 |
+
|
| 11 |
+
# 读取版本信息
|
| 12 |
+
with open(os.path.join(os.path.dirname(__file__), 'requirements.txt'), 'r', encoding='utf-8') as f:
|
| 13 |
+
requirements = [line.strip() for line in f if line.strip() and not line.startswith('#')]
|
| 14 |
+
|
| 15 |
+
# 读取README
|
| 16 |
+
with open(os.path.join(os.path.dirname(__file__), '..', 'README.md'), 'r', encoding='utf-8') as f:
|
| 17 |
+
long_description = f.read()
|
| 18 |
+
|
| 19 |
+
setup(
|
| 20 |
+
name="research-report-search",
|
| 21 |
+
version="1.0.0",
|
| 22 |
+
description="研报搜索技能 - 搜索和分析财经研究报告",
|
| 23 |
+
long_description=long_description,
|
| 24 |
+
long_description_content_type="text/markdown",
|
| 25 |
+
author="研报搜索技能开发团队",
|
| 26 |
+
author_email="",
|
| 27 |
+
url="",
|
| 28 |
+
packages=find_packages(),
|
| 29 |
+
py_modules=[
|
| 30 |
+
'config',
|
| 31 |
+
'api_client',
|
| 32 |
+
'data_processor',
|
| 33 |
+
'cli',
|
| 34 |
+
'research_report_search'
|
| 35 |
+
],
|
| 36 |
+
install_requires=requirements,
|
| 37 |
+
entry_points={
|
| 38 |
+
'console_scripts': [
|
| 39 |
+
'research-report-search=research_report_search:main',
|
| 40 |
+
'rrs=research_report_search:main',
|
| 41 |
+
],
|
| 42 |
+
},
|
| 43 |
+
classifiers=[
|
| 44 |
+
'Development Status :: 4 - Beta',
|
| 45 |
+
'Intended Audience :: Financial and Insurance Industry',
|
| 46 |
+
'Topic :: Office/Business :: Financial :: Investment',
|
| 47 |
+
'License :: OSI Approved :: MIT License',
|
| 48 |
+
'Programming Language :: Python :: 3',
|
| 49 |
+
'Programming Language :: Python :: 3.7',
|
| 50 |
+
'Programming Language :: Python :: 3.8',
|
| 51 |
+
'Programming Language :: Python :: 3.9',
|
| 52 |
+
'Programming Language :: Python :: 3.10',
|
| 53 |
+
'Programming Language :: Python :: 3.11',
|
| 54 |
+
],
|
| 55 |
+
keywords='research report, financial analysis, investment, stock, finance',
|
| 56 |
+
python_requires='>=3.7',
|
| 57 |
+
project_urls={
|
| 58 |
+
'Documentation': 'https://github.com/example/research-report-search',
|
| 59 |
+
'Source': 'https://github.com/example/research-report-search',
|
| 60 |
+
'Tracker': 'https://github.com/example/research-report-search/issues',
|
| 61 |
+
},
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def install_config():
|
| 66 |
+
"""安装配置文件"""
|
| 67 |
+
import shutil
|
| 68 |
+
from pathlib import Path
|
| 69 |
+
|
| 70 |
+
# 源配置文件路径
|
| 71 |
+
source_config = Path(__file__).parent / 'config.example.json'
|
| 72 |
+
|
| 73 |
+
# 目标配置文件路径(用户目录)
|
| 74 |
+
home_dir = Path.home()
|
| 75 |
+
config_dir = home_dir / '.research_report_search'
|
| 76 |
+
config_dir.mkdir(exist_ok=True)
|
| 77 |
+
|
| 78 |
+
target_config = config_dir / 'config.json'
|
| 79 |
+
|
| 80 |
+
# 如果配置文件不存在,复制示例配置
|
| 81 |
+
if not target_config.exists() and source_config.exists():
|
| 82 |
+
shutil.copy2(source_config, target_config)
|
| 83 |
+
print(f"已创建配置文件: {target_config}")
|
| 84 |
+
print("请编辑该文件以配置您的API设置。")
|
| 85 |
+
else:
|
| 86 |
+
print(f"配置文件已存在: {target_config}")
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def check_dependencies():
|
| 90 |
+
"""检查依赖"""
|
| 91 |
+
import subprocess
|
| 92 |
+
import importlib
|
| 93 |
+
|
| 94 |
+
print("检查依赖...")
|
| 95 |
+
|
| 96 |
+
# 检查Python版本
|
| 97 |
+
if sys.version_info < (3, 7):
|
| 98 |
+
print("错误: 需要Python 3.7或更高版本")
|
| 99 |
+
sys.exit(1)
|
| 100 |
+
|
| 101 |
+
# 检查必要依赖
|
| 102 |
+
required_packages = ['requests', 'pandas', 'numpy']
|
| 103 |
+
|
| 104 |
+
for package in required_packages:
|
| 105 |
+
try:
|
| 106 |
+
importlib.import_module(package)
|
| 107 |
+
print(f" ✓ {package}")
|
| 108 |
+
except ImportError:
|
| 109 |
+
print(f" ✗ {package} 未安装")
|
| 110 |
+
print(f" 请运行: pip install {package}")
|
| 111 |
+
|
| 112 |
+
print("\n依赖检查完成。")
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
if __name__ == "__main__":
|
| 116 |
+
# 安装时执行额外操作
|
| 117 |
+
if len(sys.argv) > 1 and sys.argv[1] == 'install':
|
| 118 |
+
print("研报搜索技能安装程序")
|
| 119 |
+
print("=" * 80)
|
| 120 |
+
|
| 121 |
+
# 检查依赖
|
| 122 |
+
check_dependencies()
|
| 123 |
+
|
| 124 |
+
# 安装配置文件
|
| 125 |
+
install_config()
|
| 126 |
+
|
| 127 |
+
print("\n安装完成!")
|
| 128 |
+
print("\n使用说明:")
|
| 129 |
+
print("1. 设置API密钥环境变量:")
|
| 130 |
+
print(" export IWENCAI_API_KEY=\"your_api_key_here\"")
|
| 131 |
+
print("\n2. 基本使用:")
|
| 132 |
+
print(" research-report-search -q \"人工智能行业研究报告\"")
|
| 133 |
+
print("\n3. 获取帮助:")
|
| 134 |
+
print(" research-report-search -h")
|
| 135 |
+
|
| 136 |
+
else:
|
| 137 |
+
# 正常setup.py执行
|
| 138 |
+
pass
|
Skills/report-search/scripts/test_basic.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
研报搜索技能基础测试
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import unittest
|
| 10 |
+
import tempfile
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from unittest.mock import patch, MagicMock
|
| 13 |
+
|
| 14 |
+
# 添加当前目录到Python路径
|
| 15 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 16 |
+
|
| 17 |
+
from config import Config, get_config
|
| 18 |
+
from data_processor import DataProcessor
|
| 19 |
+
from api_client import APIClient, APIError
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class TestConfig(unittest.TestCase):
|
| 23 |
+
"""配置模块测试"""
|
| 24 |
+
|
| 25 |
+
def setUp(self):
|
| 26 |
+
"""测试前准备"""
|
| 27 |
+
self.config = Config()
|
| 28 |
+
|
| 29 |
+
def test_default_config(self):
|
| 30 |
+
"""测试默认配置"""
|
| 31 |
+
self.assertEqual(self.config.get("api.base_url"), "https://openapi.iwencai.com")
|
| 32 |
+
self.assertEqual(self.config.get("api.endpoint"), "/v1/comprehensive/search")
|
| 33 |
+
self.assertEqual(self.config.get("search.channels"), ["report"])
|
| 34 |
+
self.assertEqual(self.config.get("search.app_id"), "AIME_SKILL")
|
| 35 |
+
|
| 36 |
+
def test_get_api_url(self):
|
| 37 |
+
"""测试获取API URL"""
|
| 38 |
+
api_url = self.config.get_api_url()
|
| 39 |
+
self.assertEqual(api_url, "https://openapi.iwencai.com/v1/comprehensive/search")
|
| 40 |
+
|
| 41 |
+
def test_validation(self):
|
| 42 |
+
"""测试配置验证"""
|
| 43 |
+
# 测试空配置
|
| 44 |
+
config = Config()
|
| 45 |
+
with self.assertRaises(ValueError):
|
| 46 |
+
config.config["api"]["base_url"] = ""
|
| 47 |
+
config.validate()
|
| 48 |
+
|
| 49 |
+
def test_environment_variables(self):
|
| 50 |
+
"""测试环境变量"""
|
| 51 |
+
with patch.dict(os.environ, {"LOG_LEVEL": "DEBUG"}):
|
| 52 |
+
config = Config()
|
| 53 |
+
self.assertEqual(config.get("logging.level"), "DEBUG")
|
| 54 |
+
|
| 55 |
+
def test_config_file_loading(self):
|
| 56 |
+
"""测试配置文件加载"""
|
| 57 |
+
# 创建临时配置文件
|
| 58 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
| 59 |
+
json.dump({
|
| 60 |
+
"api": {
|
| 61 |
+
"timeout": 60
|
| 62 |
+
}
|
| 63 |
+
}, f)
|
| 64 |
+
config_file = f.name
|
| 65 |
+
|
| 66 |
+
try:
|
| 67 |
+
config = Config(config_file)
|
| 68 |
+
self.assertEqual(config.get("api.timeout"), 60)
|
| 69 |
+
finally:
|
| 70 |
+
os.unlink(config_file)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class TestDataProcessor(unittest.TestCase):
|
| 74 |
+
"""数据处理模块测试"""
|
| 75 |
+
|
| 76 |
+
def setUp(self):
|
| 77 |
+
"""测试前准备"""
|
| 78 |
+
self.processor = DataProcessor()
|
| 79 |
+
self.test_articles = [
|
| 80 |
+
{
|
| 81 |
+
"title": "人工智能行业研究报告:买入评级,目标价120元",
|
| 82 |
+
"summary": "本报告分析了人工智能行业的发展趋势,包括技术突破、应用场景、市场规模等方面的内容。",
|
| 83 |
+
"url": "https://example.com/reports/ai-2024",
|
| 84 |
+
"publish_date": "2024-01-15 09:30:00"
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"title": "芯片行业分析报告:增持评级",
|
| 88 |
+
"summary": "报告详细介绍了芯片行业的最新发展动态和投资机会。",
|
| 89 |
+
"url": "https://example.com/reports/chip-2024",
|
| 90 |
+
"publish_date": "2024-01-14 14:20:00"
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"title": "新能源汽车行业报告",
|
| 94 |
+
"summary": "分析新能源汽车行业的发展前景和投资建议。",
|
| 95 |
+
"url": "https://example.com/reports/ev-2024",
|
| 96 |
+
"publish_date": "2024-01-13 11:15:00"
|
| 97 |
+
}
|
| 98 |
+
]
|
| 99 |
+
|
| 100 |
+
def test_filter_by_date(self):
|
| 101 |
+
"""测试按日期过滤"""
|
| 102 |
+
# 过滤2024-01-14之后的文章
|
| 103 |
+
filtered = self.processor.filter_by_date(
|
| 104 |
+
self.test_articles,
|
| 105 |
+
date_from="2024-01-14"
|
| 106 |
+
)
|
| 107 |
+
self.assertEqual(len(filtered), 2)
|
| 108 |
+
|
| 109 |
+
# 过滤2024-01-14到2024-01-15之间的文章
|
| 110 |
+
filtered = self.processor.filter_by_date(
|
| 111 |
+
self.test_articles,
|
| 112 |
+
date_from="2024-01-14",
|
| 113 |
+
date_to="2024-01-15"
|
| 114 |
+
)
|
| 115 |
+
self.assertEqual(len(filtered), 2)
|
| 116 |
+
|
| 117 |
+
def test_filter_by_days(self):
|
| 118 |
+
"""测试按最近N天过滤"""
|
| 119 |
+
# 注意:这个测试依赖于当前时间
|
| 120 |
+
# 这里我们主要测试函数是否能正常运行
|
| 121 |
+
filtered = self.processor.filter_by_days(self.test_articles, days=30)
|
| 122 |
+
self.assertIsInstance(filtered, list)
|
| 123 |
+
|
| 124 |
+
def test_sort_articles(self):
|
| 125 |
+
"""测试排序"""
|
| 126 |
+
# 按日期降序排序
|
| 127 |
+
sorted_articles = self.processor.sort_articles(
|
| 128 |
+
self.test_articles,
|
| 129 |
+
sort_by="date",
|
| 130 |
+
sort_order="desc"
|
| 131 |
+
)
|
| 132 |
+
self.assertEqual(sorted_articles[0]["title"], "人工智能行业研究报告:买入评级,目标价120元")
|
| 133 |
+
|
| 134 |
+
# 按相关性排序
|
| 135 |
+
sorted_articles = self.processor.sort_articles(
|
| 136 |
+
self.test_articles,
|
| 137 |
+
sort_by="relevance",
|
| 138 |
+
sort_order="desc"
|
| 139 |
+
)
|
| 140 |
+
self.assertIsInstance(sorted_articles, list)
|
| 141 |
+
|
| 142 |
+
def test_extract_key_info(self):
|
| 143 |
+
"""测试提取关键信息"""
|
| 144 |
+
processed = self.processor.extract_key_info(self.test_articles)
|
| 145 |
+
|
| 146 |
+
self.assertEqual(len(processed), 3)
|
| 147 |
+
|
| 148 |
+
# 检查提取的信息
|
| 149 |
+
for article in processed:
|
| 150 |
+
self.assertIn("extracted_info", article)
|
| 151 |
+
extracted = article["extracted_info"]
|
| 152 |
+
|
| 153 |
+
self.assertIn("rating", extracted)
|
| 154 |
+
self.assertIn("target_price", extracted)
|
| 155 |
+
self.assertIn("industry", extracted)
|
| 156 |
+
self.assertIn("has_analysis", extracted)
|
| 157 |
+
|
| 158 |
+
def test_save_to_csv(self):
|
| 159 |
+
"""测试保存到CSV"""
|
| 160 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 161 |
+
csv_path = Path(tmpdir) / "test.csv"
|
| 162 |
+
self.processor.save_to_csv(self.test_articles, str(csv_path))
|
| 163 |
+
|
| 164 |
+
self.assertTrue(csv_path.exists())
|
| 165 |
+
self.assertGreater(csv_path.stat().st_size, 0)
|
| 166 |
+
|
| 167 |
+
def test_save_to_json(self):
|
| 168 |
+
"""测试保存到JSON"""
|
| 169 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 170 |
+
json_path = Path(tmpdir) / "test.json"
|
| 171 |
+
self.processor.save_to_json(self.test_articles, str(json_path))
|
| 172 |
+
|
| 173 |
+
self.assertTrue(json_path.exists())
|
| 174 |
+
|
| 175 |
+
# 验证JSON格式
|
| 176 |
+
with open(json_path, 'r', encoding='utf-8') as f:
|
| 177 |
+
data = json.load(f)
|
| 178 |
+
self.assertIsInstance(data, list)
|
| 179 |
+
self.assertEqual(len(data), 3)
|
| 180 |
+
|
| 181 |
+
def test_save_to_markdown(self):
|
| 182 |
+
"""测试保存到Markdown"""
|
| 183 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 184 |
+
md_path = Path(tmpdir) / "test.md"
|
| 185 |
+
self.processor.save_to_markdown(self.test_articles, str(md_path))
|
| 186 |
+
|
| 187 |
+
self.assertTrue(md_path.exists())
|
| 188 |
+
|
| 189 |
+
# 验证Markdown内容
|
| 190 |
+
with open(md_path, 'r', encoding='utf-8') as f:
|
| 191 |
+
content = f.read()
|
| 192 |
+
self.assertIn("# 研究报告汇总", content)
|
| 193 |
+
|
| 194 |
+
def test_analyze_articles(self):
|
| 195 |
+
"""测试分析研究报告"""
|
| 196 |
+
processed = self.processor.extract_key_info(self.test_articles)
|
| 197 |
+
analysis = self.processor.analyze_articles(processed)
|
| 198 |
+
|
| 199 |
+
self.assertIn("total_count", analysis)
|
| 200 |
+
self.assertEqual(analysis["total_count"], 3)
|
| 201 |
+
|
| 202 |
+
self.assertIn("industry_distribution", analysis)
|
| 203 |
+
self.assertIn("rating_distribution", analysis)
|
| 204 |
+
self.assertIn("avg_summary_length", analysis)
|
| 205 |
+
self.assertIn("analysis_coverage", analysis)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
class TestAPIClient(unittest.TestCase):
|
| 209 |
+
"""API客户端模块测试"""
|
| 210 |
+
|
| 211 |
+
def setUp(self):
|
| 212 |
+
"""测试前准备"""
|
| 213 |
+
# 模拟环境变量
|
| 214 |
+
self.env_patch = patch.dict(os.environ, {"IWENCAI_API_KEY": "test_key"})
|
| 215 |
+
self.env_patch.start()
|
| 216 |
+
|
| 217 |
+
self.client = APIClient()
|
| 218 |
+
|
| 219 |
+
def tearDown(self):
|
| 220 |
+
"""测试后清理"""
|
| 221 |
+
self.env_patch.stop()
|
| 222 |
+
|
| 223 |
+
def test_initialization(self):
|
| 224 |
+
"""测试初始化"""
|
| 225 |
+
self.assertEqual(self.client.api_key, "test_key")
|
| 226 |
+
self.assertEqual(self.client.api_url, "https://openapi.iwencai.com/v1/comprehensive/search")
|
| 227 |
+
|
| 228 |
+
@patch('requests.post')
|
| 229 |
+
def test_search_reports_success(self, mock_post):
|
| 230 |
+
"""测试搜索研究报告成功"""
|
| 231 |
+
# 模拟API响应
|
| 232 |
+
mock_response = MagicMock()
|
| 233 |
+
mock_response.status_code = 200
|
| 234 |
+
mock_response.json.return_value = {
|
| 235 |
+
"data": [
|
| 236 |
+
{
|
| 237 |
+
"title": "测试报告",
|
| 238 |
+
"summary": "测试摘要",
|
| 239 |
+
"url": "https://example.com/test",
|
| 240 |
+
"publish_date": "2024-01-01 00:00:00"
|
| 241 |
+
}
|
| 242 |
+
]
|
| 243 |
+
}
|
| 244 |
+
mock_post.return_value = mock_response
|
| 245 |
+
|
| 246 |
+
# 调用搜索
|
| 247 |
+
articles = self.client.search_reports("测试", limit=5)
|
| 248 |
+
|
| 249 |
+
# 验证结果
|
| 250 |
+
self.assertEqual(len(articles), 1)
|
| 251 |
+
self.assertEqual(articles[0]["title"], "测试报告")
|
| 252 |
+
|
| 253 |
+
# 验证请求参数
|
| 254 |
+
mock_post.assert_called_once()
|
| 255 |
+
call_args = mock_post.call_args
|
| 256 |
+
self.assertEqual(call_args[0][0], "https://openapi.iwencai.com/v1/comprehensive/search")
|
| 257 |
+
|
| 258 |
+
# 验证请求头
|
| 259 |
+
headers = call_args[1]['headers']
|
| 260 |
+
self.assertEqual(headers['Content-Type'], 'application/json')
|
| 261 |
+
self.assertEqual(headers['Authorization'], 'Bearer test_key')
|
| 262 |
+
|
| 263 |
+
# 验证请求体
|
| 264 |
+
json_data = call_args[1]['json']
|
| 265 |
+
self.assertEqual(json_data['channels'], ['report'])
|
| 266 |
+
self.assertEqual(json_data['app_id'], 'AIME_SKILL')
|
| 267 |
+
self.assertEqual(json_data['query'], '测试')
|
| 268 |
+
|
| 269 |
+
@patch('requests.post')
|
| 270 |
+
def test_search_reports_api_error(self, mock_post):
|
| 271 |
+
"""测试API错误"""
|
| 272 |
+
# 模拟API错误响应
|
| 273 |
+
mock_response = MagicMock()
|
| 274 |
+
mock_response.status_code = 401
|
| 275 |
+
mock_response.json.return_value = {
|
| 276 |
+
"message": "认证失败"
|
| 277 |
+
}
|
| 278 |
+
mock_post.return_value = mock_response
|
| 279 |
+
|
| 280 |
+
# 验证抛出APIError
|
| 281 |
+
with self.assertRaises(APIError) as context:
|
| 282 |
+
self.client.search_reports("测试")
|
| 283 |
+
|
| 284 |
+
self.assertIn("认证失败", str(context.exception))
|
| 285 |
+
|
| 286 |
+
@patch('requests.post')
|
| 287 |
+
def test_search_reports_network_error(self, mock_post):
|
| 288 |
+
"""测试网络错误"""
|
| 289 |
+
# 模拟网络错误
|
| 290 |
+
mock_post.side_effect = ConnectionError("网络连接失败")
|
| 291 |
+
|
| 292 |
+
# 验证抛出APIError
|
| 293 |
+
with self.assertRaises(APIError) as context:
|
| 294 |
+
self.client.search_reports("测试")
|
| 295 |
+
|
| 296 |
+
self.assertIn("网络连接失败", str(context.exception))
|
| 297 |
+
|
| 298 |
+
def test_batch_search(self):
|
| 299 |
+
"""测试批量搜索"""
|
| 300 |
+
# 模拟search_reports方法
|
| 301 |
+
with patch.object(self.client, 'search_reports') as mock_search:
|
| 302 |
+
mock_search.return_value = [
|
| 303 |
+
{"title": "测试报告1"},
|
| 304 |
+
{"title": "测试报告2"}
|
| 305 |
+
]
|
| 306 |
+
|
| 307 |
+
# 调用批量搜索
|
| 308 |
+
queries = ["查询1", "查询2"]
|
| 309 |
+
results = self.client.batch_search(queries, limit_per_query=2)
|
| 310 |
+
|
| 311 |
+
# 验证结果
|
| 312 |
+
self.assertEqual(len(results), 2)
|
| 313 |
+
self.assertIn("查询1", results)
|
| 314 |
+
self.assertIn("查询2", results)
|
| 315 |
+
self.assertEqual(len(results["查询1"]), 2)
|
| 316 |
+
self.assertEqual(len(results["查询2"]), 2)
|
| 317 |
+
|
| 318 |
+
# 验证调用次数
|
| 319 |
+
self.assertEqual(mock_search.call_count, 2)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
class TestIntegration(unittest.TestCase):
|
| 323 |
+
"""集成测试"""
|
| 324 |
+
|
| 325 |
+
def setUp(self):
|
| 326 |
+
"""测试前准备"""
|
| 327 |
+
# 模拟环境变量
|
| 328 |
+
self.env_patch = patch.dict(os.environ, {"IWENCAI_API_KEY": "test_key"})
|
| 329 |
+
self.env_patch.start()
|
| 330 |
+
|
| 331 |
+
def tearDown(self):
|
| 332 |
+
"""测试后清理"""
|
| 333 |
+
self.env_patch.stop()
|
| 334 |
+
|
| 335 |
+
@patch('requests.post')
|
| 336 |
+
def test_end_to_end_workflow(self, mock_post):
|
| 337 |
+
"""测试端到端工作流程"""
|
| 338 |
+
# 模拟API响应
|
| 339 |
+
mock_response = MagicMock()
|
| 340 |
+
mock_response.status_code = 200
|
| 341 |
+
mock_response.json.return_value = {
|
| 342 |
+
"data": [
|
| 343 |
+
{
|
| 344 |
+
"title": "人工智能报告",
|
| 345 |
+
"summary": "人工智能行业分析",
|
| 346 |
+
"url": "https://example.com/ai",
|
| 347 |
+
"publish_date": "2024-01-15 09:30:00"
|
| 348 |
+
},
|
| 349 |
+
{
|
| 350 |
+
"title": "芯片报告",
|
| 351 |
+
"summary": "芯片行业分析",
|
| 352 |
+
"url": "https://example.com/chip",
|
| 353 |
+
"publish_date": "2024-01-14 14:20:00"
|
| 354 |
+
}
|
| 355 |
+
]
|
| 356 |
+
}
|
| 357 |
+
mock_post.return_value = mock_response
|
| 358 |
+
|
| 359 |
+
# 初始化组件
|
| 360 |
+
client = APIClient()
|
| 361 |
+
processor = DataProcessor()
|
| 362 |
+
|
| 363 |
+
# 搜索研究报告
|
| 364 |
+
articles = client.search_reports("人工智能", limit=5)
|
| 365 |
+
|
| 366 |
+
# 数据处理
|
| 367 |
+
processed = processor.extract_key_info(articles)
|
| 368 |
+
sorted_articles = processor.sort_articles(processed, sort_by="date", sort_order="desc")
|
| 369 |
+
|
| 370 |
+
# 验证结果
|
| 371 |
+
self.assertEqual(len(sorted_articles), 2)
|
| 372 |
+
self.assertEqual(sorted_articles[0]["title"], "人工智能报告")
|
| 373 |
+
|
| 374 |
+
# 验证提取的信息
|
| 375 |
+
self.assertIn("extracted_info", sorted_articles[0])
|
| 376 |
+
extracted = sorted_articles[0]["extracted_info"]
|
| 377 |
+
self.assertIn("rating", extracted)
|
| 378 |
+
self.assertIn("target_price", extracted)
|
| 379 |
+
self.assertIn("industry", extracted)
|
| 380 |
+
self.assertIn("has_analysis", extracted)
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
def run_tests():
|
| 384 |
+
"""运行测试"""
|
| 385 |
+
# 创建测试套件
|
| 386 |
+
suite = unittest.TestSuite()
|
| 387 |
+
|
| 388 |
+
# 添加测试类
|
| 389 |
+
suite.addTest(unittest.makeSuite(TestConfig))
|
| 390 |
+
suite.addTest(unittest.makeSuite(TestDataProcessor))
|
| 391 |
+
suite.addTest(unittest.makeSuite(TestAPIClient))
|
| 392 |
+
suite.addTest(unittest.makeSuite(TestIntegration))
|
| 393 |
+
|
| 394 |
+
# 运行测试
|
| 395 |
+
runner = unittest.TextTestRunner(verbosity=2)
|
| 396 |
+
result = runner.run(suite)
|
| 397 |
+
|
| 398 |
+
return result
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
if __name__ == "__main__":
|
| 402 |
+
print("研报搜索技能基础测试")
|
| 403 |
+
print("=" * 80)
|
| 404 |
+
|
| 405 |
+
# 运行测试
|
| 406 |
+
result = run_tests()
|
| 407 |
+
|
| 408 |
+
# 输出测试结果
|
| 409 |
+
print("\n" + "=" * 80)
|
| 410 |
+
print(f"测试结果: {result.testsRun} 个测试用例")
|
| 411 |
+
print(f"通过: {result.testsRun - len(result.failures) - len(result.errors)}")
|
| 412 |
+
print(f"失败: {len(result.failures)}")
|
| 413 |
+
print(f"错误: {len(result.errors)}")
|
| 414 |
+
|
| 415 |
+
if result.failures or result.errors:
|
| 416 |
+
sys.exit(1)
|
| 417 |
+
else:
|
| 418 |
+
print("\n所有测试通过!")
|
Skills/report-search/scripts/test_queries.txt
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 研报搜索测试查询文件
|
| 2 |
+
# 每行一个查询关键词
|
| 3 |
+
|
| 4 |
+
人工智能行业研究报告
|
| 5 |
+
芯片技术发展分析
|
| 6 |
+
新能源汽车投资前景
|
| 7 |
+
医药创新研究进展
|
| 8 |
+
金融科技发展趋势
|
| 9 |
+
5G通信行业分析
|
| 10 |
+
云计算市场报告
|
| 11 |
+
半导体产业研究
|
| 12 |
+
消费行业投资分析
|
| 13 |
+
智能制造发展报告
|
| 14 |
+
|
| 15 |
+
# 行业趋势分析
|
| 16 |
+
人工智能
|
| 17 |
+
芯片
|
| 18 |
+
新能源
|
| 19 |
+
医药
|
| 20 |
+
金融
|
| 21 |
+
科技
|
| 22 |
+
制造
|
| 23 |
+
消费
|
| 24 |
+
|
| 25 |
+
# 公司分析
|
| 26 |
+
腾讯控股分析
|
| 27 |
+
阿里巴巴研究
|
| 28 |
+
华为技术分析
|
| 29 |
+
比亚迪研究报告
|
| 30 |
+
宁德时代分析
|
| 31 |
+
茅台投资价值
|
| 32 |
+
美团点评研究
|
| 33 |
+
京东集团分析
|
| 34 |
+
|
| 35 |
+
# 投资主题
|
| 36 |
+
碳中和投资机会
|
| 37 |
+
数字经济趋势
|
| 38 |
+
专精特新企业
|
| 39 |
+
国产替代机会
|
| 40 |
+
消费升级趋势
|
| 41 |
+
科技创新投资
|
| 42 |
+
绿色能源发展
|
| 43 |
+
数字化转型
|
app/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fin-DataPilot backend package."""
|
| 2 |
+
__version__ = "0.1.0"
|
app/agent/__init__.py
ADDED
|
File without changes
|
app/agent/graph.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LangGraph StateGraph assembly + streaming entry point."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import logging
|
| 5 |
+
from typing import Any, AsyncIterator
|
| 6 |
+
|
| 7 |
+
from langgraph.graph import END, StateGraph
|
| 8 |
+
|
| 9 |
+
from app.agent.nodes.executor import executor_node
|
| 10 |
+
from app.agent.nodes.reflector import reflector_node
|
| 11 |
+
from app.agent.nodes.skill_router import skill_router_node
|
| 12 |
+
from app.agent.nodes.synthesizer import synthesize
|
| 13 |
+
from app.agent.state import AgentState, EV_DONE, EV_ERROR
|
| 14 |
+
from app.config import get_settings
|
| 15 |
+
from app.skills.registry import REGISTRY
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _build_graph() -> Any:
|
| 21 |
+
g = StateGraph(AgentState)
|
| 22 |
+
g.add_node("skill_router", skill_router_node)
|
| 23 |
+
g.add_node("executor", executor_node)
|
| 24 |
+
g.add_node("reflector", reflector_node)
|
| 25 |
+
g.add_node("synthesizer", lambda s: s) # placeholder; streaming handled outside
|
| 26 |
+
|
| 27 |
+
g.set_entry_point("skill_router")
|
| 28 |
+
|
| 29 |
+
# After router: if final_answer was set, go to synthesizer; else go to executor
|
| 30 |
+
def _after_router(state: AgentState) -> str:
|
| 31 |
+
if state.get("error"):
|
| 32 |
+
return "synthesizer"
|
| 33 |
+
if state.get("final_answer"):
|
| 34 |
+
return "synthesizer"
|
| 35 |
+
return "executor"
|
| 36 |
+
|
| 37 |
+
g.add_conditional_edges("skill_router", _after_router, {
|
| 38 |
+
"executor": "executor",
|
| 39 |
+
"synthesizer": "synthesizer",
|
| 40 |
+
})
|
| 41 |
+
|
| 42 |
+
# After executor: always go to reflector
|
| 43 |
+
g.add_edge("executor", "reflector")
|
| 44 |
+
|
| 45 |
+
# After reflector: if more rounds needed AND round limit not reached, loop back to router
|
| 46 |
+
def _after_reflector(state: AgentState) -> str:
|
| 47 |
+
verdict = state.get("reflection_verdict", "sufficient")
|
| 48 |
+
rounds = state.get("rounds_used", 0)
|
| 49 |
+
max_rounds = get_settings().agent_max_reflect_rounds
|
| 50 |
+
if verdict == "need_more" and rounds < max_rounds:
|
| 51 |
+
return "skill_router"
|
| 52 |
+
return "synthesizer"
|
| 53 |
+
|
| 54 |
+
g.add_conditional_edges("reflector", _after_reflector, {
|
| 55 |
+
"skill_router": "skill_router",
|
| 56 |
+
"synthesizer": "synthesizer",
|
| 57 |
+
})
|
| 58 |
+
|
| 59 |
+
g.add_edge("synthesizer", END)
|
| 60 |
+
|
| 61 |
+
return g.compile()
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
_GRAPH = None
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def get_graph() -> Any:
|
| 68 |
+
global _GRAPH
|
| 69 |
+
if _GRAPH is None:
|
| 70 |
+
_GRAPH = _build_graph()
|
| 71 |
+
return _GRAPH
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# ---------- Public streaming entry point ----------
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
async def run_agent_stream(
|
| 78 |
+
user_query: str,
|
| 79 |
+
history: list[dict[str, Any]],
|
| 80 |
+
session_id: str,
|
| 81 |
+
) -> AsyncIterator[dict[str, Any]]:
|
| 82 |
+
"""Stream agent events for a single user turn."""
|
| 83 |
+
from app.agent.state import (
|
| 84 |
+
EV_MESSAGE_FINAL,
|
| 85 |
+
EV_REFLECTION,
|
| 86 |
+
EV_THINK,
|
| 87 |
+
EV_TOOL_CALL,
|
| 88 |
+
EV_TOOL_RESULT,
|
| 89 |
+
)
|
| 90 |
+
from app.utils.trace import generate_trace_id
|
| 91 |
+
|
| 92 |
+
if not REGISTRY.list_specs():
|
| 93 |
+
yield {"event": EV_ERROR, "data": {"message": "No skills registered"}}
|
| 94 |
+
yield {"event": EV_DONE, "data": {}}
|
| 95 |
+
return
|
| 96 |
+
|
| 97 |
+
trace_id = generate_trace_id()
|
| 98 |
+
init_state: AgentState = {
|
| 99 |
+
"user_query": user_query,
|
| 100 |
+
"session_id": session_id,
|
| 101 |
+
"history": history,
|
| 102 |
+
"tool_calls": [],
|
| 103 |
+
"rounds_used": 0,
|
| 104 |
+
"reflection_verdict": "need_more",
|
| 105 |
+
"trace_id": trace_id,
|
| 106 |
+
}
|
| 107 |
+
yield {"event": EV_THINK, "data": {"step": "entry", "text": f"开始处理:{user_query}", "trace_id": trace_id}}
|
| 108 |
+
|
| 109 |
+
graph = get_graph()
|
| 110 |
+
final_state: AgentState = dict(init_state)
|
| 111 |
+
|
| 112 |
+
try:
|
| 113 |
+
async for event in graph.astream(init_state):
|
| 114 |
+
# event is dict {node_name: node_output}
|
| 115 |
+
for node_name, node_out in event.items():
|
| 116 |
+
if not isinstance(node_out, dict):
|
| 117 |
+
continue
|
| 118 |
+
final_state.update(node_out)
|
| 119 |
+
# Stream per-node events
|
| 120 |
+
if node_name == "skill_router":
|
| 121 |
+
tc = (node_out.get("tool_calls") or [])
|
| 122 |
+
if tc and tc[-1].get("result") is None:
|
| 123 |
+
last = tc[-1]
|
| 124 |
+
yield {
|
| 125 |
+
"event": EV_TOOL_CALL,
|
| 126 |
+
"data": {
|
| 127 |
+
"name": last["name"],
|
| 128 |
+
"args": last.get("args", {}),
|
| 129 |
+
"trace_id": last.get("trace_id", ""),
|
| 130 |
+
},
|
| 131 |
+
}
|
| 132 |
+
if node_out.get("final_answer"):
|
| 133 |
+
yield {
|
| 134 |
+
"event": EV_THINK,
|
| 135 |
+
"data": {"step": "router_final", "text": "直接生成最终答案"},
|
| 136 |
+
}
|
| 137 |
+
elif node_name == "executor":
|
| 138 |
+
tc = (node_out.get("tool_calls") or [])
|
| 139 |
+
if tc:
|
| 140 |
+
last = tc[-1]
|
| 141 |
+
yield {
|
| 142 |
+
"event": EV_TOOL_RESULT,
|
| 143 |
+
"data": {
|
| 144 |
+
"name": last["name"],
|
| 145 |
+
"ok": last.get("ok", False),
|
| 146 |
+
"duration_ms": last.get("duration_ms", 0),
|
| 147 |
+
"trace_id": last.get("trace_id", ""),
|
| 148 |
+
"result": last.get("result"),
|
| 149 |
+
"error": last.get("error"),
|
| 150 |
+
},
|
| 151 |
+
}
|
| 152 |
+
elif node_name == "reflector":
|
| 153 |
+
yield {
|
| 154 |
+
"event": EV_REFLECTION,
|
| 155 |
+
"data": {
|
| 156 |
+
"verdict": node_out.get("reflection_verdict", "sufficient"),
|
| 157 |
+
"reason": node_out.get("reflection", ""),
|
| 158 |
+
},
|
| 159 |
+
}
|
| 160 |
+
except Exception as exc: # noqa: BLE001
|
| 161 |
+
logger.exception("agent graph execution failed")
|
| 162 |
+
yield {"event": EV_ERROR, "data": {"message": f"Agent 执行失败: {exc}", "trace_id": trace_id}}
|
| 163 |
+
|
| 164 |
+
# If the router produced a final answer directly (no synthesizer streaming)
|
| 165 |
+
if final_state.get("final_answer") and not any(
|
| 166 |
+
True for _ in []
|
| 167 |
+
): # placeholder check
|
| 168 |
+
yield {
|
| 169 |
+
"event": EV_MESSAGE_FINAL,
|
| 170 |
+
"data": {
|
| 171 |
+
"content": final_state["final_answer"],
|
| 172 |
+
"tool_calls": final_state.get("tool_calls", []),
|
| 173 |
+
},
|
| 174 |
+
}
|
| 175 |
+
else:
|
| 176 |
+
# Stream synthesizer output
|
| 177 |
+
async for ev in synthesize(final_state):
|
| 178 |
+
yield ev
|
| 179 |
+
|
| 180 |
+
yield {"event": EV_DONE, "data": {"trace_id": trace_id}}
|