
Capture Alphapai Unified
- 25 installs
- 61 repo stars
- Updated March 16, 2026
- kirkluokun/awesome-a-stock-openclawskills
Helps with ai & agent building tasks.
About
capture-alphapai-unified is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- capture-alphapai-unified
- AI & Agent Building
- AI-coding skill
Capture Alphapai Unified by the numbers
- 25 all-time installs (skills.sh)
- Ranked #9,764 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/kirkluokun/awesome-a-stock-openclawskills --skill capture-alphapai-unifiedAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 25 |
|---|---|
| repo stars | ★ 61 |
| Last updated | March 16, 2026 |
| Repository | kirkluokun/awesome-a-stock-openclawskills ↗ |
What it does
Helps with ai & agent building tasks.
Files
AlphaPai Unified — 默认投研技能
融合官方API(问答/RAG/Agent)和Web版(路演/点评/热门话题),适合日常投研使用。
选择指南: 一般性问题 → 用本 skill(默认);大批量抓数据/获取列表 → 用 alphapai-web。功能矩阵
| 功能 | 数据源 | 命令 |
|---|---|---|
| 投研问答 | 官方API | qa |
| 数据召回(RAG) | 官方API | recall |
| 公司一页纸 | 官方API | company |
| 业绩点评 | 官方API | earnings |
| 投资逻辑 | 官方API | logic |
| 可比公司 | 官方API | peers |
| 行业一页纸 | 官方API | industry |
| 主题选股 | 官方API | screen |
| 观点Challenge | 官方API | challenge |
| 公告列表 | 官方API | report |
| 图表搜索 | 官方API | chart |
| 路演纪要 | Web版 | roadshow |
| 点评 | Web版 | comment |
| 热门话题 | Web版 | hot |
| 热词 | Web版 | hot-word |
| 热门报告 | Web版 | hot-report |
| 热门股票 | Web版 | hot-stock |
| 综合搜索 | Web版 | search |
| 用户信息 | Web版 | user-info |
认证配置
优先级: 1. 环境变量 ALPHAPAI_API_KEY(推荐) 2. 同级/上级/上上级目录 .env 3. config.json
API Key(官方API):
export ALPHAPAI_API_KEY=your_key_here
# 或
python scripts/alphapai.py config --set-key YOUR_KEY账号密码(Web版):
export ALPHAPAI_ACCOUNT=手机号
export ALPHAPAI_PASSWORD=密码
# 或
python scripts/alphapai.py config --set-account 手机号 --set-password 密码API Key 可在 Alpha 派电脑端获取。账号密码用于 Web 版独有功能(路演、点评、热门话题)。
---
官方API命令
投研问答
python scripts/alphapai.py qa "茅台2024经营情况"
python scripts/alphapai.py qa "新能源趋势" --think # Think模式(Wide Search)
python scripts/alphapai.py qa "近期新闻" --web-search # 联网搜索数据召回
python scripts/alphapai.py recall "茅台市值" --type comment,report
python scripts/alphapai.py recall "新能源" --no-cutoff # 完整内容数据类型: comment(点评) / report(研报) / roadShow(路演) / ann(公告) / qa(问答) / edb(时序数据)
快捷命令
# 公司一页纸
python scripts/alphapai.py company 300014.SZ:亿纬锂能
# 业绩点评(自动查询最新公告)
python scripts/alphapai.py earnings 603380.SH:易德龙
# 投资逻辑
python scripts/alphapai.py logic 300014.SZ:亿纬锂能
# 可比公司
python scripts/alphapai.py peers 002594.SZ:比亚迪
# 行业一页纸
python scripts/alphapai.py industry 白酒
# 主题选股
python scripts/alphapai.py screen "和白酒相关的公司"
# 观点Challenge
python scripts/alphapai.py challenge "小米汽车未来增长空间很大"公告/图表
# 公告列表
python scripts/alphapai.py report 603380.SH
# 图表搜索
python scripts/alphapai.py chart "新能源车销量趋势"---
Web版命令
路演纪要
# 列表
python scripts/alphapai.py roadshow --list
# 搜索
python scripts/alphapai.py roadshow --search "茅台业绩会"
# 详情(含AI摘要)
python scripts/alphapai.py roadshow --detail <纪要ID>点评
# 推荐列表
python scripts/alphapai.py comment --list
# 搜索
python scripts/alphapai.py comment --search "茅台"
# 详情(含盈利预测)
python scripts/alphapai.py comment --detail <点评ID>热门话题 / 热门报告 / 热门股票
# 热门话题列表
python scripts/alphapai.py hot
# 话题详情
python scripts/alphapai.py hot --detail <话题ID>
# 热词
python scripts/alphapai.py hot-word
# 热门报告
python scripts/alphapai.py hot-report
# 热门股票(公募/私募/险资持仓)
python scripts/alphapai.py hot-stock综合搜索 / 用户信息
# 综合搜索(路演+公众号+股票一次全返回)
python scripts/alphapai.py search "贵州茅台"
# 用户信息
python scripts/alphapai.py user-info---
输出原则
Alpha派返回内容由专业投研模型生成,直接呈现原始输出,不做二次加工:
- 禁止总结或压缩
- 禁止截断
- 保留原始 Markdown 格式
- 引用来源随正文输出
---
文件结构
capture-alphapai-unified/
├── SKILL.md
├── scripts/
│ ├── alphapai.py # 统一入口CLI
│ ├── alphapai_client.py # 官方API客户端
│ ├── alphapai_web_client.py # Web版客户端
│ └── setup.sh
├── references/
│ ├── api_reference.md # 官方API参考
│ ├── web_api_reference.md # Web版API参考
│ └── agent_modes.md # Agent模式详解
└── .env.example---
参考文件
| 文件 | 内容 |
|---|---|
| `references/api_reference.md` | 官方API完整字段定义 |
| `references/web_api_reference.md` | Web版API参考 |
| `references/agent_modes.md` | 11种Agent模式详解 |
---
升级
当用户要求升级时,拉取官方安装指引并按流程执行。
{
"api_key": "",
"base_url": "https://open-api.rabyte.cn",
"account": "",
"password": ""
}AlphaPai Agent 模式详解
股票 Agent 接口:POST /alpha/open-api/v1/paipai/stock/agent
SSE 流式返回,响应结构与问答接口一致。所有模式需传 question(必填),不同模式含义不同。
---
mode 1 — 个股业绩点评
根据公司公告、业绩会路演、研报,对公司最新财务报告进行点评。
必填参数
| 参数 | 类型 | 说明 |
|---|---|---|
--stock | CODE:NAME | 股票代码和名称,如 603380.SH:易德龙 |
--report-type | String | 报告类型:季报 / 年报 |
--report-id | String | 公告ID(先用 report 命令查询) |
--report-title | String | 公告标题 |
--report-period | String | 报告期,如 2025年一季报 |
可选参数
| 参数 | 说明 |
|---|---|
--concern | 用户关注方向 |
--template-text | 自定义业绩点评模板 |
工作流程
# Step 1:查询公告列表
python scripts/alphapai_client.py report --code 603380.SH
# Step 2:调用业绩点评
python scripts/alphapai_client.py agent --mode 1 \
--question "易德龙2025年一季报业绩点评" \
--stock 603380.SH:易德龙 \
--report-type 季报 \
--report-id HANNC002658114224 \
--report-title "易德龙:2025年第一季度报告" \
--report-period "2025年一季报"---
mode 2 — 公司一页纸
基于最新公告、路演、研报、点评,对公司进行初步完整分析。
必填参数
| 参数 | 类型 | 说明 |
|---|---|---|
--stock | CODE:NAME | 股票代码和名称 |
可选参数
| 参数 | 说明 |
|---|---|
--language | 语言(美股可选):中文 / 英文 |
--template-text | 自定义公司一页纸模板 |
python scripts/alphapai_client.py agent --mode 2 \
--question "亿纬锂能(300014.SZ)的公司一页纸" \
--stock 300014.SZ:亿纬锂能---
mode 3 — 个股调研大纲
快速生成调研一家公司时应该问的问题清单。
必填参数
| 参数 | 类型 | 说明 |
|---|---|---|
--stock | CODE:NAME | 股票代码和名称 |
可选参数
| 参数 | 说明 |
|---|---|
--template-text | 调研大纲中希望关注的内容要点 |
python scripts/alphapai_client.py agent --mode 3 \
--question "巨星科技(002444.SZ)的调研问题大纲" \
--stock 002444.SZ:巨星科技---
mode 5 — 主题选股
根据事件、主题、关键词筛选相关股票。
必填参数
| 参数 | 说明 |
|---|---|
--template-text | 选股主题描述,与 --question 保持一致 |
python scripts/alphapai_client.py agent --mode 5 \
--question "和白酒相关的公司" \
--template-text "和白酒相关的公司"---
mode 7 — 投资逻辑
梳理一家公司近期事件,提炼投资逻辑。
必填参数
| 参数 | 类型 | 说明 |
|---|---|---|
--stock | CODE:NAME | 股票代码和名称 |
可选参数
| 参数 | 说明 |
|---|---|
--template-text | 关注的分析要点、维度、指标 |
--only-answer | 仅返回最终答案,不返回中间过程 |
python scripts/alphapai_client.py agent --mode 7 \
--question "亿纬锂能(300014.SZ)的公司投资逻辑" \
--stock 300014.SZ:亿纬锂能---
mode 8 — 可比公司
从业务经营、产业链位置上,检索最具对标性的其他公司。
必填参数
| 参数 | 类型 | 说明 |
|---|---|---|
--stock | CODE:NAME | 股票代码和名称 |
可选参数
| 参数 | 说明 |
|---|---|
--concern | 对比关注点:产品参数、技术路线、估值、盈利等 |
python scripts/alphapai_client.py agent --mode 8 \
--question "比亚迪(002594.SZ)的可比公司" \
--stock 002594.SZ:比亚迪 \
--concern "关注技术路线,盈利和出海能力"---
mode 9 — 观点 Challenge
利用Alpha派信息,对用户观点进行逻辑挑战,启发多角度思考。
必填参数
| 参数 | 说明 |
|---|---|
--template-text | 待 Challenge 的观点,与 --question 中描述一致 |
可选参数
| 参数 | 说明 |
|---|---|
--concern | 关注焦点,限定 Challenge 范围 |
--start / --end | 检索信息时间范围 |
python scripts/alphapai_client.py agent --mode 9 \
--question "Challenge该观点:小米汽车未来的增长" \
--template-text "小米汽车未来的增长" \
--concern "YU7" \
--start 2024-10-25 --end 2025-10-24---
mode 11 — 行业一页纸
基于最新公告、路演、研报、点评,对一个行业进行初步完整分析。
必填参数
| 参数 | 说明 |
|---|---|
--industry | 行业名称,如 白酒、光伏 |
可选参数
| 参数 | 说明 |
|---|---|
--template-text | 自定义行业一页纸模板 |
python scripts/alphapai_client.py agent --mode 11 \
--question "白酒的行业一页纸" \
--industry 白酒---
mode 12 — 个股选基
根据个股及公募基金定期报告,查找持仓个股权重最高的基金。
必填参数
| 参数 | 类型 | 说明 |
|---|---|---|
--stock-list | CODE:NAME [...] | 股票列表,空格分隔 |
--report-date | DATE | 报告期日期,如 2025-09-30 |
--fund-type | String | 基金类型:全部 / 主动 / 指数 / ETF |
可选参数
| 参数 | 说明 |
|---|---|
--if-annual | 是否年报:0=否(默认),1=是 |
python scripts/alphapai_client.py agent --mode 12 \
--question "环旭电子、中际旭创、卓胜微的持仓基金" \
--report-date 2025-09-30 \
--fund-type 全部 \
--stock-list 601231.SH:环旭电子 300308.SZ:中际旭创 300782.SZ:卓胜微---
mode 13 — 主题选基
根据事件、主题、关键词筛选相关基金。
必填参数
| 参数 | 说明 |
|---|---|
--report-date | 报告期日期,如 2025-09-30 |
--fund-type | 基金类型:全部 / 主动 / 指数 / ETF |
可选参数
| 参数 | 说明 |
|---|---|
--if-annual | 是否年报:0=否(默认),1=是 |
python scripts/alphapai_client.py agent --mode 13 \
--question "2025三季报持有房地产的主动基金有哪些?" \
--report-date 2025-09-30 \
--fund-type 主动---
mode 15 — 画图
分析用户 query 并以图片方式展示调研结果。
必填参数
| 参数 | 说明 |
|---|---|
--picture-color | 两个 HEX 值(主色、辅色),不含 #,如 2A66F6 A5A8AF |
--picture-style | 风格:PPT风格 / 科普风格 |
可选参数
| 参数 | 说明 |
|---|---|
--source | 产出形式:0=仅图片(默认),1=图文 |
python scripts/alphapai_client.py agent --mode 15 \
--question "黄金" \
--picture-color 2A66F6 A5A8AF \
--picture-style 科普风格AlphaPai Open API 完整参考文档
本文档包含所有接口的详细字段定义和请求/响应示例。
目录
- AlphaPai Open API 完整参考文档
- 目录
- 1. 投研知识问答
- 请求参数
- 响应参数
- 回答类型码(type)
- 引用数据结构 (references)
- 引用类型枚举
- 请求示例 - 非stream模式
- 请求示例 - stream模式
- 2. 投研知识检索(基于RAG技术)
- 请求参数
- recallType 可选值
- 响应参数
- 请求示例
- 响应示例
- 3. 股票 Agent 接口
- agentMode=1 — 个股业绩点评
- agentMode=2 — 公司一页纸
- agentMode=3 — 个股调研大纲
- agentMode=5 — 主题选股
- agentMode=7 — 投资逻辑
- agentMode=8 — 可比公司
- agentMode=9 — 观点 Challenge
- agentMode=11 — 行业一页纸
- agentMode=12 — 个股选基
- agentMode=13 — 主题选基
- agentMode=15 — 画图
- 4. 获取股票公告列表
- 请求参数
- 响应参数 (data 列表每条)
- 5. 搜图表
- 请求参数
- 响应参数 (data 列表每条)
---
1. 投研知识问答
URL: POST /alpha/open-api/v1/paipai/qa-text
根据用户输入问题,采用RAG(Retrivel-Augment-Generation,检索增强生成)技术从Alpha派(一个覆盖A股、港股、美股的专业的投资研究SaaS产品)平台获取高质量的投研参考资料,然后进行总结推理回答。
请求参数
| 字段 | 必填 | 类型 | 说明 | 示例 |
|---|---|---|---|---|
| question | 是 | String | 问题内容 | "为什么美光比海力士在hbm的市占率低?" |
| mode | 是 | String | 模式 | 字符串枚举,Alpha派问答思考模式,Flash——简单搜索问答,一问一搜一答,Think——Wide Search,一问多搜一答,默认为Flash |
| context | 否 | List[String] | 多轮对话上下文 | ["云南缺电对哪些公司会造成影响?","对行业的影响呢?"] |
| questionId | 否 | String | 唯一标识,并发时区分问题 | "OZqY\_-E55-M1OwxZTL4U81713770934462" |
| isAutoRoute | 否 | Boolean | 自动路由(默认true),false强制文本回答 | true |
| isStream | 否 | Boolean | 流式返回(默认false) | false |
| isWebSearch | 否 | Boolean | 联网搜索(默认false) | false |
| isDeepReasoning | 否 | Boolean | 深度推理(默认false) | false |
| requestSelectStartTime | 否 | String | 筛选开始时间 | "2025-01-01" |
| requestSelectEndTime | 否 | String | 筛选结束时间 | "2025-03-01" |
响应参数
| 字段 | 类型 | 说明 |
|---|---|---|
| answer | String | 回答内容,stream模式逐字返回 |
| answerOrder | Integer | stream回答序号 |
| references | List[Dict] | 引用数据列表(见下方引用结构) |
| type | Integer | 回答类型(见下方类型码) |
| isEnd | Boolean | 本次回答是否完结 |
| routeName | String | 处理逻辑路由名称 |
| questionId | String | 问题唯一标识 |
回答类型码(type)
| 值 | 含义 |
|---|---|
| 101 | 问题分析阶段 |
| 102 | 获取资料阶段 |
| 103 | 获取资料完成 |
| 104 | 正在生成回答 |
| 108 | 思维链 |
| 200 | 成功 |
| 201 | 结果为空 |
| 202 | GPT |
| 203 | 用户当日超过限制 |
| 204 | 系统当前超过限制 |
| 206 | 敏感词限制 |
| 207 | PRE_QA |
| 500 | 失败 |
引用数据结构 (references)
每条引用包含以下字段:
| 字段 | 说明 |
|---|---|
| sentence | 引用句子 |
| rank | 排序号 |
| chunk | 上下文文本 |
| id | 引用资源id |
| type | 引用资源类型 |
| title | 标题 |
| publishDate | 发布日期 |
| isSelfPrivate | 是否私有纪要 |
| url | 引用资源链接 |
| teamName | 团队名称 |
| instShortName | 机构名称 |
| filePath | 引用文件路径 |
引用类型枚举
| type值 | 含义 |
|---|---|
| comment | 机构点评 |
| vps | 基金定期报告 |
| report | 内资研报 |
| foreign_report | 外资研报 |
| wechat_public_article | 公众号 |
| ann | 公司公告 |
| roadShow | 路演纪要 |
| roadShow_ir | 上市公司披露投关纪要 |
| roadShow_us | 美股纪要 |
| table | 公告/研报表格 |
| image | 研报图片 |
| edb | EDB数据库 |
请求示例 - 非stream模式
{
"question": "请梳理下国内各车厂智能驾驶的研发进度",
"questionId": "OZqY_-E55-M1OwxZTL4U81713770934462",
"isStream": false,
"isAutoRoute": false
}请求示例 - stream模式
{
"question": "讯兔科技首席科学家是谁?",
"questionId": "OZqY_-E55-M1OwxZTL4U81713770934462",
"isStream": true,
"isAutoRoute": false
}---
2. 投研知识检索(基于RAG技术)
根据用户输入问题,采用RAG(Retrivel-Augment-Generation,检索增强生成)技术从Alpha派(一个覆盖A股、港股、美股的专业的投资研究SaaS产品)平台检索召回高质量的投研参考资料
URL: POST /alpha/open-api/v1/paipai/recall-data
请求参数
| 字段 | 必填 | 类型 | 说明 | 示例 |
|---|---|---|---|---|
| query | 是 | String | 查询问题 | "贵州茅台2024年市值多少?" |
| recallType | 是 | List[String] | 召回数据类型筛选 | ["comment", "qa", "report"] |
| isCutOff | 否 | Boolean | 是否截断数据(默认true)。true=与送入大模型一致;false=返回排序后截断前的完整内容 | false |
| startTime | 否 | String | 开始日期,格式yyyy-MM-dd | "2025-02-18" |
| endTime | 否 | String | 结束日期,格式yyyy-MM-dd | "2025-03-20" |
recallType 可选值
List[str],文档类型列表,JSON列表字符串,示例:[]表示不限制类型,["report"]表示限定report类型。支持的报告类型及含义如下:
- "comment"——点评,覆盖了国内券商分析师,每日给专业机构投资者发送的点评信息(时效性高,但可能存在部分吹水、夸大、信息质量不扎实的问题)
- "vps"——基金定期报告
- "report"——国内券商研报,覆盖了国内券商分析师撰写的研究报告
- "foreign_report"——海外券商研报 ,覆盖了国外头部券商分析师(比如摩根、花旗等机构分析师)撰写的研究报告
- "wechat_public_article"——微信公众号,覆盖了国内微信生态下的一些对投研有帮助的公众号内容,包括各个行业主题相关的公众号资讯,以及上市公司官方微信公众号
- "ann"——上市公司官方公告,覆盖了上市公司在交易所发布的最权威最官方口径的公告内容
- "roadShow"——路演会议纪要,包括上市公司的业绩交流会、券商路演、专家交流等各种国内主流路演平台的会议纪要
- "roadShow_ir"——上市公司官方披露的公开路演纪要,量少,且优于披露要求稀缺性弱于
roadShow类型 - "roadShow_us"——美股上市公司earnings_scripts纪要,
- "roadShow_od"——私域会议纪要,"table"——公告/研报中的数据表格,
- "image"——研报中的图片,通常包括一些产业链图示、指标分析对比的图表等,
- "qa"——基于调研纪要、上市公司公告,提取的调研与路演中的Q&A
- "edb"——宏观经济、行业、个股主题的时间序列数据库
响应参数
返回 data 为 List,每条数据结构:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 数据ID |
| type | String | 数据类型(comment/qa/report等) |
| contextInfo | String | 上下文信息(含发布时间、标题等) |
| chunks | List[String] | 文本块内容 |
| sentenceIds | List[String] | 句子ID列表 |
| contextText | String | 问题文本(仅qa类型有值) |
| answer | String | 回答文本(仅qa类型有值) |
请求示例
{
"query": "贵州茅台2024年市值多少?",
"isCutOff": false,
"recallType": ["comment", "qa", "report"],
"startTime": "2025-02-18",
"endTime": "2025-03-20"
}响应示例
{
"code": 200000,
"message": "success",
"data": [
{
"id": "HCMT00000000839881",
"type": "comment",
"contextInfo": "(发布时间为:2025-03-18 08:53:40)标题:河南&江苏白酒综合大商交流250317,点评内容:",
"chunks": ["波汾价格从470-480升至500..."],
"sentenceIds": ["HCMT00000000839881_11"],
"contextText": "",
"answer": ""
},
{
"id": "TRANSQA000011039428",
"type": "qa",
"contextInfo": "(发布时间为:2025-02-18)股票:中国神华...",
"chunks": [],
"sentenceIds": [],
"contextText": "2024年年度的分红展望是什么?",
"answer": "2023年按A股口径分红比例为75.2%..."
}
]
}---
3. 股票 Agent 接口
URL: POST /alpha/open-api/v1/paipai/stock/agent
专业 Agentic-Workflow,支持多种投研场景。SSE 流式返回,响应结构与文本问答接口一致(type 状态码、references 结构相同)。
所有模式均需传 question(String,必填),⚠️不同Mode下,question的含义不同,详情参考下文
---
agentMode=1 — 个股业绩点评
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| question | 是 | String | 模板字符串,{stock.name}{stockReportPeriod}业绩点评,比如:易德龙2025年一季报业绩点评 |
| stock | 是 | Dict | {"code": "603380.SH", "name": "易德龙"} |
| reportType | 是 | String | 报告类型,如 季报 年报 |
| stockReportId | 是 | String | 公告ID(先从4. 获取股票公告列表查询获取) |
| stockReportTitle | 是 | String | 公告标题,参考从4. 获取股票公告列表查询获取的结果 |
| stockReportPeriod | 是 | String | 报告期,如 2025年一季报 |
| template | 是 | Integer | 0(固定传alpha派模板)1 时需要填写templateText字段 |
| templateText | 否 | String | 用户自己的业绩点评模板 |
| templateConcern | 否 | String | 用户点评中,希望关注的话题、要点 |
如果用户需要根据自己的点评模板填写,需要修改templateText,如果额外一些关注话题,修改templateConcern
示例参数:
{
"agentMode": 1,
"question": "易德龙2025年一季报业绩点评",
"stock": {
"code": "603380.SH",
"name": "易德龙"
},
"template": 0,
"templateText": "",
"reportType": "季报",
"stockReportId": "HANNC002658114224",
"stockReportTitle": "易德龙(603380.SH):苏州易德龙科技股份有限公司2025年第一季度报告",
"stockReportPeriod": "2025年一季报",
"templateConcern": ""
}---
agentMode=2 — 公司一页纸
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| question | 是 | String | 模板字符串,{stock.name}({stock.code})的公司一页纸,比如:亿纬锂能(300014.SZ)的公司一页纸 |
| stock | 是 | Dict | {"code": "300014.SZ", "name": "亿纬锂能"} |
| template | 是 | Integer | 0(固定传alpha派模板)1 时需要填写templateText字段 |
| templateText | 否 | String | 用户自己的公司一页纸分析模板 |
| language | 否 | String | 语言,美股可选 中文 / 英文 |
示例参数:
{
"agentMode": 2,
"question": "亿纬锂能(300014.SZ)的公司一页纸",
"stock": {
"code": "300014.SZ",
"name": "亿纬锂能"
},
"template": 0,
"templateText": ""
}---
agentMode=3 — 个股调研大纲
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| question | 是 | String | 模板字符串,{stock.name}({stock.code})的调研问题大纲,比如:巨星科技(002444.SZ)的调研问题大纲 |
| stock | 是 | Dict | 股票信息,{"code": "002444.SZ", "name": "巨星科技"} |
| template | 是 | Integer | 0(固定参数) |
| templateText | 否 | String | 用户在调研大纲总结中关注的内容,默认传空字符串 |
示例参数:
{
"agentMode": 3,
"question": "巨星科技(002444.SZ)的调研问题大纲",
"stock": {
"code": "002444.SZ",
"name": "巨星科技"
},
"template": 0,
"templateText": ""
}---
agentMode=5 — 主题选股
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| question | 是 | String | 选股主题描述,与templateText保持一致,比如:和白酒相关的公司 |
| template | 是 | Integer | 1(固定参数) |
| templateText | 是 | String | 选股主题描述,与question保持一致 |
示例参数:
{
"agentMode": 5,
"question": "和白酒相关的公司",
"template": 1,
"templateText": "和白酒相关的公司"
}---
agentMode=7 — 投资逻辑
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| question | 是 | String | 模板字符串,{stock.name}({stock.code})的公司投资逻辑,比如:亿纬锂能(300014.SZ)的公司投资逻辑 |
| stock | 是 | Dict | 股票信息,{"code": "300014.SZ", "name": "亿纬锂能"} |
| template | 是 | Integer | 0(固定参数) |
| templateText | 否 | String | 用户在投资逻辑中关注的分析要点、维度、指标等,默认传空字符串 |
| onlyAnswer | 否 | Boolean | 是否只返回最终答案,不返回中间过程(默认 false) |
示例参数:
{
"agentMode": 7,
"question": "亿纬锂能(300014.SZ)的公司投资逻辑",
"onlyAnswer": false,
"stock": {
"code": "300014.SZ",
"name": "亿纬锂能"
},
"template": 0,
"templateText": ""
}---
agentMode=8 — 可比公司
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| question | 是 | String | 模板字符串,{stock.name}({stock.code})的可比公司,比如:比亚迪(002594.SZ)的可比公司 |
| stock | 是 | Dict | 股票信息,{"code": "002594.SZ", "name": "比亚迪"} |
| template | 是 | Integer | 1(固定参数) |
| templateText | 否 | String | 暂未启用,默认传空字符串 |
| templateConcern | 否 | String | 对比过程中,用户关心的话题,比如产品参数、技术路线,估值、盈利等 |
示例参数:
{
"agentMode": 8,
"question": "比亚迪(002594.SZ)的可比公司",
"stock": {
"code": "002594.SZ",
"name": "比亚迪"
},
"template": 1,
"templateText": "",
"templateConcern": "关注技术路线,盈利和出海能力"
}---
agentMode=9 — 观点 Challenge
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| question | 是 | String | 模板字符串,Challenge该观点:{templateText},比如:Challenge该观点:小米汽车未来的增长 |
| template | 是 | Integer | 1(固定参数) |
| templateText | 是 | String | 待 Challenge 的观点内容,与question中观点描述保持一致 |
| templateConcern | 否 | String | 关注焦点,可进一步限定 Challenge 范围,如 YU7 |
| requestSelectStartTime | 否 | String | 检索信息起始时间,格式 yyyy-MM-dd |
| requestSelectEndTime | 否 | String | 检索信息截止时间,格式 yyyy-MM-dd |
示例参数:
{
"agentMode": 9,
"question": "Challenge该观点:小米汽车未来的增长",
"template": 1,
"templateText": "小米汽车未来的增长",
"templateConcern": "YU7",
"requestSelectStartTime": "2024-10-25",
"requestSelectEndTime": "2025-10-24"
}---
agentMode=11 — 行业一页纸
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| question | 是 | String | 模板字符串,{inputIndustry}的行业一页纸,比如:白酒的行业一页纸 |
| inputIndustry | 是 | String | 行业名称,如 白酒、光伏 |
| template | 是 | Integer | 0(固定传alpha派模板);1 时需要填写templateText字段 |
| templateText | 否 | String | 用户自己的行业一页纸模板,template=1 时填写,默认传空字符串 |
示例参数:
{
"agentMode": 11,
"question": "白酒的行业一页纸",
"inputIndustry": "白酒",
"template": 0,
"templateText": ""
}---
agentMode=12 — 个股选基
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| question | 是 | String | 自然语言描述查询条件,格式参考:{报告期}持有{股票名列表}的基金有哪些?,比如:2025三季报持有环旭电子、中际旭创、卓胜微的基金有哪些? |
| stockList | 是 | List[Dict] | 股票列表,每项 {"code": "...", "name": "..."} |
| reportDate | 是 | String | 报告期对应日期,格式 yyyy-MM-dd;需要参考ifAnnual参数,每年四档:03-31一季报、06-30 二级报(ifAnnual=0)/中报(ifAnnual=1)、09-30三季报、12-31四季报(ifAnnual=0)/年报(ifAnnual=1),如 2025-09-30 |
| fundType | 是 | String | 基金类型筛选:全部 / 主动 / 指数 / ETF |
| template | 是 | Integer | 0(固定参数) |
| templateText | 否 | String | 暂未启用,默认传空字符串 |
| ifAnnual | 否 | Integer | 是否年报:0=否(默认),1=是 |
示例参数:
{
"agentMode": 12,
"question": "2025三季报持有环旭电子、中际旭创、卓胜微的基金有哪些?",
"fundType": "全部",
"reportDate": "2025-09-30",
"ifAnnual": 0,
"template": 0,
"templateText": "",
"stockList": [
{
"code": "601231.SH",
"name": "环旭电子"
},
{
"code": "300308.SZ",
"name": "中际旭创"
},
{
"code": "300782.SZ",
"name": "卓胜微"
}
]
}---
agentMode=13 — 主题选基
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| question | 是 | String | 自然语言描述查询条件,格式参考:{报告期}持有{主题}的{基金类型}基金有哪些?,比如:2025三季报持有房地产的主动基金有哪些? |
| reportDate | 是 | String | 报告期对应日期,格式 yyyy-MM-dd;需要参考ifAnnual参数,每年四档:03-31一季报、06-30 二级报(ifAnnual=0)/中报(ifAnnual=1)、09-30三季报、12-31四季报(ifAnnual=0)/年报(ifAnnual=1),如 2025-09-30,表示2025三季报 |
| fundType | 是 | String | 基金类型筛选:全部 / 主动 / 指数 / ETF |
| template | 是 | Integer | 0(固定参数) |
| templateText | 否 | String | 暂未启用,默认传空字符串 |
| ifAnnual | 否 | Integer | 是否年报:0=否(默认),1=是 |
示例参数:
{
"agentMode": 13,
"question": "2025三季报持有房地产的主动基金有哪些?",
"fundType": "主动",
"reportDate": "2025-09-30",
"ifAnnual": 0,
"template": 0,
"templateText": ""
}---
agentMode=15 — 画图
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| question | 是 | String | 画图主题,即用户实际输入的内容,比如:黄金、新能源汽车 |
| pictureColor | 是 | List[String] | 颜色HEX值,二元组,分别表示主色和辅色,不含 # 前缀,如 ["2A66F6", "A5A8AF"] |
| pictureStyle | 是 | String | 画图风格:PPT风格 / 科普风格 |
| source | 否 | Integer | 产出形式:0=仅图片(默认),1=图文 |
示例参数:
{
"agentMode": 15,
"question": "黄金",
"pictureColor": ["2A66F6", "A5A8AF"],
"pictureStyle": "科普风格",
"source": 0
}---
4. 获取股票公告列表
URL: POST /alpha/open-api/v1/paipai/stock/report
根据股票代码获取上市公司在交易所公开的公告列表,常用于查询业绩点评所需的 stockReportId。
请求参数
| 字段 | 必填 | 类型 | 说明 | 示例值 |
|---|---|---|---|---|
| code | 是 | String | 股票编码 | "603380.SH" |
响应参数 (data 列表每条)
| 字段 | 类型 | 说明 |
|---|---|---|
| stockCode | String | 股票编码 |
| stockName | String | 股票名称 |
| reportType | String | 报告类型 |
| reportPeriod | String | 报告期 |
| stockReportId | String | 公告ID |
| stockReportTitle | String | 报告标题 |
---
5. 搜图表
URL: POST /alpha/open-api/v1/paipai/search-image
根据语义从研报和公告中搜索相关图片和表格。
请求参数
| 字段 | 必填 | 类型 | 说明 | 示例值 |
|---|---|---|---|---|
| queryText | 是 | String | 检索查询的文本内容 | "黄金价格趋势" |
| filesRange | 否 | List[String] | 来源类型代码列表,可选值:3-内资研报,8-外资研报,6-公告,9-三方研报 | ["3","8"] |
| topk | 否 | Integer | 返回结果数量,范围1-100,默认50 | 80 |
| recallMode | 否 | String | 召回模式: both(默认)/vector_only/es_only | "both" |
| useLlmRank | 否 | Boolean | 是否使用LLM重排序,默认false | false |
| startDate | 否 | String | 开始日期,格式 yyyy-MM-dd | "2025-01-01" |
| endDate | 否 | String | 结束日期,格式 yyyy-MM-dd | "2025-10-01" |
响应参数 (data 列表每条)
| 字段 | 类型 | 说明 |
|---|---|---|
| articleId | String | 文章ID |
| articleTitle | String | 文章标题 |
| articleType | String | 文章类型 |
| publishDate | String | 发布日期 yyyy-MM-dd HH:mm:ss |
| pageIndex | Integer | 页码索引 |
| bbox | String | 图片在页面上的位置坐标 [x1, y1, x2, y2] |
| captionList | List[String] | 图片标题列表 |
| footnoteList | List[String] | 脚注列表 |
| imageUrl | String | 图片URL |
| source | String | 图片来源 |
| llmScore | Float | LLM评分 |
| llmReason | String | LLM评分理由 |
| institutionList | List[Dict] | 机构信息列表 (code, name, logo) |
| authorList | List[String] | 作者列表 |
---
AlphaPAI API 参考
所有端点前缀: https://alphapai-web.rabyte.cn/external/alpha/api/>
认证: HeaderAuthorization: <JWT token>+x-from: web(不带 Bearer 前缀)
认证
登录
POST v2/authentication/accountLogin
Body: {"mobile": "手机号", "password": "密码"}
Response: {"code": 200000, "data": {"token": "eyJ..."}}Token 有效期约 30 天。注意:新登录会使旧 token 立即失效(单设备互踢),不要在多处同时登录。
用户信息
GET v2/authorization/user/info
Response: {"code": 200000, "data": {"id": 81476, "uid": "...", "name": "刘崇武", ...}}搜索
智能搜索(综合)
POST reading/smartSearch
Body: {"word": "关键词"}
Response.data keys:
- roadshowSummary: {total, list[]} — 路演纪要
- wechatArticleInstitution: {total, list[]} — 机构公众号
- wechatArticleIndustry: {total, list[]} — 行业公众号
- wechatArticleAccount: {total, list[]} — 公众号账号
- stock: {total, list[]} — 股票
- concept: {total, list[]} — 概念板块
- company: ... — 公司
- investigation: ... — 调研股票搜索
GET reading/stock/search?keyword=关键词
Response.data: {total, list, tip}搜索建议
GET reading/stock/searchSuggest?keyword=关键词路演纪要
纪要列表
POST reading/roadshow/summary/list
Body: {"pageNum": 1, "pageSize": 20}
Response.data: {total: 353562, list: [{id, title, analyst, industry, stock, publishInstitution, content, roadshowDate, aiSummary, aiSummaryV3, ...}]}纪要详情
GET reading/roadshow/summary/detail?id=<纪要id>
Response.data 关键字段:
- title: 标题
- content: 原文(HTML,可能为 null 表示无权限)
- aiSummary: {id, title, content(HTML)} — AI 摘要 v1
- aiSummaryV3: {fullTextSummary(纯文本), summarySegment[{startTime, endTime, content}]} — AI 摘要 v3(推荐)
- analyst, industry, stock, publishInstitution, roadshowDate, duration, wordCount纪要搜索
POST reading/roadshow/summary/search
Body: {"keyword": "关键词", "pageNum": 1, "pageSize": 20}热门路演推荐
GET reading/roadshow/summary/hot/recommend
Response.data: {total, list[]}注意: 列表接口不返回 AI 摘要字段,需逐条调 roadshow/summary/detail 获取。
点评
点评搜索
POST reading/comment/search
Body: {"word": "关键词", "pageNum": 1, "pageSize": 20}
Response.data: {total, list: [{id, title, content, institution, stock, analyst, time, ...}]}点评详情
GET reading/comment/detail?commentId=<点评id>
Response.data 关键字段:
- title, content(纯文本正文), institution, stock, analyst, time
- stockEarningForecastVoList — 盈利预测
- type: 类型(如"个股推荐"等)注意: 参数名是 commentId,不是 id。
点评推荐
GET reading/comment/list/recommend
Response.data: {type, list[]}热门话题
当前热门批次列表
GET mix/hot/topic/current/batch/list
Response.data: [{id, topicName, newFlag, upFlag, continueFlag, summary, index, stock}]返回当前所有热门话题(约 20 条),每条含话题名、摘要、关联股票。
话题详情
GET mix/hot/topic/detail?id=<话题id>
Response.data: {id, topicName, summary, stock, newFlag, upFlag, continueFlag, index}话题 AI 深度分析
GET mix/hot/topic/agent/detail?id=<话题id>
Response.data: {sessionId, question, answer, type:"ai_chat", ...}返回 AI 对该话题的深度分析。
最新热门报告
GET mix/hot/topic/report/latest
Response.data: {id, title, content, contentJson, audio, batchName, date, updateTime}热门报告列表 v2
GET mix/hot/topic/report/latest/v2
Response.data: list[]热门股票(机构持仓)
GET mix/hot/topic/stock/list
Response.data: {publicList, privateList, insuranceList, updateTime}按公募/私募/险资分类的热门股票。
热门研报推荐
GET reading/report/hot/recommend
Response.data: {total, list[]}热词
GET reading/hot/word
Response.data: list[10] — 当前热搜词AI 问答
意图识别
POST mix/chat/service/intent
Body: {"query": "你的问题"}
Response.data: {"intent_flag": true/false, "entities": ...}注意: 参数名是 query,不是 question 或 content。
对话历史
POST mix/paipai/common/service/history/session/list
Body: {"current": 1, "size": 20}
Response.data: {total, records[], size, current}其他
今日计数
GET reading/count/today
Response.data: {roadshowSummaryNum, investigationNum, industryWechatArticleNum, institutionWechatArticleNum, commentNum}微信文章首页
GET reading/wechat/article/all/home
Response.data: list[3] — 按分类的文章列表
每条: {id, pv, industry, title, publishDate, sourceId}分析师热门话题
GET reading/analyst/hot/topic/list
Response.data: {pageNum, pageSize, totalPageNum, totalSize, data[]}错误码
| code | 含义 |
|---|---|
| 200000 | 成功 |
| 401000 | 未认证 / token 过期 |
| 400000 | 参数错误 |
| 500000 | 服务端错误(通常是参数格式不对) |
| 500303 | 模板参数缺失 |
#!/usr/bin/env python3
"""
AlphaPai Research CLI
=====================
Alpha派投研 API 命令行工具。
用法:
python alphapai_client.py config --set-key YOUR_KEY
python alphapai_client.py qa --question "贵州茅台2024年经营情况?"
python alphapai_client.py recall --query "茅台市值" --type comment,qa --start 2025-01-01
模块用法(供其他脚本导入):
from alphapai_client import AlphaPaiClient, load_config
client = AlphaPaiClient(load_config())
result = client.qa_text("问题")
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.error
from typing import Any, Dict, List, Optional
# ============================================================
# 配置管理
# ============================================================
DEFAULT_CONFIG_PATH = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json"
)
def _parse_env_file(env_path: str) -> Dict[str, str]:
"""解析 .env 文件,返回 key-value 字典"""
result = {}
if not os.path.exists(env_path):
return result
try:
with open(env_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
key, _, value = line.partition("=")
key = key.strip()
value = value.strip().strip('"').strip("'")
if key:
result[key] = value
except Exception:
pass
return result
def _find_env_value(key: str, start_dir: str) -> Optional[str]:
"""从当前目录向上逐级查找 .env 文件中的 key
查找顺序:
1. start_dir/.env
2. start_dir/../.env (上一级)
3. start_dir/../../.env (上上级)
"""
env_file = ".env"
max_levels = 3 # 当前 + 上两级
current_dir = start_dir
for _ in range(max_levels):
env_path = os.path.join(current_dir, env_file)
env_dict = _parse_env_file(env_path)
if key in env_dict and env_dict[key]:
return env_dict[key]
# 向上一级
parent = os.path.dirname(current_dir)
if parent == current_dir: # 已到根目录
break
current_dir = parent
return None
def load_config(config_path: str = DEFAULT_CONFIG_PATH) -> Optional[Dict]:
"""加载API配置,按优先级查找
查找顺序:
1. 环境变量 ALPHAPAI_API_KEY / ALPHAPAI_BASE_URL
2. 同级目录 .env
3. 上一级目录 .env
4. 上上级目录 .env
5. config.json
返回 {"api_key": "...", "base_url": "..."} 或 None
"""
base_url = "https://open-api.rabyte.cn"
# 1. 优先从环境变量读取(推荐方式,最安全)
env_key = os.environ.get("ALPHAPAI_API_KEY")
env_url = os.environ.get("ALPHAPAI_BASE_URL")
if env_url:
base_url = env_url
if env_key:
return {"api_key": env_key, "base_url": base_url}
# 2-4. 从 .env 文件逐级查找
skill_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
env_key = _find_env_value("ALPHAPAI_API_KEY", skill_dir)
env_url = _find_env_value("ALPHAPAI_BASE_URL", skill_dir)
if env_url:
base_url = env_url
if env_key:
return {"api_key": env_key, "base_url": base_url}
# 5. fallback 到 config.json(兼容旧方式)
if os.path.exists(config_path):
try:
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
if config.get("api_key"):
config.setdefault("base_url", base_url)
return config
except Exception:
pass
return None
def save_config(
api_key: str,
base_url: str = "https://open-api.rabyte.cn",
config_path: str = DEFAULT_CONFIG_PATH,
) -> Dict:
"""保存API配置到 config.json"""
config = {"api_key": api_key, "base_url": base_url}
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_path, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
return config
# ============================================================
# API Client(可单独导入使用)
# ============================================================
class AlphaPaiClient:
"""AlphaPai Open API 客户端(零第三方依赖,使用 urllib)"""
def __init__(self, config: Dict):
self.base_url = config["base_url"].rstrip("/")
self.headers = {
"app-agent": config["api_key"],
"Content-Type": "application/json",
}
def _post(self, endpoint: str, payload: Dict, stream: bool = False) -> Any:
"""通用 POST 请求。
stream=True 时返回原始 HTTPResponse(用于 SSE 流式解析),
stream=False 时返回解析后的 JSON 字典。
"""
url = f"{self.base_url}{endpoint}"
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(url, data=body, headers=self.headers, method="POST")
resp = urllib.request.urlopen(req, timeout=120)
if stream:
return resp
data = resp.read().decode("utf-8", errors="replace")
return json.loads(data)
@staticmethod
def parse_sse(response) -> Dict:
"""解析 SSE 流式响应,返回 {"answer": str, "references": list}。
从 urllib HTTPResponse 逐块读取,拼接后按 SSE 事件(双换行分隔)解析。
"""
buffer, answer, references = "", "", []
while True:
chunk = response.read(4096)
if not chunk:
break
buffer += chunk.decode("utf-8", errors="replace")
while "\n\n" in buffer:
event, buffer = buffer.split("\n\n", 1)
event = event.strip()
if not event.startswith("data: "):
continue
line = event[6:].strip()
if not line:
continue
try:
data = json.loads(line)
if isinstance(data, dict):
d = data.get("data", data)
if d.get("answer"):
answer += d["answer"]
if d.get("references"):
references.extend(d["references"])
except json.JSONDecodeError:
continue
return {"answer": answer, "references": references}
def qa_text(
self,
question: str,
context: Optional[List[str]] = None,
mode: str = "Flash",
is_stream: bool = False,
is_auto_route: bool = False,
is_web_search: bool = False,
is_deep_reasoning: bool = False,
start_time: Optional[str] = None,
end_time: Optional[str] = None,
question_id: Optional[str] = None,
) -> Dict:
"""投研知识问答接口。is_stream=True时内部解析SSE并返回聚合结果。"""
payload = {
"question": question,
"mode": mode,
"isStream": is_stream,
"isAutoRoute": is_auto_route,
"userInfo": {
"userCode": "001010",
"userName": "rabyte",
"roleName": "基金经理",
"roleCode": 5,
},
}
if context:
payload["context"] = context
if is_web_search:
payload["isWebSearch"] = True
if is_deep_reasoning:
payload["isDeepReasoning"] = True
if start_time:
payload["requestSelectStartTime"] = start_time
if end_time:
payload["requestSelectEndTime"] = end_time
if question_id:
payload["questionId"] = question_id
if is_stream:
return self.parse_sse(
self._post("/alpha/open-api/v1/paipai/qa-text", payload, stream=True)
)
return self._post("/alpha/open-api/v1/paipai/qa-text", payload)
def recall_data(
self,
query: str,
is_cut_off: bool = True,
recall_type: Optional[List[str]] = None,
start_time: Optional[str] = None,
end_time: Optional[str] = None,
) -> Dict:
"""获取问题召回数据"""
payload = {"query": query, "isCutOff": is_cut_off}
if recall_type:
payload["recallType"] = recall_type
else:
payload["recallType"] = []
if start_time:
payload["startTime"] = start_time
if end_time:
payload["endTime"] = end_time
return self._post("/alpha/open-api/v1/paipai/recall-data", payload)
def stock_agent(
self,
question: str,
agent_mode: int,
stock: Optional[Dict] = None,
template: int = 0,
template_text: str = "",
report_type: Optional[str] = None,
stock_report_id: Optional[str] = None,
stock_report_title: Optional[str] = None,
stock_report_period: Optional[str] = None,
template_concern: Optional[str] = None,
request_select_start_time: Optional[str] = None,
request_select_end_time: Optional[str] = None,
input_industry: Optional[str] = None,
report_date: Optional[str] = None,
fund_type: Optional[str] = None,
if_annual: Optional[int] = None,
stock_list: Optional[List[Dict]] = None,
picture_color: Optional[List[str]] = None,
picture_style: Optional[str] = None,
source: Optional[int] = None,
language: Optional[str] = None,
only_answer: Optional[bool] = None,
) -> Dict:
"""Alpha派投研Agent接口(公司一页纸、业绩点评、调研大纲等),SSE 流式请求。"""
payload: Dict = {
"question": question,
"agentMode": agent_mode,
"template": template,
"templateText": template_text,
}
if stock:
payload["stock"] = stock
if report_type:
payload["reportType"] = report_type
if stock_report_id:
payload["stockReportId"] = stock_report_id
if stock_report_title:
payload["stockReportTitle"] = stock_report_title
if stock_report_period:
payload["stockReportPeriod"] = stock_report_period
if template_concern:
payload["templateConcern"] = template_concern
if request_select_start_time:
payload["requestSelectStartTime"] = request_select_start_time
if request_select_end_time:
payload["requestSelectEndTime"] = request_select_end_time
if input_industry:
payload["inputIndustry"] = input_industry
if report_date:
payload["reportDate"] = report_date
if fund_type:
payload["fundType"] = fund_type
if if_annual is not None:
payload["ifAnnual"] = if_annual
if stock_list:
payload["stockList"] = stock_list
if picture_color:
payload["pictureColor"] = picture_color
if picture_style:
payload["pictureStyle"] = picture_style
if source is not None:
payload["source"] = source
if language:
payload["language"] = language
if only_answer is not None:
payload["onlyAnswer"] = only_answer
return self.parse_sse(
self._post("/alpha/open-api/v1/paipai/stock/agent", payload, stream=True)
)
def stock_report(self, code: str) -> Dict:
"""获取某个股票的公告列表(上市公司在交易所公开的公告)"""
return self._post("/alpha/open-api/v1/paipai/stock/report", {"code": code})
def search_image(
self,
query_text: str,
files_range: Optional[List[str]] = None,
topk: int = 50,
recall_mode: str = "both",
use_llm_rank: bool = False,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
) -> Dict:
"""搜图表:从研报/公告中搜索相关图片和表格"""
payload: Dict = {
"queryText": query_text,
"topk": topk,
"recallMode": recall_mode,
"useLlmRank": use_llm_rank,
}
if files_range:
payload["filesRange"] = files_range
if start_date:
payload["startDate"] = start_date
if end_date:
payload["endDate"] = end_date
return self._post("/alpha/open-api/v1/paipai/search-image", payload)
# ============================================================
# 输出格式化
# ============================================================
_TYPE_LABEL = {
"comment": "机构点评",
"vps": "基金定期报告",
"report": "内资研报",
"foreign_report": "外资研报",
"wechat_public_article": "公众号",
"ann": "公司公告",
"roadShow": "路演纪要",
"roadShow_ir": "上市公司披露投关纪要",
"roadShow_us": "美股纪要",
"table": "公告/研报表格",
"image": "研报图片",
"edb": "EDB数据库",
}
def _type_label(type_val: str) -> str:
return _TYPE_LABEL.get(type_val, type_val)
def _require_success(result: Dict):
"""检查响应码,非成功则打印错误并退出"""
if result.get("code") != 200000:
print(
f"[错误] code={result.get('code')} message={result.get('message')}",
file=sys.stderr,
)
sys.exit(1)
def format_qa(result: Dict) -> str:
# parse_sse returns {"answer": ..., "references": [...]} directly (no code wrapper)
if "answer" in result:
answer = result.get("answer", "(无回答)")
references = result.get("references", [])
else:
_require_success(result)
data = result.get("data", {})
answer = data.get("answer", "(无回答)")
references = data.get("references", [])
lines = ["[回答]", answer]
if references:
lines.append(f"\n[引用来源 {len(references)} 条]")
for i, ref in enumerate(references, 1):
date = f" ({ref.get('publishDate', '')})" if ref.get("publishDate") else ""
lines.append(
f" {i}. [{_type_label(ref.get('type', ''))}] {ref.get('title', '')}{date}"
)
return "\n".join(lines)
def format_recall(result: Dict) -> str:
_require_success(result)
items = result.get("data", [])
lines = [f"[召回数据 {len(items)} 条]\n"]
for i, item in enumerate(items, 1):
item_type = item.get("type", "")
context_info = item.get("contextInfo", "")
lines.append(f"#{i} [{_type_label(item_type)}]")
lines.append(f" {context_info}")
if item_type == "qa":
if item.get("contextText"):
lines.append(f" Q: {item['contextText']}")
if item.get("answer"):
text = item["answer"]
lines.append(f" A: {text[:300]}{'...' if len(text) > 300 else ''}")
elif item.get("chunks"):
text = item["chunks"][0]
lines.append(f" {text[:300]}{'...' if len(text) > 300 else ''}")
lines.append("")
return "\n".join(lines)
def format_agent(result: Dict) -> str:
if "answer" in result:
answer = result.get("answer", "(无回答)")
references = result.get("references", [])
else:
_require_success(result)
data = result.get("data", {})
answer = data.get("answer", "(无回答)")
references = data.get("references", [])
lines = ["[Agent 回答]", answer]
if references:
lines.append(f"\n[引用来源 {len(references)} 条]")
for i, ref in enumerate(references, 1):
date = f" ({ref.get('publishDate', '')})" if ref.get("publishDate") else ""
lines.append(
f" {i}. [{_type_label(ref.get('type', ''))}] {ref.get('title', '')}{date}"
)
return "\n".join(lines)
def format_report(result: Dict) -> str:
_require_success(result)
items = result.get("data", [])
if not items:
return "[公告列表] 未找到相关公告"
lines = [f"[公告列表 {len(items)} 条]\n"]
for i, item in enumerate(items, 1):
lines.append(
f"#{i} [{item.get('reportType', '')}] {item.get('stockReportTitle', '')}"
)
lines.append(
f" 期间: {item.get('reportPeriod', '')} ID: {item.get('stockReportId', '')}"
)
lines.append("")
return "\n".join(lines)
def format_image(result: Dict) -> str:
_require_success(result)
items = result.get("data", [])
if not items:
return "[图表搜索] 未找到相关图表"
lines = [f"[图表搜索 {len(items)} 条]\n"]
for i, item in enumerate(items, 1):
lines.append(f"#{i} {item.get('articleTitle', '')}")
caption = ", ".join(item.get("captionList") or [])
if caption:
lines.append(f" 说明: {caption}")
lines.append(
f" 来源: {item.get('source', '')} 日期: {item.get('publishDate', '')}"
)
if item.get("imageUrl"):
lines.append(f" 图片: {item['imageUrl']}")
lines.append("")
return "\n".join(lines)
# ============================================================
# CLI 子命令
# ============================================================
def cmd_config(args):
config_path = DEFAULT_CONFIG_PATH
if args.set_key or args.set_url:
config = load_config(config_path) or {}
if args.set_key:
config["api_key"] = args.set_key
if args.set_url:
config["base_url"] = args.set_url
config.setdefault("base_url", "https://open-api.rabyte.cn")
save_config(config["api_key"], config["base_url"], config_path)
key = config["api_key"]
masked = key[:4] + "****" + key[-4:] if len(key) > 8 else "****"
print(f"配置已保存: api_key={masked} base_url={config['base_url']}")
print(f"路径: {config_path}")
elif args.show:
config = load_config(config_path)
if not config:
print(
"未找到配置,请先运行: python alphapai_client.py config --set-key YOUR_KEY"
)
sys.exit(1)
key = config["api_key"]
masked = key[:4] + "****" + key[-4:] if len(key) > 8 else "****"
print(f"api_key : {masked}")
print(f"base_url: {config['base_url']}")
print(f"路径 : {config_path}")
else:
print(
"用法: python alphapai_client.py config --show | --set-key KEY [--set-url URL]"
)
def cmd_qa(args):
config = load_config()
if not config:
print(
"[错误] 未找到配置,请先运行: python alphapai_client.py config --set-key YOUR_KEY",
file=sys.stderr,
)
sys.exit(1)
client = AlphaPaiClient(config)
result = client.qa_text(
question=args.question,
context=args.context or None,
mode=args.mode,
is_stream=True,
is_web_search=args.web_search,
is_deep_reasoning=args.deep_reasoning,
start_time=args.start,
end_time=args.end,
)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(format_qa(result))
def cmd_recall(args):
config = load_config()
if not config:
print(
"[错误] 未找到配置,请先运行: python alphapai_client.py config --set-key YOUR_KEY",
file=sys.stderr,
)
sys.exit(1)
client = AlphaPaiClient(config)
recall_type = [t.strip() for t in args.type.split(",")] if args.type else []
result = client.recall_data(
query=args.query,
is_cut_off=not args.no_cutoff,
recall_type=recall_type,
start_time=args.start,
end_time=args.end,
)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(format_recall(result))
def _parse_stock(s: str) -> Dict:
"""解析 CODE:NAME 格式的股票参数"""
parts = s.split(":", 1)
return {"code": parts[0], "name": parts[1] if len(parts) == 2 else ""}
def cmd_agent(args):
config = load_config()
if not config:
print(
"[错误] 未找到配置,请先运行: python alphapai_client.py config --set-key YOUR_KEY",
file=sys.stderr,
)
sys.exit(1)
client = AlphaPaiClient(config)
stock = _parse_stock(args.stock) if args.stock else None
stock_list = [_parse_stock(s) for s in args.stock_list] if args.stock_list else None
# modes 5/8/9 have template fixed at 1 per API spec
template = 1 if args.mode in (5, 8, 9) else args.template
result = client.stock_agent(
question=args.question,
agent_mode=args.mode,
stock=stock,
template=template,
template_text=args.template_text or "",
report_type=args.report_type,
stock_report_id=args.report_id,
stock_report_title=args.report_title,
stock_report_period=args.report_period,
template_concern=args.concern,
request_select_start_time=args.start,
request_select_end_time=args.end,
input_industry=args.industry,
report_date=args.report_date,
fund_type=args.fund_type,
if_annual=args.if_annual,
stock_list=stock_list,
picture_color=args.picture_color,
picture_style=args.picture_style,
source=args.source,
language=args.language,
only_answer=True if args.only_answer else None,
)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(format_agent(result))
def cmd_report(args):
config = load_config()
if not config:
print(
"[错误] 未找到配置,请先运行: python alphapai_client.py config --set-key YOUR_KEY",
file=sys.stderr,
)
sys.exit(1)
client = AlphaPaiClient(config)
result = client.stock_report(code=args.code)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(format_report(result))
def cmd_image(args):
config = load_config()
if not config:
print(
"[错误] 未找到配置,请先运行: python alphapai_client.py config --set-key YOUR_KEY",
file=sys.stderr,
)
sys.exit(1)
client = AlphaPaiClient(config)
result = client.search_image(
query_text=args.query,
files_range=args.files_range,
topk=args.topk,
recall_mode=args.recall_mode,
use_llm_rank=args.llm_rank,
start_date=args.start,
end_date=args.end,
)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(format_image(result))
# ============================================================
# CLI 入口
# ============================================================
def main():
parser = argparse.ArgumentParser(
prog="alphapai_client.py",
description="AlphaPai Research CLI — Alpha派投研API命令行工具",
)
sub = parser.add_subparsers(dest="command", required=True)
# --- config ---
p_cfg = sub.add_parser("config", help="查看或设置API配置")
p_cfg.add_argument("--show", action="store_true", help="显示当前配置")
p_cfg.add_argument("--set-key", metavar="KEY", help="设置api_key")
p_cfg.add_argument(
"--set-url",
metavar="URL",
help="设置base_url(默认 https://open-api.rabyte.cn)",
)
# --- qa ---
p_qa = sub.add_parser("qa", help="投研知识问答")
p_qa.add_argument("--question", "-q", required=True, help="问题内容")
p_qa.add_argument(
"--mode",
default="Flash",
choices=["Flash", "Think"],
help="问答模式: Flash=简单搜索问答,一问一搜一答(默认); Think=Wide Search,一问多搜一答",
)
p_qa.add_argument(
"--context",
"-c",
nargs="+",
metavar="MSG",
help="多轮对话上下文,按顺序传入历史消息列表",
)
p_qa.add_argument("--web-search", action="store_true", help="开启联网搜索")
p_qa.add_argument("--deep-reasoning", action="store_true", help="开启深度推理")
p_qa.add_argument("--start", metavar="YYYY-MM-DD", help="数据筛选开始日期")
p_qa.add_argument("--end", metavar="YYYY-MM-DD", help="数据筛选结束日期")
p_qa.add_argument("--json", action="store_true", help="输出原始JSON(供程序解析)")
# --- recall ---
p_rc = sub.add_parser("recall", help="获取问题召回的底层数据")
p_rc.add_argument("--query", "-q", required=True, help="查询问题")
p_rc.add_argument(
"--type",
"-t",
metavar="TYPES",
help="数据类型,逗号分隔,如 comment,qa,report(不传则全类型)",
)
p_rc.add_argument(
"--no-cutoff",
action="store_true",
help="返回截断前完整内容(默认截断,与送入大模型的数据一致)",
)
p_rc.add_argument("--start", metavar="YYYY-MM-DD", help="数据筛选开始日期")
p_rc.add_argument("--end", metavar="YYYY-MM-DD", help="数据筛选结束日期")
p_rc.add_argument("--json", action="store_true", help="输出原始JSON(供程序解析)")
# --- agent ---
p_ag = sub.add_parser(
"agent", help="股票 Agent(公司一页纸、业绩点评、调研大纲等)"
)
p_ag.add_argument(
"--mode",
"-m",
type=int,
required=True,
help="Agent模式: 1=业绩点评 2=公司一页纸 3=调研大纲 5=主题选股 "
"7=投资逻辑 8=可比公司 9=观点Challenge 11=行业一页纸 "
"12=个股选基 13=主题选基 15=画图",
)
p_ag.add_argument("--question", "-q", required=True, help="问题内容")
p_ag.add_argument(
"--stock",
metavar="CODE:NAME",
help="股票信息,格式 CODE:NAME,如 300014.SZ:亿纬锂能",
)
p_ag.add_argument(
"--template",
type=int,
default=0,
help="模版类型: 0=alpha派模板(默认) 1=用户模板",
)
p_ag.add_argument(
"--template-text", metavar="TEXT", help="用户模版正文(template=1时需传)"
)
p_ag.add_argument(
"--report-type", metavar="TYPE", help="报告类型(业绩点评必填),如: 季报"
)
p_ag.add_argument("--report-id", metavar="ID", help="公告ID(业绩点评必填)")
p_ag.add_argument(
"--report-title", metavar="TITLE", help="报告标题(业绩点评必填)"
)
p_ag.add_argument(
"--report-period",
metavar="PERIOD",
help="报告期(业绩点评必填),如: 2025年一季报",
)
p_ag.add_argument(
"--concern", metavar="TEXT", help="用户关注内容(业绩点评/观点Challenge选填)"
)
p_ag.add_argument(
"--industry", metavar="NAME", help="行业信息(行业一页纸必填),如: 白酒"
)
p_ag.add_argument(
"--report-date",
metavar="DATE",
help="报告期(个股选基/主题选基必填),如: 2025-09-30",
)
p_ag.add_argument(
"--fund-type",
metavar="TYPE",
help="基金类型(个股选基/主题选基必填): 全部|主动|指数|ETF",
)
p_ag.add_argument(
"--if-annual", type=int, choices=[0, 1], help="是否年报: 0=否 1=是"
)
p_ag.add_argument(
"--stock-list",
nargs="+",
metavar="CODE:NAME",
help="股票列表(个股选基必填),如: 601231.SH:环旭电子 300308.SZ:中际旭创",
)
p_ag.add_argument(
"--picture-color",
nargs="+",
metavar="HEX",
help="图片颜色HEX值(画图必填),如: 2A66F6 A5A8AF",
)
p_ag.add_argument(
"--picture-style",
metavar="STYLE",
help="图片风格(画图必填): PPT风格|科普风格",
)
p_ag.add_argument(
"--source",
type=int,
choices=[0, 1],
help="画图样式: 0=仅图片(默认) 1=图文",
)
p_ag.add_argument(
"--language", metavar="LANG", help="语言(美股公司一页纸可选): 中文|英文"
)
p_ag.add_argument(
"--only-answer",
action="store_true",
help="仅返回最终答案(mode 7 投资逻辑可选)",
)
p_ag.add_argument("--start", metavar="YYYY-MM-DD", help="数据开始日期")
p_ag.add_argument("--end", metavar="YYYY-MM-DD", help="数据结束日期")
p_ag.add_argument("--json", action="store_true", help="输出原始JSON(供程序解析)")
# --- report ---
p_rp = sub.add_parser("report", help="获取股票公告列表(供业绩点评查询公告ID)")
p_rp.add_argument("--code", "-c", required=True, help="股票编码,如 603380.SH")
p_rp.add_argument("--json", action="store_true", help="输出原始JSON(供程序解析)")
# --- image ---
p_img = sub.add_parser("image", help="搜图表(从研报/公告中搜索图片和表格)")
p_img.add_argument("--query", "-q", required=True, help="搜索内容")
p_img.add_argument(
"--files-range",
nargs="+",
metavar="CODE",
help="来源类型代码列表,可选值: 3=内资研报 8=外资研报 6=公告 9=三方研报",
)
p_img.add_argument("--topk", type=int, default=50, help="返回数量(1-100,默认50)")
p_img.add_argument(
"--recall-mode",
default="both",
choices=["both", "vector_only", "es_only"],
help="召回模式(默认both)",
)
p_img.add_argument("--llm-rank", action="store_true", help="使用LLM重排序")
p_img.add_argument("--start", metavar="YYYY-MM-DD", help="开始日期")
p_img.add_argument("--end", metavar="YYYY-MM-DD", help="结束日期")
p_img.add_argument("--json", action="store_true", help="输出原始JSON(供程序解析)")
args = parser.parse_args()
{
"config": cmd_config,
"qa": cmd_qa,
"recall": cmd_recall,
"agent": cmd_agent,
"report": cmd_report,
"image": cmd_image,
}[args.command](args)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
AlphaPai Web Client
===================
Alpha派 Web版 API 客户端(账号密码认证)。
用于获取路演纪要、点评、热门话题等功能。
"""
import base64
import json
import os
import time
import urllib.request
import urllib.parse
from pathlib import Path
from typing import Dict, List, Optional, Any
from html.parser import HTMLParser
BASE_URL = "https://alphapai-web.rabyte.cn/external/alpha/api"
def _strip_html(html: str) -> str:
"""去除 HTML 标签"""
class _Strip(HTMLParser):
def __init__(self):
super().__init__()
self.parts = []
def handle_data(self, data):
self.parts.append(data)
s = _Strip()
s.feed(html or "")
return "".join(s.parts)
def _decode_jwt_exp(token: str) -> float:
"""从 JWT token 中提取 exp 时间戳。
解析 JWT 第二段(payload)的 base64 编码,提取 exp 字段。
返回 0 表示无法解析(不阻塞后续流程,由 API 401 兜底)。
"""
try:
payload = token.split(".")[1]
# 补齐 base64 padding
payload += "=" * (4 - len(payload) % 4)
data = json.loads(base64.b64decode(payload))
return float(data.get("exp", 0))
except Exception:
return 0
class AlphaPaiWebClient:
"""Alpha派 Web版 API 客户端"""
def __init__(self, account: str, password: str, token_cache_path: str = None):
self.account = account
self.password = password
self.token_cache_path = token_cache_path or os.path.join(
os.path.dirname(__file__), "..", "auth_state.json"
)
self.token = None
self._load_token()
def _load_token(self):
"""从缓存加载 token,兼容两种格式,检查 JWT 过期时间。
格式1(简化): {"token": "...", "account": "..."}
格式2(origins): {"origins": [{"localStorage": [{"name": "USER_AUTH_TOKEN", "value": "..."}]}]}
"""
if not os.path.exists(self.token_cache_path):
return
try:
with open(self.token_cache_path, "r") as f:
data = json.load(f)
# 优先读 token 字段
token = data.get("token")
# fallback: origins 格式
if not token:
origins = data.get("origins", [])
if origins:
for item in origins[0].get("localStorage", []):
if item.get("name") == "USER_AUTH_TOKEN":
token = item.get("value")
break
if not token:
return
# 检查 JWT 过期时间,提前 60 秒判定过期
exp = _decode_jwt_exp(token)
if exp > 0 and time.time() > exp - 60:
import sys
print("[auth] 缓存 token 已过期,需重新登录", file=sys.stderr)
return
self.token = token
except Exception:
pass
def _save_token(self):
"""保存 token 到缓存,同时写入两种格式保持兼容。"""
os.makedirs(os.path.dirname(self.token_cache_path), exist_ok=True)
auth = {
"origin": "https://alphapai-web.rabyte.cn",
"token": self.token,
"account": self.account,
"origins": [{
"origin": "https://alphapai-web.rabyte.cn",
"localStorage": [
{"name": "USER_AUTH_TOKEN", "value": self.token},
],
}],
}
with open(self.token_cache_path, "w") as f:
json.dump(auth, f, indent=2, ensure_ascii=False)
def _request(self, method: str, endpoint: str, data: dict = None, auth: bool = True) -> dict:
"""发送请求"""
url = f"{BASE_URL}/{endpoint}"
headers = {"Content-Type": "application/json"}
if auth and self.token:
headers["Authorization"] = self.token
headers["x-from"] = "web"
body = None
if data:
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
if e.code == 401:
# Token 过期,重新登录
self._login()
headers["Authorization"] = self.token
req = urllib.request.Request(url, data=body, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
raise
def _login(self):
"""登录获取 token"""
data = {"mobile": self.account, "password": self.password}
result = self._request("POST", "v2/authentication/accountLogin", data, auth=False)
if result.get("code") == 200000:
self.token = result.get("data", {}).get("token")
self._save_token()
else:
raise Exception(f"登录失败: {result.get('message', '未知错误')}")
def ensure_login(self):
"""确保已登录"""
if not self.token:
self._login()
# ========== 路演纪要 ==========
def get_roadshow_list(self, page: int = 1, page_size: int = 20) -> dict:
"""获取路演纪要列表"""
self.ensure_login()
return self._request("POST", "reading/roadshow/summary/list", {
"pageNum": page, "pageSize": page_size
})
def get_roadshow_detail(self, roadshow_id: str) -> dict:
"""获取路演纪要详情(含AI摘要)"""
self.ensure_login()
result = self._request("GET", f"reading/roadshow/summary/detail?id={roadshow_id}")
return result.get("data", {})
def search_roadshow(self, keyword: str, page: int = 1, page_size: int = 20) -> dict:
"""搜索路演纪要"""
self.ensure_login()
return self._request("POST", "reading/roadshow/summary/search", {
"keyword": keyword, "pageNum": page, "pageSize": page_size
})
def get_hot_roadshow(self) -> dict:
"""获取热门路演推荐"""
self.ensure_login()
result = self._request("GET", "reading/roadshow/summary/hot/recommend")
return result.get("data", {})
# ========== 点评 ==========
def search_comment(self, keyword: str, page: int = 1, page_size: int = 20) -> dict:
"""搜索点评"""
self.ensure_login()
return self._request("POST", "reading/comment/search", {
"word": keyword, "pageNum": page, "pageSize": page_size
})
def get_comment_detail(self, comment_id: str) -> dict:
"""获取点评详情"""
self.ensure_login()
result = self._request("GET", f"reading/comment/detail?commentId={comment_id}")
return result.get("data", {})
def get_comment_recommend(self) -> dict:
"""获取推荐点评"""
self.ensure_login()
result = self._request("GET", "reading/comment/list/recommend")
return result.get("data", {})
# ========== 热门话题 ==========
def get_hot_topics(self) -> List[dict]:
"""获取当前热门话题列表"""
self.ensure_login()
result = self._request("GET", "mix/hot/topic/current/batch/list")
return result.get("data", [])
def get_hot_topic_detail(self, topic_id: str) -> dict:
"""获取话题详情"""
self.ensure_login()
result = self._request("GET", f"mix/hot/topic/detail?id={topic_id}")
return result.get("data", {})
def get_hot_topic_agent(self, topic_id: str) -> dict:
"""获取话题AI分析"""
self.ensure_login()
result = self._request("GET", f"mix/hot/topic/agent/detail?id={topic_id}")
return result.get("data", {})
def get_hot_words(self) -> List[str]:
"""获取热搜词"""
self.ensure_login()
result = self._request("GET", "reading/hot/word")
return result.get("data", [])
def get_hot_report(self) -> dict:
"""获取最新热门报告"""
self.ensure_login()
result = self._request("GET", "mix/hot/topic/report/latest")
return result.get("data", {})
def get_hot_stocks(self) -> dict:
"""获取热门股票(机构持仓)"""
self.ensure_login()
result = self._request("GET", "mix/hot/topic/stock/list")
return result.get("data", {})
# ========== 搜索 ==========
def smart_search(self, keyword: str) -> dict:
"""智能搜索(综合)"""
self.ensure_login()
result = self._request("POST", "reading/smartSearch", {"word": keyword})
return result.get("data", {})
def search_stock(self, keyword: str) -> dict:
"""搜索股票"""
self.ensure_login()
result = self._request("GET", f"reading/stock/search?keyword={urllib.parse.quote(keyword)}")
return result.get("data", {})
# ========== 其他 ==========
def get_user_info(self) -> dict:
"""获取用户信息"""
self.ensure_login()
result = self._request("GET", "v2/authorization/user/info")
return result.get("data", {})
def get_today_count(self) -> dict:
"""获取今日内容统计"""
self.ensure_login()
result = self._request("GET", "reading/count/today")
return result.get("data", {})
# 命令行入口(兼容旧用法)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="AlphaPai Web Client")
parser.add_argument("command", help="命令")
parser.add_argument("args", nargs="*", help="参数")
parser.add_argument("--detail", action="store_true", help="获取详情")
args = parser.parse_args()
# 从环境变量或 .env 读取账号密码
account = os.environ.get("ALPHAPAI_ACCOUNT")
password = os.environ.get("ALPHAPAI_PASSWORD")
if not account or not password:
# 尝试从 .env 读取
env_path = Path(__file__).parent.parent / ".env"
if env_path.exists():
for line in env_path.read_text().splitlines():
if line.startswith("ALPHAPAI_ACCOUNT="):
account = line.split("=", 1)[1].strip().strip('"')
elif line.startswith("ALPHAPAI_PASSWORD="):
password = line.split("=", 1)[1].strip().strip('"')
if not account or not password:
print("错误: 未配置账号密码,请设置 ALPHAPAI_ACCOUNT 和 ALPHAPAI_PASSWORD")
exit(1)
client = AlphaPaiWebClient(account, password)
if args.command == "topics":
topics = client.get_hot_topics()
for t in topics or []:
name = (t.get("topicName") or "")[:30]
summary = (t.get("summary") or "")[:50]
print(f"{name:30} | {summary}")
elif args.command == "topic":
if not args.args:
print("用法: alphapai_web_client.py topic <id>")
else:
detail = client.get_hot_topic_detail(args.args[0])
print(json.dumps(detail, ensure_ascii=False, indent=2))
elif args.command == "roadshow":
result = client.get_roadshow_list()
for r in result.get("list", []):
print(f"{r.get('roadshowDate', ''):12} | {r.get('title', '')[:60]}")
elif args.command == "roadshow-search":
if not args.args:
print("用法: alphapai_web_client.py roadshow-search <关键词>")
else:
result = client.search_roadshow(args.args[0])
for r in result.get("list", []):
print(f"{r.get('roadshowDate', ''):12} | {r.get('title', '')[:60]}")
elif args.command == "roadshow-detail":
if not args.args:
print("用法: alphapai_web_client.py roadshow-detail <id>")
else:
detail = client.get_roadshow_detail(args.args[0])
print(f"# {detail.get('title', '')}")
if detail.get("aiSummaryV3"):
print(detail["aiSummaryV3"].get("fullTextSummary", ""))
elif args.command == "comment-search":
if not args.args:
print("用法: alphapai_web_client.py comment-search <关键词>")
else:
result = client.search_comment(args.args[0])
for r in result.get("list", []):
print(f"{r.get('time', ''):12} | {r.get('title', '')[:60]}")
elif args.command == "comment-detail":
if not args.args:
print("用法: alphapai_web_client.py comment-detail <id>")
else:
detail = client.get_comment_detail(args.args[0])
print(f"# {detail.get('title', '')}")
print(detail.get("content", "")[:2000])
elif args.command == "hot-word":
words = client.get_hot_words()
for i, w in enumerate(words, 1):
print(f"{i:2}. {w}")
else:
print(f"未知命令: {args.command}")
print("可用命令: topics, topic, roadshow, roadshow-search, roadshow-detail, comment-search, comment-detail, hot-word")#!/usr/bin/env python3
"""
AlphaPai Unified CLI
====================
Alpha派统一命令行工具,融合官方API和Web版功能。
用法:
python alphapai.py qa "茅台经营情况"
python alphapai.py recall "新能源" --type report
python alphapai.py company 300014.SZ
python alphapai.py roadshow --search "茅台业绩会"
python alphapai.py hot
认证优先级:
1. 环境变量 ALPHAPAI_API_KEY
2. 同级/上级/上上级目录 .env
3. config.json
4. 账号密码(ALPHAPAI_ACCOUNT/ALPHAPAI_PASSWORD)
"""
import argparse
import json
import os
import sys
from typing import Any, Dict, List, Optional
# 添加脚本目录到路径
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SCRIPT_DIR)
# ============================================================
# 配置管理(融合版)
# ============================================================
DEFAULT_CONFIG_PATH = os.path.join(os.path.dirname(SCRIPT_DIR), "config.json")
def _parse_env_file(env_path: str) -> Dict[str, str]:
"""解析 .env 文件"""
result = {}
if not os.path.exists(env_path):
return result
try:
with open(env_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
key, _, value = line.partition("=")
key = key.strip()
value = value.strip().strip('"').strip("'")
if key:
result[key] = value
except Exception:
pass
return result
def _find_env_value(key: str, start_dir: str) -> Optional[str]:
"""从当前目录向上逐级查找 .env 文件中的 key"""
max_levels = 3
current_dir = start_dir
for _ in range(max_levels):
env_path = os.path.join(current_dir, ".env")
env_dict = _parse_env_file(env_path)
if key in env_dict and env_dict[key]:
return env_dict[key]
parent = os.path.dirname(current_dir)
if parent == current_dir:
break
current_dir = parent
return None
def load_config() -> Dict:
"""加载配置,返回包含所有认证信息的字典"""
skill_dir = os.path.dirname(SCRIPT_DIR)
# 从环境变量或.env读取
api_key = os.environ.get("ALPHAPAI_API_KEY")
if not api_key:
api_key = _find_env_value("ALPHAPAI_API_KEY", skill_dir)
base_url = os.environ.get("ALPHAPAI_BASE_URL")
if not base_url:
base_url = _find_env_value("ALPHAPAI_BASE_URL", skill_dir)
if not base_url:
base_url = "https://open-api.rabyte.cn"
# 账号密码(Web版)
account = os.environ.get("ALPHAPAI_ACCOUNT")
password = os.environ.get("ALPHAPAI_PASSWORD")
if not account:
account = _find_env_value("ALPHAPAI_ACCOUNT", skill_dir)
if not password:
password = _find_env_value("ALPHAPAI_PASSWORD", skill_dir)
# 从 config.json 读取
config_path = DEFAULT_CONFIG_PATH
if os.path.exists(config_path):
try:
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
if not api_key and config.get("api_key"):
api_key = config["api_key"]
if not base_url and config.get("base_url"):
base_url = config["base_url"]
if not account and config.get("account"):
account = config["account"]
if not password and config.get("password"):
password = config["password"]
except Exception:
pass
return {
"api_key": api_key,
"base_url": base_url,
"account": account,
"password": password,
"config_path": config_path,
}
def save_config(api_key: str = None, base_url: str = None, account: str = None, password: str = None):
"""保存配置到 config.json"""
config = {}
config_path = DEFAULT_CONFIG_PATH
# 读取现有配置
if os.path.exists(config_path):
try:
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
except Exception:
pass
# 更新
if api_key is not None:
config["api_key"] = api_key
if base_url is not None:
config["base_url"] = base_url
if account is not None:
config["account"] = account
if password is not None:
config["password"] = password
# 写入
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_path, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
# ============================================================
# 命令处理
# ============================================================
def cmd_config(args):
"""配置管理"""
if args.set_key:
save_config(api_key=args.set_key)
print(f"API Key 已保存")
elif args.set_account and args.set_password:
save_config(account=args.set_account, password=args.set_password)
print(f"账号密码已保存")
elif args.show:
config = load_config()
print(f"API Key : {config['api_key'][:8] + '****' + config['api_key'][-4:] if config['api_key'] else '未配置'}")
print(f"Base URL : {config['base_url']}")
print(f"Account : {config['account'] or '未配置'}")
print(f"Password : {'****' if config['password'] else '未配置'}")
print(f"配置路径 : {config['config_path']}")
else:
print("用法: alphapai config --show 或 --set-key KEY 或 --set-account PHONE --set-password PWD")
def cmd_qa(args):
"""投研问答(官方API)"""
config = load_config()
if not config["api_key"]:
print("错误: 未配置 API Key,请运行: alphapai config --set-key YOUR_KEY")
return
from alphapai_client import AlphaPaiClient
client = AlphaPaiClient({"api_key": config["api_key"], "base_url": config["base_url"]})
mode = "Think" if args.think else "Flash"
result = client.qa_text(
question=args.question,
mode=mode,
context=args.context or None,
web_search=args.web_search,
deep_reasoning=args.deep_reasoning,
start_date=args.start,
end_date=args.end,
)
if result.get("answer"):
print(result["answer"])
else:
print("未获取到回答")
def cmd_recall(args):
"""数据召回(官方API)"""
config = load_config()
if not config["api_key"]:
print("错误: 未配置 API Key")
return
from alphapai_client import AlphaPaiClient
client = AlphaPaiClient({"api_key": config["api_key"], "base_url": config["base_url"]})
recall_types = args.type.split(",") if args.type else None
result = client.recall_data(
query=args.query,
recall_type=recall_types,
cutoff=not args.no_cutoff,
start_time=args.start,
end_time=args.end,
)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
for item in result.get("data", []):
print(f"\n### {item.get('type', 'unknown')} | {item.get('contextInfo', '')[:50]}...")
for chunk in item.get("chunks", [])[:3]:
print(f" - {chunk[:200]}...")
def cmd_agent(args):
"""股票Agent(官方API)"""
config = load_config()
if not config["api_key"]:
print("错误: 未配置 API Key")
return
from alphapai_client import AlphaPaiClient
client = AlphaPaiClient({"api_key": config["api_key"], "base_url": config["base_url"]})
# 解析股票参数
stock = None
if args.stock:
parts = args.stock.split(":")
if len(parts) == 2:
stock = {"code": parts[0], "name": parts[1]}
else:
stock = {"code": parts[0], "name": parts[0]}
# 解析股票列表
stock_list = None
if getattr(args, 'stock_list', None):
stock_list = []
for s in args.stock_list:
parts = s.split(":")
if len(parts) == 2:
stock_list.append({"code": parts[0], "name": parts[1]})
else:
stock_list.append({"code": parts[0], "name": parts[0]})
result = client.stock_agent(
question=args.question,
agent_mode=args.mode,
stock=stock,
input_industry=getattr(args, 'input_industry', None),
stock_list=stock_list,
report_date=getattr(args, 'report_date', None),
fund_type=getattr(args, 'fund_type', None),
template_text=getattr(args, 'template_text', None) or "",
template_concern=getattr(args, 'concern', None),
report_type=getattr(args, 'report_type', None),
stock_report_id=getattr(args, 'report_id', None),
stock_report_title=getattr(args, 'report_title', None),
stock_report_period=getattr(args, 'report_period', None),
picture_color=getattr(args, 'picture_color', None),
picture_style=getattr(args, 'picture_style', None),
)
if result.get("answer"):
print(result["answer"])
def cmd_report(args):
"""公告列表(官方API)"""
config = load_config()
if not config["api_key"]:
print("错误: 未配置 API Key")
return
from alphapai_client import AlphaPaiClient
client = AlphaPaiClient({"api_key": config["api_key"], "base_url": config["base_url"]})
result = client.get_stock_reports(args.code)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
for r in result.get("data", []):
print(f"{r.get('reportPeriod', ''):15} | {r.get('reportType', ''):6} | {r.get('stockReportTitle', '')[:50]}")
def cmd_image(args):
"""搜图表(官方API)"""
config = load_config()
if not config["api_key"]:
print("错误: 未配置 API Key")
return
from alphapai_client import AlphaPaiClient
client = AlphaPaiClient({"api_key": config["api_key"], "base_url": config["base_url"]})
files_range = args.files_range.split(",") if args.files_range else None
result = client.search_image(
query=args.query,
files_range=files_range,
topk=args.topk,
start_date=args.start,
end_date=args.end,
)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
for img in result.get("data", [])[:10]:
print(f"{img.get('articleTitle', '')[:40]} | {img.get('imageUrl', '')[:60]}")
# ========== Web版命令 ==========
def _get_web_client():
"""获取Web客户端"""
config = load_config()
if not config["account"] or not config["password"]:
print("错误: 未配置账号密码,请运行: alphapai config --set-account PHONE --set-password PWD")
return None
from alphapai_web_client import AlphaPaiWebClient
return AlphaPaiWebClient(config["account"], config["password"])
def cmd_roadshow(args):
"""路演纪要(Web版)"""
client = _get_web_client()
if not client:
return
if args.detail:
result = client.get_roadshow_detail(args.detail)
if result:
print(f"\n# {result.get('title', '')}")
if result.get("aiSummaryV3"):
print(result["aiSummaryV3"].get("fullTextSummary", ""))
elif args.search:
result = client.search_roadshow(args.search, page=args.page, page_size=args.page_size)
for r in result.get("list", []):
print(f"{r.get('roadshowDate', ''):12} | {r.get('title', '')[:60]}")
else:
result = client.get_roadshow_list(page=args.page, page_size=args.page_size)
for r in result.get("list", []):
print(f"{r.get('roadshowDate', ''):12} | {r.get('title', '')[:60]}")
def cmd_comment(args):
"""点评(Web版)"""
client = _get_web_client()
if not client:
return
if args.detail:
result = client.get_comment_detail(args.detail)
if result:
print(f"\n# {result.get('title', '')}")
print(result.get("content", "")[:2000])
elif args.search:
result = client.search_comment(args.search, page=args.page, page_size=args.page_size)
for r in result.get("list", []):
print(f"{r.get('time', ''):12} | {r.get('title', '')[:60]}")
else:
result = client.get_comment_recommend()
for r in result.get("list", []):
print(f"{r.get('time', ''):12} | {r.get('title', '')[:60]}")
def cmd_hot(args):
"""热门话题(Web版)"""
client = _get_web_client()
if not client:
return
if args.detail:
result = client.get_hot_topic_detail(args.detail)
if result:
print(f"\n# {result.get('topicName', '')}")
print(result.get("summary", "") or "")
else:
result = client.get_hot_topics()
for t in result or []:
new_flag = "🆕" if t.get("newFlag") else " "
name = (t.get("topicName") or "")[:30]
summary = (t.get("summary") or "")[:50]
print(f"{new_flag} {name} | {summary}")
def cmd_hot_word(args):
"""热词(Web版)"""
client = _get_web_client()
if not client:
return
result = client.get_hot_words()
for i, w in enumerate(result or [], 1):
# w 可能是字符串或字典
if isinstance(w, dict):
print(f"{i:2}. {w.get('word', w)}")
else:
print(f"{i:2}. {w}")
def cmd_hot_report(args):
"""热门报告(Web版)"""
client = _get_web_client()
if not client:
return
from html.parser import HTMLParser
def _strip(html: str) -> str:
class _S(HTMLParser):
def __init__(self):
super().__init__()
self.parts = []
def handle_data(self, d):
self.parts.append(d)
s = _S()
s.feed(html or "")
return "".join(s.parts)
result = client.get_hot_report()
if result:
print(f"# {result.get('title', '')}")
print(f"日期: {result.get('date', '')}")
print()
print(_strip(result.get("content", "")))
def cmd_hot_stock(args):
"""热门股票(Web版)"""
client = _get_web_client()
if not client:
return
result = client.get_hot_stocks()
if not result:
print("无数据")
return
print(f"更新时间: {result.get('updateTime', '')}\n")
for key, label in [("publicList", "公募"), ("privateList", "私募"), ("insuranceList", "险资")]:
items = result.get(key, [])
if items:
print(f"## {label} ({len(items)} 只)")
for item in items[:20]:
if isinstance(item, dict):
name = item.get("stockName", item.get("name", ""))
code = item.get("stockCode", item.get("code", ""))
print(f" - {name} ({code})")
print()
def cmd_search(args):
"""综合搜索(Web版)"""
client = _get_web_client()
if not client:
return
result = client.smart_search(args.keyword)
if not result:
print("无结果")
return
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
return
# 路演纪要
rs = result.get("roadshowSummary", {})
if rs and rs.get("list"):
print(f"## 路演纪要 (共 {rs.get('total', '?')} 条)")
for item in rs["list"][:5]:
print(f" - {item.get('title', '')[:60]} | {item.get('publishInstitution', '')}")
print()
# 股票
stock = result.get("stock", {})
if stock and stock.get("list"):
print(f"## 相关股票")
for item in stock["list"][:5]:
if isinstance(item, dict):
print(f" - {item.get('name', '')} ({item.get('code', '')})")
print()
def cmd_user_info(args):
"""用户信息(Web版)"""
client = _get_web_client()
if not client:
return
result = client.get_user_info()
print(json.dumps(result, ensure_ascii=False, indent=2))
# ============================================================
# 快捷命令
# ============================================================
def cmd_company(args):
"""公司一页纸(快捷命令)"""
args.mode = 2
args.question = f"公司一页纸: {args.code}"
args.stock = args.code
cmd_agent(args)
def cmd_earnings(args):
"""业绩点评(快捷命令)"""
config = load_config()
if not config["api_key"]:
print("错误: 未配置 API Key")
return
from alphapai_client import AlphaPaiClient
client = AlphaPaiClient({"api_key": config["api_key"], "base_url": config["base_url"]})
# 先查公告
code = args.code.split(":")[0] if ":" in args.code else args.code
reports = client.stock_report(code)
if not reports.get("data"):
print("未找到公告")
return
# 取最新一份
latest = reports["data"][0]
print(f"公告: {latest.get('stockReportTitle', '')}")
print(f"期间: {latest.get('reportPeriod', '')}")
print()
# 调用业绩点评
name = args.code.split(":")[1] if ":" in args.code else code
result = client.stock_agent(
question=f"{name}{latest.get('reportPeriod', '')}业绩点评",
agent_mode=1,
stock={"code": code, "name": name},
report_type=latest.get("reportType", ""),
stock_report_id=latest.get("stockReportId", ""),
stock_report_title=latest.get("stockReportTitle", ""),
stock_report_period=latest.get("reportPeriod", ""),
)
if result.get("answer"):
print(result["answer"])
def cmd_logic(args):
"""投资逻辑(快捷命令)"""
args.mode = 7
args.question = f"{args.code} 投资逻辑"
args.stock = args.code
args.input_industry = None
args.stock_list = None
args.report_date = None
args.fund_type = None
args.template_text = None
args.concern = None
args.report_type = None
args.report_id = None
args.report_title = None
args.report_period = None
args.picture_color = None
args.picture_style = None
cmd_agent(args)
def cmd_peers(args):
"""可比公司(快捷命令)"""
args.mode = 8
args.question = f"{args.code} 可比公司"
args.stock = args.code
args.input_industry = None
args.stock_list = None
args.report_date = None
args.fund_type = None
args.template_text = None
args.concern = None
args.report_type = None
args.report_id = None
args.report_title = None
args.report_period = None
args.picture_color = None
args.picture_style = None
cmd_agent(args)
def cmd_industry(args):
"""行业一页纸(快捷命令)"""
args.mode = 11
args.question = f"{args.industry} 行业一页纸"
args.stock = None
args.input_industry = args.industry
args.stock_list = None
args.report_date = None
args.fund_type = None
args.template_text = None
args.concern = None
args.report_type = None
args.report_id = None
args.report_title = None
args.report_period = None
args.picture_color = None
args.picture_style = None
cmd_agent(args)
def cmd_screen(args):
"""主题选股(快捷命令)"""
args.mode = 5
args.question = args.theme
args.stock = None
args.input_industry = None
args.stock_list = None
args.report_date = None
args.fund_type = None
args.template_text = args.theme
args.concern = None
args.report_type = None
args.report_id = None
args.report_title = None
args.report_period = None
args.picture_color = None
args.picture_style = None
cmd_agent(args)
def cmd_challenge(args):
"""观点Challenge(快捷命令)"""
args.mode = 9
args.question = f"Challenge该观点: {args.viewpoint}"
args.stock = None
args.input_industry = None
args.stock_list = None
args.report_date = None
args.fund_type = None
args.template_text = args.viewpoint
args.concern = None
args.report_type = None
args.report_id = None
args.report_title = None
args.report_period = None
args.picture_color = None
args.picture_style = None
cmd_agent(args)
# ============================================================
# 主入口
# ============================================================
def main():
parser = argparse.ArgumentParser(
description="Alpha派统一CLI - 融合官方API和Web版功能",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
subparsers = parser.add_subparsers(dest="command", help="命令")
# config
p_config = subparsers.add_parser("config", help="配置管理")
p_config.add_argument("--show", action="store_true", help="显示配置")
p_config.add_argument("--set-key", metavar="KEY", help="设置API Key")
p_config.add_argument("--set-account", metavar="PHONE", help="设置账号")
p_config.add_argument("--set-password", metavar="PWD", help="设置密码")
p_config.set_defaults(func=cmd_config)
# qa
p_qa = subparsers.add_parser("qa", help="投研问答")
p_qa.add_argument("question", help="问题")
p_qa.add_argument("--think", action="store_true", help="Think模式")
p_qa.add_argument("--web-search", action="store_true", help="联网搜索")
p_qa.add_argument("--deep-reasoning", action="store_true", help="深度推理")
p_qa.add_argument("--context", nargs="+", help="多轮对话历史")
p_qa.add_argument("--start", help="开始日期")
p_qa.add_argument("--end", help="结束日期")
p_qa.set_defaults(func=cmd_qa)
# recall
p_recall = subparsers.add_parser("recall", help="数据召回")
p_recall.add_argument("query", help="查询")
p_recall.add_argument("--type", help="数据类型(逗号分隔)")
p_recall.add_argument("--no-cutoff", action="store_true", help="返回完整内容")
p_recall.add_argument("--start", help="开始日期")
p_recall.add_argument("--end", help="结束日期")
p_recall.add_argument("--json", action="store_true", help="输出JSON")
p_recall.set_defaults(func=cmd_recall)
# agent
p_agent = subparsers.add_parser("agent", help="股票Agent")
p_agent.add_argument("--mode", type=int, required=True, help="模式(1-15)")
p_agent.add_argument("--question", help="问题")
p_agent.add_argument("--stock", help="股票 CODE:NAME")
p_agent.add_argument("--industry", help="行业")
p_agent.add_argument("--stock-list", nargs="+", help="股票列表")
p_agent.add_argument("--report-date", help="报告日期")
p_agent.add_argument("--fund-type", help="基金类型")
p_agent.add_argument("--template-text", help="模板文本")
p_agent.add_argument("--concern", help="关注点")
p_agent.add_argument("--report-type", help="报告类型")
p_agent.add_argument("--report-id", help="公告ID")
p_agent.add_argument("--report-title", help="公告标题")
p_agent.add_argument("--report-period", help="报告期")
p_agent.add_argument("--picture-color", nargs=2, help="颜色HEX")
p_agent.add_argument("--picture-style", help="画图风格")
p_agent.set_defaults(func=cmd_agent)
# report
p_report = subparsers.add_parser("report", help="公告列表")
p_report.add_argument("code", help="股票代码")
p_report.add_argument("--json", action="store_true", help="输出JSON")
p_report.set_defaults(func=cmd_report)
# image / chart
p_image = subparsers.add_parser("image", aliases=["chart"], help="搜图表")
p_image.add_argument("query", help="查询")
p_image.add_argument("--files-range", help="来源类型")
p_image.add_argument("--topk", type=int, default=10, help="返回数量")
p_image.add_argument("--start", help="开始日期")
p_image.add_argument("--end", help="结束日期")
p_image.add_argument("--json", action="store_true", help="输出JSON")
p_image.set_defaults(func=cmd_image)
# roadshow
p_roadshow = subparsers.add_parser("roadshow", help="路演纪要")
p_roadshow.add_argument("--list", action="store_true", help="列表")
p_roadshow.add_argument("--search", help="搜索关键词")
p_roadshow.add_argument("--detail", help="纪要ID")
p_roadshow.add_argument("--page", type=int, default=1)
p_roadshow.add_argument("--page-size", type=int, default=20)
p_roadshow.set_defaults(func=cmd_roadshow)
# comment
p_comment = subparsers.add_parser("comment", help="点评")
p_comment.add_argument("--list", action="store_true", help="推荐列表")
p_comment.add_argument("--search", help="搜索关键词")
p_comment.add_argument("--detail", help="点评ID")
p_comment.add_argument("--page", type=int, default=1)
p_comment.add_argument("--page-size", type=int, default=20)
p_comment.set_defaults(func=cmd_comment)
# hot
p_hot = subparsers.add_parser("hot", help="热门话题")
p_hot.add_argument("--detail", help="话题ID")
p_hot.set_defaults(func=cmd_hot)
# hot-word
p_hw = subparsers.add_parser("hot-word", help="热词")
p_hw.set_defaults(func=cmd_hot_word)
# hot-report
p_hr = subparsers.add_parser("hot-report", help="热门报告")
p_hr.set_defaults(func=cmd_hot_report)
# hot-stock
p_hs = subparsers.add_parser("hot-stock", help="热门股票(机构持仓)")
p_hs.set_defaults(func=cmd_hot_stock)
# search (综合搜索)
p_search = subparsers.add_parser("search", help="综合搜索")
p_search.add_argument("keyword", help="搜索关键词")
p_search.add_argument("--json", action="store_true", help="输出JSON")
p_search.set_defaults(func=cmd_search)
# user-info
p_ui = subparsers.add_parser("user-info", help="用户信息")
p_ui.set_defaults(func=cmd_user_info)
# 快捷命令
p_company = subparsers.add_parser("company", help="公司一页纸")
p_company.add_argument("code", help="股票 CODE:NAME")
p_company.set_defaults(func=cmd_company)
p_earnings = subparsers.add_parser("earnings", help="业绩点评")
p_earnings.add_argument("code", help="股票 CODE:NAME")
p_earnings.set_defaults(func=cmd_earnings)
p_logic = subparsers.add_parser("logic", help="投资逻辑")
p_logic.add_argument("code", help="股票 CODE:NAME")
p_logic.set_defaults(func=cmd_logic)
p_peers = subparsers.add_parser("peers", help="可比公司")
p_peers.add_argument("code", help="股票 CODE:NAME")
p_peers.set_defaults(func=cmd_peers)
p_ind = subparsers.add_parser("industry", help="行业一页纸")
p_ind.add_argument("industry", help="行业名称")
p_ind.set_defaults(func=cmd_industry)
p_screen = subparsers.add_parser("screen", help="主题选股")
p_screen.add_argument("theme", help="选股主题")
p_screen.set_defaults(func=cmd_screen)
p_challenge = subparsers.add_parser("challenge", help="观点Challenge")
p_challenge.add_argument("viewpoint", help="待挑战观点")
p_challenge.set_defaults(func=cmd_challenge)
# 解析
args = parser.parse_args()
if args.command is None:
parser.print_help()
return
# 启动时检查配置完整性,给出明确提示
if args.command not in ("config",):
config = load_config()
has_api = bool(config.get("api_key"))
has_web = bool(config.get("account") and config.get("password"))
if not has_api and not has_web:
print("警告: 未配置任何认证信息,所有命令将无法使用。", file=sys.stderr)
print(" 官方API: alphapai config --set-key YOUR_KEY", file=sys.stderr)
print(" Web版: alphapai config --set-account PHONE --set-password PWD", file=sys.stderr)
print(" 或设置环境变量: ALPHAPAI_API_KEY / ALPHAPAI_ACCOUNT + ALPHAPAI_PASSWORD", file=sys.stderr)
sys.exit(1)
# 针对 API 类命令但缺 API Key 的情况提前警告
api_commands = {"qa", "recall", "agent", "report", "image", "chart",
"company", "earnings", "logic", "peers", "industry", "screen", "challenge"}
web_commands = {"roadshow", "comment", "hot", "hot-word", "hot-report",
"hot-stock", "search", "user-info"}
if args.command in api_commands and not has_api:
print(f"错误: 命令 '{args.command}' 需要官方 API Key,但未配置。", file=sys.stderr)
print(" 运行: alphapai config --set-key YOUR_KEY", file=sys.stderr)
sys.exit(1)
if args.command in web_commands and not has_web:
print(f"错误: 命令 '{args.command}' 需要账号密码,但未配置。", file=sys.stderr)
print(" 运行: alphapai config --set-account PHONE --set-password PWD", file=sys.stderr)
sys.exit(1)
if hasattr(args, "func"):
args.func(args)
else:
parser.print_help()
if __name__ == "__main__":
main()#!/bin/bash
set -e
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
echo "[alphapai-unified] Checking dependencies..."
# 检查 python3
if ! command -v python3 &> /dev/null; then
echo "[alphapai-unified] ERROR: python3 not found"
exit 1
fi
# 检查 requests 模块
python3 -c "import requests" 2>/dev/null || {
echo "[alphapai-unified] Installing requests..."
pip install requests --quiet
}
# 检查认证配置
has_auth=0
# 检查 API Key
if [ -n "$ALPHAPAI_API_KEY" ]; then
echo "[alphapai-unified] API Key found in environment"
has_auth=1
else
# 检查 .env 文件
for d in "$SKILL_DIR" "$(dirname "$SKILL_DIR")" "$(dirname "$(dirname "$SKILL_DIR")")"; do
if [ -f "$d/.env" ]; then
KEY=$(grep -E '^ALPHAPAI_API_KEY=' "$d/.env" 2>/dev/null | cut -d= -f2- || true)
if [ -n "$KEY" ]; then
echo "[alphapai-unified] API Key found in $d/.env"
has_auth=1
break
fi
fi
done
fi
# 检查账号密码
if [ -n "$ALPHAPAI_ACCOUNT" ] && [ -n "$ALPHAPAI_PASSWORD" ]; then
echo "[alphapai-unified] Account credentials found in environment"
has_auth=1
fi
if [ $has_auth -eq 0 ]; then
echo "[alphapai-unified] WARNING: No authentication configured"
echo "[alphapai-unified] Set ALPHAPAI_API_KEY for official API features"
echo "[alphapai-unified] Set ALPHAPAI_ACCOUNT/ALPHAPAI_PASSWORD for web features"
fi
echo "[alphapai-unified] Setup complete."