上传文件至 gpdm
This commit is contained in:
parent
2035e0e7f4
commit
ac5184ecb0
122
gpdm/SKILL.md
Normal file
122
gpdm/SKILL.md
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
---
|
||||||
|
name: stock
|
||||||
|
description: "当用户发送「股票 代码」或「gp 代码」时触发。调用 mairuiapi 实时交易数据接口获取行情,再调用本地微信机器人发消息接口把股票信息发给当前用户。"
|
||||||
|
argument-hint: "参数为股票代码,例如 600519 或 000001"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Stock Skill
|
||||||
|
|
||||||
|
## 描述
|
||||||
|
这是一个用于查询股票实时行情并直接发送给当前用户的技能。
|
||||||
|
|
||||||
|
当用户发送 `股票 600519` 或 `gp 000001` 时,调用 mairuiapi 实时交易数据接口获取行情数据,再调用本地微信机器人接口把格式化后的股票信息发出去。
|
||||||
|
|
||||||
|
这个仓库里额外提供了一个可执行脚本 `scripts/stock.py`,方便宿主机器人直接调用。
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
- 用户发送 `股票 <股票代码>` 或 `gp <股票代码>`
|
||||||
|
- 股票代码为纯数字(如 `600519`、`000001`)
|
||||||
|
|
||||||
|
## 接口信息
|
||||||
|
- 获取股票行情:`https://api.mairuiapi.com/hsrl/ssjy/{stock_code}/{licence}`
|
||||||
|
- 请求方式:`GET`
|
||||||
|
- 发消息接口:`http://127.0.0.1:{ROBOT_WECHAT_CLIENT_PORT}/api/v1/robot/message/send/text`
|
||||||
|
- 请求方式:`POST`
|
||||||
|
- 本地脚本:`scripts/stock.py`
|
||||||
|
|
||||||
|
### API 返回示例(贵州茅台 600519)
|
||||||
|
```json
|
||||||
|
[{
|
||||||
|
"t": "2026-05-19 15:00:00",
|
||||||
|
"p": 1323.0,
|
||||||
|
"pc": -0.75,
|
||||||
|
"ud": -9.95,
|
||||||
|
"v": 4.97,
|
||||||
|
"cje": 6594983723.0,
|
||||||
|
"zf": 1.73,
|
||||||
|
"hs": 0.4,
|
||||||
|
"pe": 15.2,
|
||||||
|
"lb": 0.89,
|
||||||
|
"fm": 0.18,
|
||||||
|
"h": 1342.68,
|
||||||
|
"l": 1319.61,
|
||||||
|
"o": 1336.0,
|
||||||
|
"yc": 1332.95,
|
||||||
|
"sz": 16567534944.45,
|
||||||
|
"lt": 16567534944.45,
|
||||||
|
"zs": 0.04,
|
||||||
|
"sjl": 6.12,
|
||||||
|
"zdf60": -13.24,
|
||||||
|
"zdfnc": -3.93
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 字段说明
|
||||||
|
| 字段 | 数据类型 | 字段说明 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| fm | number | 五分钟涨跌幅(%) |
|
||||||
|
| h | number | 最高价(元) |
|
||||||
|
| hs | number | 换手(%) |
|
||||||
|
| lb | number | 量比(%) |
|
||||||
|
| l | number | 最低价(元) |
|
||||||
|
| lt | number | 流通市值(元) |
|
||||||
|
| o | number | 开盘价(元) |
|
||||||
|
| pe | number | 市盈率(动态,总市值除以预估全年净利润,例如当前公布一季度净利润1000万,则预估全年净利润4000万) |
|
||||||
|
| pc | number | 涨跌幅(%) |
|
||||||
|
| p | number | 当前价格(元) |
|
||||||
|
| sz | number | 总市值(元) |
|
||||||
|
| cje | number | 成交额(元) |
|
||||||
|
| ud | number | 涨跌额(元) |
|
||||||
|
| v | number | 成交量(手) |
|
||||||
|
| yc | number | 昨日收盘价(元) |
|
||||||
|
| zf | number | 振幅(%) |
|
||||||
|
| zs | number | 涨速(%) |
|
||||||
|
| sjl | number | 市净率 |
|
||||||
|
| zdf60 | number | 60日涨跌幅(%) |
|
||||||
|
| zdfnc | number | 年初至今涨跌幅(%) |
|
||||||
|
| t | string | 更新时间 yyyy-MM-ddHH:mm:ss |
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
- `ROBOT_WECHAT_CLIENT_PORT`:本地微信机器人服务端口。
|
||||||
|
- `ROBOT_FROM_WX_ID`:当前消息来源用户的 wxid。
|
||||||
|
- `STOCK_LICENCE`:mairuiapi 的 licence 密钥(可选,默认使用内置 licence)。
|
||||||
|
|
||||||
|
## 执行步骤
|
||||||
|
1. 当用户发送 `股票 <代码>` 或 `gp <代码>` 时触发该技能。
|
||||||
|
2. 在仓库根目录下执行本地脚本:`python3 scripts/stock.py <股票代码>`。
|
||||||
|
3. 脚本从环境变量 `STOCK_LICENCE` 获取 licence,如果未设置则使用内置默认值。
|
||||||
|
4. 脚本发送 `GET` 请求到 `https://api.mairuiapi.com/hsrl/ssjy/{stock_code}/{licence}`。
|
||||||
|
5. 脚本解析返回的 JSON 数据,提取关键字段。
|
||||||
|
6. 脚本从环境变量中读取 `ROBOT_WECHAT_CLIENT_PORT` 和 `ROBOT_FROM_WX_ID`。
|
||||||
|
7. 脚本发送 `POST` 请求到 `http://127.0.0.1:{ROBOT_WECHAT_CLIENT_PORT}/api/v1/robot/message/send/text`,请求体为:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"to_wxid": "{ROBOT_FROM_WX_ID}",
|
||||||
|
"content": "【600519】
|
||||||
|
📈 当前价:1323.00
|
||||||
|
📊 涨跌幅:-0.75%
|
||||||
|
📊 涨跌额:-9.95
|
||||||
|
⬆️ 最高:1342.68
|
||||||
|
⬇️ 最低:1319.61
|
||||||
|
🔓 今开:1336.00
|
||||||
|
🔒 昨收:1332.95
|
||||||
|
📈 成交量:4.97手
|
||||||
|
💰 成交额:6594983723.00
|
||||||
|
🔄 换手率:0.40%
|
||||||
|
📐 振幅:1.73%
|
||||||
|
📊 市盈率:15.20
|
||||||
|
📊 市净率:6.12
|
||||||
|
📊 量比:0.89
|
||||||
|
⚡ 涨速:0.04%
|
||||||
|
⏱ 五分钟涨跌:0.18%
|
||||||
|
📅 60日涨跌:-13.24%
|
||||||
|
📅 年初至今:-3.93%
|
||||||
|
====================
|
||||||
|
更新时间:2026-05-19 15:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
8. 如果任一步骤失败,回复兜底文案:`股票查询失败,请稍后再试。`
|
||||||
|
|
||||||
|
## 回复要求
|
||||||
|
- 成功时,直接发送格式化股票信息,不要额外追加解释文字。
|
||||||
|
- 失败时,使用固定兜底文案回复。
|
||||||
BIN
gpdm/stock.cpython-312.pyc
Normal file
BIN
gpdm/stock.cpython-312.pyc
Normal file
Binary file not shown.
187
gpdm/stock.py
Normal file
187
gpdm/stock.py
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
# 将 stdout 编码设置为 UTF-8,确保 emoji 等字符能正常输出
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
else:
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
||||||
|
sys.stderr = sys.stdout
|
||||||
|
|
||||||
|
QUOTE_API_URL = "https://api.mairuiapi.com/hsrl/ssjy/{}/{}"
|
||||||
|
DEFAULT_LICENCE = "3E81CB37-4BCE-4DAF-B0AC-AEA069A58973"
|
||||||
|
FALLBACK_TEXT = "股票查询失败,请稍后再试。"
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_stock_code(code: str) -> str:
|
||||||
|
"""验证并标准化股票代码(仅保留6位纯数字)"""
|
||||||
|
code = code.strip()
|
||||||
|
# 去掉可能的前缀(sh/SH/sz/SZ/bj/BJ)
|
||||||
|
code = re.sub(r"^(sh|sz|bj)", "", code, flags=re.IGNORECASE)
|
||||||
|
if re.match(r"^\d{6}$", code):
|
||||||
|
return code
|
||||||
|
# 如果传入了完整代码但包含其他字符,尝试提取数字
|
||||||
|
digits = re.findall(r"\d", code)
|
||||||
|
if len(digits) >= 6:
|
||||||
|
return "".join(digits[:6])
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_licence() -> str:
|
||||||
|
"""获取 licence,优先从环境变量读取"""
|
||||||
|
return os.environ.get("STOCK_LICENCE", "").strip() or DEFAULT_LICENCE
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_stock_quote(stock_code: str) -> dict | None:
|
||||||
|
"""获取股票行情数据并解析为字典"""
|
||||||
|
licence = get_licence()
|
||||||
|
url = QUOTE_API_URL.format(stock_code, licence)
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as response:
|
||||||
|
payload = json.load(response)
|
||||||
|
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 处理 JSON 数组返回格式 [{}, ...]
|
||||||
|
if isinstance(payload, list) and len(payload) > 0:
|
||||||
|
payload = payload[0]
|
||||||
|
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 检查接口返回是否包含有效数据
|
||||||
|
if "p" not in payload:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def format_stock_message(data: dict, stock_code: str = "") -> str:
|
||||||
|
"""格式化股票信息为可读文本"""
|
||||||
|
p = data.get("p", 0) or 0
|
||||||
|
pc = data.get("pc", 0) or 0
|
||||||
|
ud = data.get("ud", 0) or 0
|
||||||
|
|
||||||
|
up_icon = "📈" if float(pc) >= 0 else "📉"
|
||||||
|
title = f"【{stock_code}】" if stock_code else "【查询结果】"
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
title,
|
||||||
|
f"{'=' * 20}",
|
||||||
|
f"{up_icon} 当前价:{_fmt(p)}",
|
||||||
|
f"📊 涨跌幅:{_fmt(pc)}%",
|
||||||
|
f"📊 涨跌额:{_fmt(ud)}",
|
||||||
|
f"⬆️ 最高:{_fmt(data.get('h'))}",
|
||||||
|
f"⬇️ 最低:{_fmt(data.get('l'))}",
|
||||||
|
f"🔓 今开:{_fmt(data.get('o'))}",
|
||||||
|
f"🔒 昨收:{_fmt(data.get('yc'))}",
|
||||||
|
f"📈 成交量:{_fmt(data.get('v'))}手",
|
||||||
|
f"💰 成交额:{_fmt(data.get('cje'))}",
|
||||||
|
f"🔄 换手率:{_fmt(data.get('hs'))}%",
|
||||||
|
f"📐 振幅:{_fmt(data.get('zf'))}%",
|
||||||
|
f"📊 市盈率:{_fmt(data.get('pe'))}",
|
||||||
|
f"📊 市净率:{_fmt(data.get('sjl'))}",
|
||||||
|
f"📊 量比:{_fmt(data.get('lb'))}",
|
||||||
|
f"⚡ 涨速:{_fmt(data.get('zs'))}%",
|
||||||
|
f"⏱ 五分钟涨跌:{_fmt(data.get('fm'))}%",
|
||||||
|
f"📅 60日涨跌:{_fmt(data.get('zdf60'))}%",
|
||||||
|
f"📅 年初至今:{_fmt(data.get('zdfnc'))}%",
|
||||||
|
f"📊 总市值:{_fmt(data.get('sz'))}",
|
||||||
|
f"📊 流通市值:{_fmt(data.get('lt'))}",
|
||||||
|
f"{'=' * 20}",
|
||||||
|
f"更新时间:{data.get('t', '未知')}",
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt(val) -> str:
|
||||||
|
"""统一格式化数值,金融数据保留2位小数"""
|
||||||
|
if val is None:
|
||||||
|
return "0"
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
return f"{val:.2f}"
|
||||||
|
return str(val)
|
||||||
|
|
||||||
|
|
||||||
|
def send_text(text: str) -> bool:
|
||||||
|
"""发送文本消息到微信机器人"""
|
||||||
|
robot_port = os.environ.get("ROBOT_WECHAT_CLIENT_PORT", "").strip()
|
||||||
|
to_wxid = os.environ.get("ROBOT_FROM_WX_ID", "").strip()
|
||||||
|
if not robot_port or not to_wxid:
|
||||||
|
return False
|
||||||
|
|
||||||
|
api_url = (
|
||||||
|
f"http://127.0.0.1:{robot_port}/api/v1/robot/message/send/text"
|
||||||
|
)
|
||||||
|
body = json.dumps(
|
||||||
|
{
|
||||||
|
"to_wxid": to_wxid,
|
||||||
|
"content": text,
|
||||||
|
}
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
request = urllib.request.Request(
|
||||||
|
api_url,
|
||||||
|
data=body,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=10) as response:
|
||||||
|
if 200 <= response.status < 300:
|
||||||
|
return True
|
||||||
|
payload = json.load(response)
|
||||||
|
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
code = payload.get("code")
|
||||||
|
return code == 200 or code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = sys.argv[1:]
|
||||||
|
if not args:
|
||||||
|
sys.stdout.write("请提供股票代码,例如:python3 stock.py 600519\n")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
raw_code = args[0]
|
||||||
|
stock_code = normalize_stock_code(raw_code)
|
||||||
|
if not stock_code:
|
||||||
|
sys.stdout.write(FALLBACK_TEXT)
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
data = fetch_stock_quote(stock_code)
|
||||||
|
if not data:
|
||||||
|
sys.stdout.write(FALLBACK_TEXT)
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
message = format_stock_message(data, stock_code)
|
||||||
|
if send_text(message):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 发送失败,输出到stdout让宿主机器人捕获
|
||||||
|
sys.stdout.write(message)
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
raise SystemExit(1)
|
||||||
Loading…
Reference in New Issue
Block a user