根据用户提供的 公司名称 或 股票代码,自动从巨潮资讯网下载对应上市公司财报 PDF,随后上传至 ima 知识库的指定文件夹。
ima_api.cjs、preflight-check.cjs、cos-upload.cjs 脚本。~/.config/ima/client_id 和 ~/.config/ima/api_key 自动读取;也可通过环境变量 IMA_CLIENT_ID / IMA_API_KEY 传入。当用户消息中包含以下关键词之一时触发本技能:
财报 — 最核心触发词年报 / 年度报告 / 年度财报季报 / 季度报告 / 一季报 / 半年报 / 三季报下载财报 / 下载年报 / 下载季报上市公司报告触发后的预设行为:若用户同时提供了公司名称/股票代码 + 目标 ima 文件夹名称,直接执行完整流程;若信息不完整,分步收集。
| 缺少项 | 行为 |
|---|---|
| -------- | ------ |
| 缺少公司名称/股票代码 | 询问:"请提供要下载财报的公司名称或股票代码" |
| 缺少报告类型 | 默认下载最新「年度报告」;若用户提及季报/半年报则按需筛选 |
| 缺少目标 ima 文件夹 | 上传至 ima 知识库根目录 |
| 搜索结果多个匹配 | 列出候选报告让用户选择(展示标题、日期、大小) |
| 目标文件夹不存在 | 告知用户文件夹不存在,上传至根目录或让用户先创建 |
| ima 凭证未配置 | 引导用户完成凭证配置(见下方凭证检查) |
在发起任何 API 请求之前检查:
test -f ~/.config/ima/client_id && test -f ~/.config/ima/api_key && echo "✅" || echo "⚠️ NO CREDENTIALS"
若未配置,提示用户:
mkdir -p ~/.config/ima
echo "your_client_id" > ~/.config/ima/client_id
echo "your_api_key" > ~/.config/ima/api_key
┌─────────────────────────────────────────────────────┐
│ 阶段一:从巨潮资讯下载财报 PDF │
│ Step 1 → Step 2 → Step 3 → Step 4 │
│ 构造查询 发送请求 解析结果 下载PDF │
│ │
│ ✅ 阶段一成功完成(PDF 已落盘) │
│ │ │
│ ▼ │
│ 阶段二:上传 PDF 至 ima 知识库 │
│ Step 5 → Step 6 → Step 7 → Step 8 → Step 9 │
│ 前置检查 获取知识库 定位文件夹 上传COS 添加知识 │
│ │
│ ✅ 全部完成 → 向用户报告结果 + 清理临时文件 │
└─────────────────────────────────────────────────────┘
请求接口:
POST http://www.cninfo.com.cn/new/hisAnnouncement/query
Content-Type: application/x-www-form-urlencoded
核心参数:
| 参数 | 值 | 说明 |
|---|---|---|
| ------ | ----- | ------ |
pageNum | 1 | 页码 |
pageSize | 30 | 每页条数 |
tabName | fulltext | 全文搜索模式 |
searchkey | {公司名/代码} {报告类型关键词} | 空格分隔多词 |
seDate | 可选,如 2024-01-01~2025-12-31 | 限定公告日期范围 |
报告类型关键词映射:
| 用户表述 | searchkey 追加关键词 |
|---|---|
| ---------- | --------------------- |
| 年报 / 年度报告 | 年度报告 |
| 一季报 / 一季度报告 | 第一季度报告 |
| 半年报 / 半年度报告 | 半年度报告 |
| 三季报 / 三季度报告 | 第三季度报告 |
| 未指定 | 年度报告(默认) |
示例 — 搜索平安银行 2024 年年度报告:
curl -s -X POST "http://www.cninfo.com.cn/new/hisAnnouncement/query" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "pageNum=1" \
--data-urlencode "pageSize=30" \
--data-urlencode "tabName=fulltext" \
--data-urlencode "searchkey=平安银行 年度报告" \
--data-urlencode "seDate=2024-01-01~2025-06-01"
响应为 JSON 格式,路径 announcements[],每条包含:
| 字段 | 说明 | 用法 |
|---|---|---|
| ------ | ------ | ------ |
secCode | 股票代码 | 展示给用户 |
secName | 公司简称 | 展示给用户 |
announcementId | 公告唯一 ID | 构造下载 URL |
announcementTitle | 公告标题 | 展示给用户,帮助判断 |
announcementTime | Unix 毫秒时间戳 | 展示发布日期 |
adjunctUrl | PDF 相对路径 | 拼装下载 URL(核心字段) |
adjunctSize | 文件大小(字节) | 展示给用户 |
adjunctType | 附件类型 | 应为 PDF |
用 python3 / node 解析 JSON,筛选 adjunctType=PDF 且标题匹配报告类型的条目。
若 totalAnnouncement > 1,列出候选报告:
🔍 找到 {N} 份 {公司名} 的财报:
1. 📄 {announcementTitle} — 发布于 {日期},{大小}
2. 📄 {announcementTitle} — 发布于 {日期},{大小}
...
请输入序号选择要下载的报告(输入 "全部" 下载所有)
仅 1 条匹配时,直接进入下载步骤。
下载 URL 格式:
http://static.cninfo.com.cn/{adjunctUrl}
下载方式:
# 保存到临时目录,文件名取公告标题
TMP_DIR=$(mktemp -d)
PDF_NAME="{公告标题}.pdf"
curl -sL -o "$TMP_DIR/$PDF_NAME" "http://static.cninfo.com.cn/{adjunctUrl}"
下载后验证:
# 检查文件存在且大小 > 0
test -s "$TMP_DIR/$PDF_NAME" && echo "✅ 下载成功: $(ls -lh "$TMP_DIR/$PDF_NAME" | awk '{print $5}')" || echo "❌ 下载失败"
若下载失败:终止流程,向用户报告错误。
> ⚠️ 前置条件:阶段一必须成功完成(PDF 已存在于本地临时目录)。
环境变量设置(每次调用前):
export IMA_CLIENT_ID=$(cat ~/.config/ima/client_id)
export IMA_API_KEY=$(cat ~/.config/ima/api_key)
export IMA_SKILL_VERSION="1.1.7"
export SKILL_DIR="/workspace/skills/ima-skill"
export IMA_BASE_DIR="$SKILL_DIR"
PREFLIGHT=$(node "$SKILL_DIR/knowledge-base/scripts/preflight-check.cjs" --file "$PDF_FILE")
echo "$PREFLIGHT"
# 若 pass=false → 终止,将 reason 展示给用户
提取关键字段:
FILE_NAME=$(echo "$PREFLIGHT" | node -e "process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).file_name)")
FILE_SIZE=$(echo "$PREFLIGHT" | node -e "process.stdout.write(String(JSON.parse(require('fs').readFileSync(0,'utf8')).file_size))")
MEDIA_TYPE=$(echo "$PREFLIGHT" | node -e "process.stdout.write(String(JSON.parse(require('fs').readFileSync(0,'utf8')).media_type))")
CONTENT_TYPE=$(echo "$PREFLIGHT" | node -e "process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).content_type)")
FILE_EXT=$(echo "$PREFLIGHT" | node -e "process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).file_ext)")
情况 A — 用户指定了知识库名称:
# 搜索知识库
ima_api "openapi/wiki/v1/search_knowledge_base" '{"query":"{知识库名称}","cursor":"","limit":20}'
# 从 info_list 中匹配目标知识库,提取 id
情况 B — 用户未指定知识库:
# 列出可添加的知识库
ima_api "openapi/wiki/v1/get_addable_knowledge_base_list" '{"cursor":"","limit":20}'
# 展示列表让用户选择;仅有 1 个时自动选择
当用户指定了文件夹名称时:
# 在知识库中搜索文件夹
ima_api "openapi/wiki/v1/search_knowledge" "{
\"query\": \"{文件夹名称}\",
\"knowledge_base_id\": \"{kb_id}\",
\"cursor\": \"\"
}"
info_list 中筛选 media_id 以 folder_ 开头的条目media_id 作为 FOLDER_IDfolder_id)当用户未指定文件夹时:
跳过此步,上传至知识库根目录(后续步骤中不传 folder_id)。
8a. 重名检查:
ima_api "openapi/wiki/v1/check_repeated_names" "{
\"params\": [{\"name\": \"$FILE_NAME\", \"media_type\": $MEDIA_TYPE}],
\"knowledge_base_id\": \"{kb_id}\"
${FOLDER_ID:+, \"folder_id\": \"$FOLDER_ID\"}
}"
若 is_repeated=true → 在文件名后追加时间戳:{name}_20250101120000.{ext}
8b. 创建媒体:
CREATE_RESP=$(ima_api "openapi/wiki/v1/create_media" "{
\"file_name\": \"$FILE_NAME\",
\"file_size\": $FILE_SIZE,
\"content_type\": \"$CONTENT_TYPE\",
\"knowledge_base_id\": \"{kb_id}\",
\"file_ext\": \"$FILE_EXT\"
}")
# code≠0 → 终止,将 msg 展示给用户
MEDIA_ID=$(echo "$CREATE_RESP" | node -e "process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).data.media_id)")
提取 COS 凭证字段:secret_id、secret_key、token、bucket_name、region、cos_key、start_time、expired_time。
8c. COS 上传:
node "$SKILL_DIR/knowledge-base/scripts/cos-upload.cjs" \
--file "$PDF_FILE" \
--secret-id "{secret_id}" \
--secret-key "{secret_key}" \
--token "{token}" \
--bucket "{bucket_name}" \
--region "{region}" \
--cos-key "{cos_key}" \
--content-type "$CONTENT_TYPE" \
--start-time "{start_time}" \
--expired-time "{expired_time}" \
--timeout 300000
# ⛔ 非零退出 → 立即终止,不要执行 Step 9
仅当 Step 8c 成功(退出码 0)后才执行:
ima_api "openapi/wiki/v1/add_knowledge" "{
\"media_type\": $MEDIA_TYPE,
\"media_id\": \"$MEDIA_ID\",
\"title\": \"$FILE_NAME\",
\"knowledge_base_id\": \"{kb_id}\"
${FOLDER_ID:+, \"folder_id\": \"$FOLDER_ID\"}
\"file_info\": {
\"cos_key\": \"{cos_key}\",
\"file_size\": $FILE_SIZE,
\"file_name\": \"$FILE_NAME\"
}
}"
全部步骤成功后:
# 清理临时下载文件
rm -rf "$TMP_DIR"
向用户报告:
✅ 财报归集完成!
📥 下载:{公告标题}
📊 公司:{公司名}({股票代码})
📅 发布日期:{日期}
📦 大小:{文件大小}
📂 已存入:ima 知识库「{知识库名称}」{文件夹路径}
| 阶段 | 步骤 | 常见错误 | 处理方式 |
|---|---|---|---|
| ------ | ------ | ---------- | ---------- |
| 阶段一 | Step 2 | 查询返回空结果 | 提示公司名/代码可能不正确,或该时间段无对应报告 |
| 阶段一 | Step 4 | PDF 下载失败(404/网络错误) | 重试一次,仍失败则报告用户 |
| 阶段二 | Step 5 | 文件类型不被支持 | 将 reason 展示给用户后终止 |
| 阶段二 | Step 6 | 找不到可用的知识库 | 引导用户先在 ima 客户端创建知识库 |
| 阶段二 | Step 7 | 文件夹不存在 | 告知用户后改为上传至根目录 |
| 阶段二 | Step 8b | create_media 返回 code≠0 | 将 msg 直接展示给用户 |
| 阶段二 | Step 8c | COS 上传失败 | 终止流程,展示错误信息 |
| 阶段二 | Step 9 | add_knowledge 返回 code≠0 | 将 msg 直接展示给用户 |
| 全局 | — | 凭证未配置 | 引导用户完成凭证设置 |
为简化重复调用,阶段二中所有对 ima API 的调用统一通过以下方式:
ima_api() {
local api_path="$1"
local body="$2"
cd "$SKILL_DIR"
node ima_api.cjs "$api_path" "$body" \
"{\"clientId\":\"$IMA_CLIENT_ID\",\"apiKey\":\"$IMA_API_KEY\"}" \
2>/tmp/ima_api_err
}
错误检查:每次 ima_api 调用后检查退出码,非零退出时解析 /tmp/ima_api_err 中的 code 和 msg。
示例 1 — 基本用法:
> 用户:"下载平安银行的2024年年报到 ima 的「财务分析」文件夹"
流程:搜索"平安银行 年度报告" → 下载 PDF → 上传至「财务分析」文件夹 ✅
示例 2 — 股票代码:
> 用户:"000001 的一季报下载到财报库"
流程:搜索"000001 第一季度报告" → 下载 PDF → 上传至「财报库」✅
示例 3 — 批量下载:
> 用户:"把茅台、腾讯、阿里的最新年报都下载到研究库里"
流程:逐家公司依次执行完整流程(串行,一家完成再下一家)。
共 1 个版本