上传文件至 gp

This commit is contained in:
lj091715 2026-05-19 09:56:18 +08:00
parent aa989cd856
commit 2cf3c6b793
2 changed files with 157 additions and 107 deletions

View File

@ -1,74 +1,118 @@
--- ---
name: stock name: stock
description: "当用户发送「股票 代码」或「gp 代码」时触发。调用腾讯股票行情接口获取实时数据,再调用本地微信机器人发消息接口把股票信息发给当前用户。" description: "当用户发送「股票 代码」或「gp 代码」时触发。调用 mairuiapi 实时交易数据接口获取行情,再调用本地微信机器人发消息接口把股票信息发给当前用户。"
argument-hint: "参数为股票代码,例如 600519 或 sz000001" argument-hint: "参数为股票代码,例如 600519 或 000001"
--- ---
# Stock Skill # Stock Skill
## 描述 ## 描述
这是一个用于查询股票实时行情并直接发送给当前用户的技能。 这是一个用于查询股票实时行情并直接发送给当前用户的技能。
当用户发送 `股票 600519``gp sz000001` 时,调用腾讯股票行情接口获取实时数据,再调用本地微信机器人接口把格式化后的股票信息发出去。 当用户发送 `股票 600519``gp 000001` 时,调用 mairuiapi 实时交易数据接口获取行情数据,再调用本地微信机器人接口把格式化后的股票信息发出去。
这个仓库里额外提供了一个可执行脚本 `scripts/stock.py`,方便宿主机器人直接调用。 这个仓库里额外提供了一个可执行脚本 `scripts/stock.py`,方便宿主机器人直接调用。
## 触发条件 ## 触发条件
- 用户发送 `股票 <股票代码>``gp <股票代码>` - 用户发送 `股票 <股票代码>``gp <股票代码>`
- 股票代码可以是纯数字(如 `600519`),也可以带市场前缀(如 `sh600519`、`sz000001` - 股票代码为纯数字(如 `600519`、`000001`
## 接口信息 ## 接口信息
- 获取股票行情:`https://qt.gtimg.cn/q={stock_code}` - 获取股票行情:`https://api.mairuiapi.com/hsrl/ssjy/{stock_code}/{licence}`
- 请求方式:`GET` - 请求方式:`GET`
- 发消息接口:`http://127.0.0.1:{ROBOT_WECHAT_CLIENT_PORT}/api/v1/robot/message/send/text` - 发消息接口:`http://127.0.0.1:{ROBOT_WECHAT_CLIENT_PORT}/api/v1/robot/message/send/text`
- 请求方式:`POST` - 请求方式:`POST`
- 本地脚本:`scripts/stock.py` - 本地脚本:`scripts/stock.py`
### 腾讯股票行情返回示例 ### API 返回示例(贵州茅台 600519
``` ```json
v_sh600519="1~贵州茅台~600519~1945.50~1942.00~1941.00~1946.80~1940.02~1945.71~4291131~8365444900~183~1945.60~826~1945.59~649~1945.58~699~1945.57~589~1945.56~1449~1945.71~1014~1945.72~722~1945.73~369~1945.74~471~1945.75~202~1945.76~2026-05-19~15:00:00~00~-1.18~-0.06~1945.50~1906.02~1945.50~1895216~4901415~1.22~836544.4900~18.55~403727.15~0.37~107.86~15.03~232.10~10.49~10.61~PB~0.23~0.53~-0.22~10.99~1945.50~1900.16~~ 1044479.27~1.22~1895216~4901415~0.06~10.47~10.49~10.61~0.01~-82.56~1945.50~1906.02~0.03~-0.06~1945.50~-0.39~1942.00~1036.50~1.88~181580~185384~1945.50~1900.16~0.00~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~" [{
"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
}]
``` ```
### 字段说明 ### 字段说明
| 索引 | 含义 | | 字段 | 数据类型 | 字段说明 |
|------|------| |------|---------|---------|
| 1 | 股票名称 | | fm | number | 五分钟涨跌幅(% |
| 2 | 股票代码 | | h | number | 最高价(元) |
| 3 | 当前价格 | | hs | number | 换手(% |
| 4 | 昨收价 | | lb | number | 量比(% |
| 5 | 今开价 | | l | number | 最低价(元) |
| 6 | 成交量(手) | | lt | number | 流通市值(元) |
| 7 | 成交额(万) | | o | number | 开盘价(元) |
| 8 | 最高价 | | pe | number | 市盈率动态总市值除以预估全年净利润例如当前公布一季度净利润1000万则预估全年净利润4000万 |
| 9 | 最低价 | | pc | number | 涨跌幅(% |
| 10 | 涨跌额 | | p | number | 当前价格(元) |
| 11 | 涨跌幅(%) | | 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_WECHAT_CLIENT_PORT`:本地微信机器人服务端口。
- `ROBOT_FROM_WX_ID`:当前消息来源用户的 wxid。 - `ROBOT_FROM_WX_ID`:当前消息来源用户的 wxid。
- `STOCK_LICENCE`mairuiapi 的 licence 密钥(可选,默认使用内置 licence
## 执行步骤 ## 执行步骤
1. 当用户发送 `股票 <代码>``gp <代码>` 时触发该技能。 1. 当用户发送 `股票 <代码>``gp <代码>` 时触发该技能。
2. 在仓库根目录下执行本地脚本:`python3 scripts/stock.py <股票代码>`。 2. 在仓库根目录下执行本地脚本:`python3 scripts/stock.py <股票代码>`。
3. 脚本内部自动处理股票代码格式(补全市场前缀 sh/sz 3. 脚本从环境变量 `STOCK_LICENCE` 获取 licence如果未设置则使用内置默认值
4. 脚本发送 `GET` 请求到 `https://qt.gtimg.cn/q={stock_code}` 4. 脚本发送 `GET` 请求到 `https://api.mairuiapi.com/hsrl/ssjy/{stock_code}/{licence}`。
5. 脚本解析返回的行情数据,提取关键字段。 5. 脚本解析返回的 JSON 数据,提取关键字段。
6. 脚本从环境变量中读取 `ROBOT_WECHAT_CLIENT_PORT``ROBOT_FROM_WX_ID` 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`,请求体为: 7. 脚本发送 `POST` 请求到 `http://127.0.0.1:{ROBOT_WECHAT_CLIENT_PORT}/api/v1/robot/message/send/text`,请求体为:
```json ```json
{ {
"to_wxid": "{ROBOT_FROM_WX_ID}", "to_wxid": "{ROBOT_FROM_WX_ID}",
"content": "【{股票名称}({股票代码})】 "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. 如果任一步骤失败,回复兜底文案:`股票查询失败,请稍后再试。` 8. 如果任一步骤失败,回复兜底文案:`股票查询失败,请稍后再试。`

View File

@ -16,98 +16,102 @@ else:
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = sys.stdout sys.stderr = sys.stdout
QUOTE_API_URL = "https://qt.gtimg.cn/q={}" QUOTE_API_URL = "https://api.mairuiapi.com/hsrl/ssjy/{}/{}"
DEFAULT_LICENCE = "3E81CB37-4BCE-4DAF-B0AC-AEA069A58973"
FALLBACK_TEXT = "股票查询失败,请稍后再试。" FALLBACK_TEXT = "股票查询失败,请稍后再试。"
def normalize_stock_code(code: str) -> str: def normalize_stock_code(code: str) -> str:
"""自动补全股票代码市场前缀""" """验证并标准化股票代码仅保留6位纯数字"""
code = code.strip().upper() code = code.strip()
# 如果已经带前缀,直接返回 # 去掉可能的前缀sh/SH/sz/SZ/bj/BJ
if code.startswith("SH") or code.startswith("SZ"): code = re.sub(r"^(sh|sz|bj)", "", code, flags=re.IGNORECASE)
return code
# 如果已经是带 sh/sz 前缀
if re.match(r"^(sh|sz)\d{6}$", code, re.IGNORECASE):
return code.upper()
# 纯数字处理
if re.match(r"^\d{6}$", code): if re.match(r"^\d{6}$", code):
# 沪市6开头 return code
if code.startswith("6"): # 如果传入了完整代码但包含其他字符,尝试提取数字
return f"SH{code}" digits = re.findall(r"\d", code)
# 深市0或3开头 if len(digits) >= 6:
elif code.startswith(("0", "3")): return "".join(digits[:6])
return f"SZ{code}" return ""
# 北交所8开头
elif code.startswith(("8", "4")):
return f"BJ{code}"
# 无法识别
return code
def parse_market_prefix(code: str) -> str: def get_licence() -> str:
"""将 SH/SZ 转换为 qq 接口使用的 sh/sz 前缀""" """获取 licence优先从环境变量读取"""
code = code.upper() return os.environ.get("STOCK_LICENCE", "").strip() or DEFAULT_LICENCE
if code.startswith("SH"):
return f"sh{code[2:]}"
elif code.startswith("SZ"):
return f"sz{code[2:]}"
elif code.startswith("BJ"):
return f"bj{code[2:]}"
return code
def fetch_stock_quote(stock_code: str) -> dict | None: def fetch_stock_quote(stock_code: str) -> dict | None:
"""获取股票行情数据并解析为字典""" """获取股票行情数据并解析为字典"""
licence = get_licence()
url = QUOTE_API_URL.format(stock_code, licence)
try: try:
url = QUOTE_API_URL.format(stock_code) req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(url, timeout=10) as response: with urllib.request.urlopen(req, timeout=10) as response:
raw = response.read().decode("gbk") payload = json.load(response)
except (urllib.error.URLError, TimeoutError, OSError): except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError):
return None return None
# 解析返回数据格式如v_sh600519="..."; # 处理 JSON 数组返回格式 [{}, ...]
match = re.search(r'="([^"]+)"', raw) if isinstance(payload, list) and len(payload) > 0:
if not match: payload = payload[0]
if not isinstance(payload, dict):
return None return None
fields = match.group(1).split("~") # 检查接口返回是否包含有效数据
if len(fields) < 45: if "p" not in payload:
return None return None
return { return payload
"name": fields[1], # 股票名称
"code": fields[2], # 股票代码
"price": fields[3], # 当前价格
"yest_close": fields[4], # 昨收价
"open": fields[5], # 今开价
"volume": fields[6], # 成交量(手)
"amount": fields[37], # 成交额(万)
"high": fields[33], # 最高价
"low": fields[34], # 最低价
"change": fields[31], # 涨跌额
"change_pct": fields[32], # 涨跌幅(%)
}
def format_stock_message(data: dict) -> str: def format_stock_message(data: dict, stock_code: str = "") -> str:
"""格式化股票信息为可读文本""" """格式化股票信息为可读文本"""
up_icon = "📈" if float(data.get("change", 0)) >= 0 else "📉" 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 = [ lines = [
f"{data['name']}({data['code']})】", title,
f"{'=' * 20}", f"{'=' * 20}",
f"{up_icon} 当前价:{data['price']}", f"{up_icon} 当前价:{_fmt(p)}",
f"📊 涨跌幅:{data['change_pct']}%", f"📊 涨跌幅:{_fmt(pc)}%",
f"📊 涨跌额:{data['change']}", f"📊 涨跌额:{_fmt(ud)}",
f"⬆️ 最高:{data['high']}", f"⬆️ 最高:{_fmt(data.get('h'))}",
f"⬇️ 最低:{data['low']}", f"⬇️ 最低:{_fmt(data.get('l'))}",
f"🔓 今开:{data['open']}", f"🔓 今开:{_fmt(data.get('o'))}",
f"🔒 昨收:{data['yest_close']}", f"🔒 昨收:{_fmt(data.get('yc'))}",
f"📈 成交量:{data['volume']}", f"📈 成交量:{_fmt(data.get('v'))}",
f"💰 成交额:{data['amount']}", 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) 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: def send_text(text: str) -> bool:
"""发送文本消息到微信机器人""" """发送文本消息到微信机器人"""
robot_port = os.environ.get("ROBOT_WECHAT_CLIENT_PORT", "").strip() robot_port = os.environ.get("ROBOT_WECHAT_CLIENT_PORT", "").strip()
@ -151,17 +155,19 @@ def main() -> int:
return 0 return 0
raw_code = args[0] raw_code = args[0]
normalized = normalize_stock_code(raw_code) stock_code = normalize_stock_code(raw_code)
quoted_code = parse_market_prefix(normalized) if not stock_code:
sys.stdout.write(FALLBACK_TEXT)
sys.stdout.write("\n")
return 0
# 在QQ接口中多个股票用逗号分隔 data = fetch_stock_quote(stock_code)
data = fetch_stock_quote(quoted_code)
if not data: if not data:
sys.stdout.write(FALLBACK_TEXT) sys.stdout.write(FALLBACK_TEXT)
sys.stdout.write("\n") sys.stdout.write("\n")
return 0 return 0
message = format_stock_message(data) message = format_stock_message(data, stock_code)
if send_text(message): if send_text(message):
return 0 return 0