diff --git a/gp/SKILL.md b/gp/SKILL.md index 6954968..0d0a46b 100644 --- a/gp/SKILL.md +++ b/gp/SKILL.md @@ -1,74 +1,118 @@ ---- -name: stock -description: "当用户发送「股票 代码」或「gp 代码」时触发。调用腾讯股票行情接口获取实时数据,再调用本地微信机器人发消息接口把股票信息发给当前用户。" -argument-hint: "参数为股票代码,例如 600519 或 sz000001" ---- +--- +name: stock +description: "当用户发送「股票 代码」或「gp 代码」时触发。调用 mairuiapi 实时交易数据接口获取行情,再调用本地微信机器人发消息接口把股票信息发给当前用户。" +argument-hint: "参数为股票代码,例如 600519 或 000001" +--- # Stock Skill ## 描述 这是一个用于查询股票实时行情并直接发送给当前用户的技能。 -当用户发送 `股票 600519` 或 `gp sz000001` 时,调用腾讯股票行情接口获取实时数据,再调用本地微信机器人接口把格式化后的股票信息发出去。 +当用户发送 `股票 600519` 或 `gp 000001` 时,调用 mairuiapi 实时交易数据接口获取行情数据,再调用本地微信机器人接口把格式化后的股票信息发出去。 这个仓库里额外提供了一个可执行脚本 `scripts/stock.py`,方便宿主机器人直接调用。 ## 触发条件 - 用户发送 `股票 <股票代码>` 或 `gp <股票代码>` -- 股票代码可以是纯数字(如 `600519`),也可以带市场前缀(如 `sh600519`、`sz000001`) +- 股票代码为纯数字(如 `600519`、`000001`) ## 接口信息 -- 获取股票行情:`https://qt.gtimg.cn/q={stock_code}` +- 获取股票行情:`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` -### 腾讯股票行情返回示例 -``` -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~" +### 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 +}] ``` ### 字段说明 -| 索引 | 含义 | -|------|------| -| 1 | 股票名称 | -| 2 | 股票代码 | -| 3 | 当前价格 | -| 4 | 昨收价 | -| 5 | 今开价 | -| 6 | 成交量(手) | -| 7 | 成交额(万) | -| 8 | 最高价 | -| 9 | 最低价 | -| 10 | 涨跌额 | -| 11 | 涨跌幅(%) | +| 字段 | 数据类型 | 字段说明 | +|------|---------|---------| +| 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. 脚本内部自动处理股票代码格式(补全市场前缀 sh/sz)。 -4. 脚本发送 `GET` 请求到 `https://qt.gtimg.cn/q={stock_code}`。 -5. 脚本解析返回的行情数据,提取关键字段。 +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": "【{股票名称}({股票代码})】 - 当前价:{当前价格} - 涨跌幅:{涨跌幅}% - 涨跌额:{涨跌额} - 最高:{最高价} - 最低:{最低价} - 今开:{今开价} - 昨收:{昨收价} - 成交量:{成交量} - 成交额:{成交额}" + "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. 如果任一步骤失败,回复兜底文案:`股票查询失败,请稍后再试。` diff --git a/gp/stock.py b/gp/stock.py index 5d475e8..86d9b33 100644 --- a/gp/stock.py +++ b/gp/stock.py @@ -16,98 +16,102 @@ else: sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") 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 = "股票查询失败,请稍后再试。" 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() - # 纯数字处理 + """验证并标准化股票代码(仅保留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): - # 沪市: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 + return code + # 如果传入了完整代码但包含其他字符,尝试提取数字 + digits = re.findall(r"\d", code) + if len(digits) >= 6: + return "".join(digits[:6]) + return "" -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 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: - 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): + 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 - # 解析返回数据,格式如:v_sh600519="..."; - match = re.search(r'="([^"]+)"', raw) - if not match: + # 处理 JSON 数组返回格式 [{}, ...] + if isinstance(payload, list) and len(payload) > 0: + payload = payload[0] + + if not isinstance(payload, dict): return None - fields = match.group(1).split("~") - if len(fields) < 45: + # 检查接口返回是否包含有效数据 + if "p" not in payload: 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], # 涨跌幅(%) - } + return payload -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 = [ - f"【{data['name']}({data['code']})】", + title, 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']}万", + 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() @@ -151,17 +155,19 @@ def main() -> int: return 0 raw_code = args[0] - normalized = normalize_stock_code(raw_code) - quoted_code = parse_market_prefix(normalized) + stock_code = normalize_stock_code(raw_code) + if not stock_code: + sys.stdout.write(FALLBACK_TEXT) + sys.stdout.write("\n") + return 0 - # 在QQ接口中,多个股票用逗号分隔 - data = fetch_stock_quote(quoted_code) + 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) + message = format_stock_message(data, stock_code) if send_text(message): return 0