上传文件至 /
This commit is contained in:
commit
1529293478
78
SKILL.md
Normal file
78
SKILL.md
Normal file
@ -0,0 +1,78 @@
|
||||
---
|
||||
name: stock
|
||||
description: "当用户发送「股票 代码」或「gp 代码」时触发。调用腾讯股票行情接口获取实时数据,再调用本地微信机器人发消息接口把股票信息发给当前用户。"
|
||||
argument-hint: "参数为股票代码,例如 600519 或 sz000001"
|
||||
---
|
||||
|
||||
# Stock Skill
|
||||
|
||||
## 描述
|
||||
这是一个用于查询股票实时行情并直接发送给当前用户的技能。
|
||||
|
||||
当用户发送 `股票 600519` 或 `gp sz000001` 时,调用腾讯股票行情接口获取实时数据,再调用本地微信机器人接口把格式化后的股票信息发出去。
|
||||
|
||||
这个仓库里额外提供了一个可执行脚本 `scripts/stock.py`,方便宿主机器人直接调用。
|
||||
|
||||
## 触发条件
|
||||
- 用户发送 `股票 <股票代码>` 或 `gp <股票代码>`
|
||||
- 股票代码可以是纯数字(如 `600519`),也可以带市场前缀(如 `sh600519`、`sz000001`)
|
||||
|
||||
## 接口信息
|
||||
- 获取股票行情:`https://qt.gtimg.cn/q={stock_code}`
|
||||
- 请求方式:`GET`
|
||||
- 发消息接口:`http://127.0.0.1:{ROBOT_WECHAT_CLIENT_PORT}/api/v1/robot/message/send/text`
|
||||
- 请求方式:`POST`
|
||||
- 本地脚本:`scripts/stock.py`
|
||||
|
||||
### 腾讯股票行情返回示例
|
||||
```
|
||||
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~"
|
||||
```
|
||||
|
||||
### 字段说明
|
||||
| 索引 | 含义 |
|
||||
|------|------|
|
||||
| 1 | 股票名称 |
|
||||
| 2 | 股票代码 |
|
||||
| 3 | 当前价格 |
|
||||
| 4 | 昨收价 |
|
||||
| 5 | 今开价 |
|
||||
| 6 | 成交量(手) |
|
||||
| 7 | 成交额(万) |
|
||||
| 8 | 最高价 |
|
||||
| 9 | 最低价 |
|
||||
| 10 | 涨跌额 |
|
||||
| 11 | 涨跌幅(%) |
|
||||
|
||||
## 环境变量
|
||||
- `ROBOT_WECHAT_CLIENT_PORT`:本地微信机器人服务端口。
|
||||
- `ROBOT_FROM_WX_ID`:当前消息来源用户的 wxid。
|
||||
|
||||
## 执行步骤
|
||||
1. 当用户发送 `股票 <代码>` 或 `gp <代码>` 时触发该技能。
|
||||
2. 在仓库根目录下执行本地脚本:`python3 scripts/stock.py <股票代码>`。
|
||||
3. 脚本内部自动处理股票代码格式(补全市场前缀 sh/sz)。
|
||||
4. 脚本发送 `GET` 请求到 `https://qt.gtimg.cn/q={stock_code}`。
|
||||
5. 脚本解析返回的行情数据,提取关键字段。
|
||||
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": "【{股票名称}({股票代码})】
|
||||
当前价:{当前价格}
|
||||
涨跌幅:{涨跌幅}%
|
||||
涨跌额:{涨跌额}
|
||||
最高:{最高价}
|
||||
最低:{最低价}
|
||||
今开:{今开价}
|
||||
昨收:{昨收价}
|
||||
成交量:{成交量}
|
||||
成交额:{成交额}"
|
||||
}
|
||||
```
|
||||
8. 如果任一步骤失败,回复兜底文案:`股票查询失败,请稍后再试。`
|
||||
|
||||
## 回复要求
|
||||
- 成功时,直接发送格式化股票信息,不要额外追加解释文字。
|
||||
- 失败时,使用固定兜底文案回复。
|
||||
BIN
stock.cpython-312.pyc
Normal file
BIN
stock.cpython-312.pyc
Normal file
Binary file not shown.
181
stock.py
Normal file
181
stock.py
Normal file
@ -0,0 +1,181 @@
|
||||
#!/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://qt.gtimg.cn/q={}"
|
||||
FALLBACK_TEXT = "股票查询失败,请稍后再试。"
|
||||
|
||||
|
||||
def normalize_stock_code(code: str) -> str:
|
||||
"""自动补全股票代码市场前缀"""
|
||||
code = code.strip().upper()
|
||||
# 如果已经带前缀,直接返回
|
||||
if code.startswith("SH") or code.startswith("SZ"):
|
||||
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):
|
||||
# 沪市:6开头
|
||||
if code.startswith("6"):
|
||||
return f"SH{code}"
|
||||
# 深市:0或3开头
|
||||
elif code.startswith(("0", "3")):
|
||||
return f"SZ{code}"
|
||||
# 北交所:8开头
|
||||
elif code.startswith(("8", "4")):
|
||||
return f"BJ{code}"
|
||||
# 无法识别
|
||||
return code
|
||||
|
||||
|
||||
def parse_market_prefix(code: str) -> str:
|
||||
"""将 SH/SZ 转换为 qq 接口使用的 sh/sz 前缀"""
|
||||
code = code.upper()
|
||||
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:
|
||||
"""获取股票行情数据并解析为字典"""
|
||||
try:
|
||||
url = QUOTE_API_URL.format(stock_code)
|
||||
with urllib.request.urlopen(url, timeout=10) as response:
|
||||
raw = response.read().decode("gbk")
|
||||
except (urllib.error.URLError, TimeoutError, OSError):
|
||||
return None
|
||||
|
||||
# 解析返回数据,格式如:v_sh600519="...";
|
||||
match = re.search(r'="([^"]+)"', raw)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
fields = match.group(1).split("~")
|
||||
if len(fields) < 45:
|
||||
return None
|
||||
|
||||
return {
|
||||
"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:
|
||||
"""格式化股票信息为可读文本"""
|
||||
up_icon = "📈" if float(data.get("change", 0)) >= 0 else "📉"
|
||||
lines = [
|
||||
f"【{data['name']}({data['code']})】",
|
||||
f"{'=' * 20}",
|
||||
f"{up_icon} 当前价:{data['price']}",
|
||||
f"📊 涨跌幅:{data['change_pct']}%",
|
||||
f"📊 涨跌额:{data['change']}",
|
||||
f"⬆️ 最高:{data['high']}",
|
||||
f"⬇️ 最低:{data['low']}",
|
||||
f"🔓 今开:{data['open']}",
|
||||
f"🔒 昨收:{data['yest_close']}",
|
||||
f"📈 成交量:{data['volume']}",
|
||||
f"💰 成交额:{data['amount']}万",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
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]
|
||||
normalized = normalize_stock_code(raw_code)
|
||||
quoted_code = parse_market_prefix(normalized)
|
||||
|
||||
# 在QQ接口中,多个股票用逗号分隔
|
||||
data = fetch_stock_quote(quoted_code)
|
||||
if not data:
|
||||
sys.stdout.write(FALLBACK_TEXT)
|
||||
sys.stdout.write("\n")
|
||||
return 0
|
||||
|
||||
message = format_stock_message(data)
|
||||
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