From f9aa0f65c1223bff2986f4eec8562c83c6aab44f Mon Sep 17 00:00:00 2001 From: hp0912 <809211365@qq.com> Date: Sat, 6 Jun 2026 16:28:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=91=E9=80=81=E8=A1=A8=E6=83=85?= =?UTF-8?q?=E6=8A=80=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 ++- skills/send-emoji/SKILL.md | 102 ++++++++++++++ skills/send-emoji/scripts/send_emoji.py | 176 ++++++++++++++++++++++++ 3 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 skills/send-emoji/SKILL.md create mode 100644 skills/send-emoji/scripts/send_emoji.py diff --git a/README.md b/README.md index c8baf9d..bf7f8fb 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ ``` -**发送视频的时候也可以调用 Agent 接口** +**发送视频的时候可以调用 Agent 接口** ``` [POST] http://127.0.0.1:{ROBOT_WECHAT_CLIENT_PORT}/api/v1/robot/message/send/video/url @@ -73,7 +73,7 @@ } ``` -**发送语音的时候也可以调用 Agent 接口** +**发送语音的时候可以调用 Agent 接口** ``` [POST] http://127.0.0.1:{ROBOT_WECHAT_CLIENT_PORT}/api/v1/robot/message/send/voice @@ -107,3 +107,17 @@ "at": [], // 字符串数组,你要艾特的人的微信 id } ``` + +**Agent 发送表情消息接口** + +``` +[POST] http://127.0.0.1:{ROBOT_WECHAT_CLIENT_PORT}/api/v1/robot/message/send/emoji + +请求体 Body: + +{ + "to_wxid": "{{ROBOT_FROM_WX_ID}}", + "Md5": "xxxxxxx", // 表情 md5 + "TotalLen": 263, // 表情长度 +} +``` diff --git a/skills/send-emoji/SKILL.md b/skills/send-emoji/SKILL.md new file mode 100644 index 0000000..5bd6faf --- /dev/null +++ b/skills/send-emoji/SKILL.md @@ -0,0 +1,102 @@ +--- +name: send-emoji +description: "发送微信表情技能。使用表情包聊天可以活跃气氛,也可以缓解尴尬,你可以在适当的时候发送表情包活跃聊天氛围,也可以在遇到不方便回答或者不想回答的问题时发送一个表情包来巧妙地回避。" +argument-hint: "需要 name,如 [开心]、[快乐];可重复传入发送多个表情。ended 标志表示发送表情后结束对话。" +--- + +# Send Emoji Skill + +## 描述 + +这是一个向当前微信会话发送表情的技能。 + +技能脚本位于 `scripts/send_emoji.py`,内部维护了表情名称到微信表情标识(Md5 / TotalLen)的映射表,对 AI 模型完全透明——模型只需传入表情名称(如 `[开心]`),脚本自动查找对应的 Md5 和 TotalLen 并调用客户端接口发送。 + +## 可用表情 + +| 名称 | +| -------- | +| `[调皮]` | +| `[无语]` | +| `[爱心]` | +| `[安慰]` | +| `[嘲笑]` | +| `[傻瓜]` | +| `[厉害]` | +| `[生气]` | +| `[开心]` | + +> 表情映射表在脚本 `EMOJI_MAP` 中维护,新增表情直接在脚本中添加即可,无需修改 SKILL.md。 + +## 触发条件 + +- Agent 根据上下文判断需要用表情回应时,从可用表情列表中选取合适的。 +- 用户要求发送某个微信表情。 +- 用户说「发个表情」「来个表情包」「发个 xxx 表情」。 + +## 入参规范 + +```json +{ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "表情名称,如 [开心]、[快乐]。必须是可用表情列表中的名称。" + }, + "names": { + "type": "array", + "items": { + "type": "string" + }, + "description": "可选,多个表情名称数组。" + }, + "ended": { + "type": "boolean", + "description": "是否结束当前对话。当 Agent 已经完成表情发送、要说的话已说完、要做的事已做完时,设置为 true。" + } + }, + "anyOf": [{ "required": ["name"] }, { "required": ["names"] }], + "additionalProperties": false +} +``` + +对应命令行参数: + +- `--name <表情名称>` 必填或可重复传入,如 `--name '[开心]'` +- `--names ` 可选,用于一次传入多个名称,如 `--names '["[开心]", "[快乐]"]'` +- `--ended` 可选标志 + +```bash +python3 scripts/send_emoji.py --name '[开心]' +``` + +发送多个表情: + +```bash +python3 scripts/send_emoji.py --name '[开心]' --name '[快乐]' +``` + +当 Agent 认为任务已完成、对话可以结束时,加上 `--ended` 标志: + +```bash +python3 scripts/send_emoji.py --name '[开心]' --ended +``` + +4. 脚本在内部查找 EMOJI_MAP,获取对应的 Md5 和 TotalLen,调用客户端接口 `POST http://127.0.0.1:{ROBOT_WECHAT_CLIENT_PORT}/api/v1/robot/message/send/emoji` 发送表情。 + +## 校验规则 + +- `name` 必须存在于脚本内置的 EMOJI_MAP 中,否则脚本会报错并列出可用表情。 +- 环境变量 `ROBOT_WECHAT_CLIENT_PORT` 和 `ROBOT_FROM_WX_ID` 必须已配置。 + +## ended 行为 + +- 当 `--ended` 传入时,脚本在正常输出末尾追加打印独立一行 `ended`。 +- Agent 检测到输出以 `ended` 结尾时,会自动退出 Agent 循环结束对话。 + +## 回复要求 + +- 成功时,脚本输出「表情发送成功」,表示表情已通过客户端接口直接发送,无需 AI 智能体再做额外处理。 +- 如果传入 `--ended`,输出末尾会追加 `ended`,Agent 会自动结束对话。 +- 失败时,返回脚本输出的具体错误信息(含可用表情列表)。 diff --git a/skills/send-emoji/scripts/send_emoji.py b/skills/send-emoji/scripts/send_emoji.py new file mode 100644 index 0000000..2898446 --- /dev/null +++ b/skills/send-emoji/scripts/send_emoji.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import json +import os +import sys +import urllib.request + +sys.stderr = sys.stdout + +# ============================================================ +# 表情映射表 —— 对 AI 模型透明,脚本内部维护 +# 格式: "[名称]" -> {"Md5": str, "TotalLen": int} +# ============================================================ +EMOJI_MAP: dict[str, dict[str, object]] = { + "[调皮]": { + "Md5": "da56c104712858765ff6edb61083c4ab", + "TotalLen": 10250946, + }, + "[无语]": { + "Md5": "8690c66ab09a767cb5f7c7818c0a517f", + "TotalLen": 26296, + }, + "[爱心]": { + "Md5": "1b44ec167ccb38d8260a57bccaa516e5", + "TotalLen": 8337713, + }, + "[安慰]": { + "Md5": "ad2f9eb42093bf73190cef07d8958e6b", + "TotalLen": 418037, + }, + "[嘲笑]": { + "Md5": "16644536f3300a81d96f712a399ff92e", + "TotalLen": 205506, + }, + "[傻瓜]": { + "Md5": "45484d7b1133b16e68285943a6dc1ba3", + "TotalLen": 74925, + }, + "[厉害]": { + "Md5": "de4fb8aff77f474746efa3e7a6d638dc", + "TotalLen": 57770, + }, + "[生气]": { + "Md5": "b74fc587508b4b1ce13009ed73585f44", + "TotalLen": 59990, + }, + "[开心]": { + "Md5": "4aae9dbe27651859ed526bcaad5f1f34", + "TotalLen": 19788, + }, +} + + +def _http_post_json(url: str, body: dict, timeout: int = 300) -> dict: + data = json.dumps(body).encode("utf-8") + req = urllib.request.Request( + url, + data=data, + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=timeout) as resp: + text = resp.read().decode("utf-8") + if not text.strip(): + return {} + return json.loads(text) + + +def _expand_json_array_values(values: list[str], label: str) -> list[str]: + expanded: list[str] = [] + for value in values: + stripped = value.strip() + if not stripped: + continue + if stripped.startswith("["): + parsed = json.loads(stripped) + if not isinstance(parsed, list): + raise ValueError(f"{label} 必须是字符串数组") + for item in parsed: + if not isinstance(item, str): + raise ValueError(f"{label} 必须是字符串数组") + if item.strip(): + expanded.append(item.strip()) + continue + expanded.append(stripped) + return expanded + + +def _parse_cli_params(argv: list[str]) -> tuple[list[str], bool]: + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--name", action="append", default=[]) + parser.add_argument("--names", action="append", default=[]) + parser.add_argument("--ended", action="store_true", default=False) + + namespace, unknown = parser.parse_known_args(argv) + if unknown: + raise ValueError(f"存在不支持的参数: {' '.join(unknown)}") + + names = _expand_json_array_values(namespace.name + namespace.names, "names") + # 去重,保持顺序 + deduped: list[str] = [] + seen = set() + for name in names: + if name not in seen: + seen.add(name) + deduped.append(name) + + return deduped, namespace.ended + + +def _lookup_emoji(name: str) -> dict[str, object]: + """在 EMOJI_MAP 中查找表情,未找到抛出 ValueError。""" + if name not in EMOJI_MAP: + available = ", ".join(EMOJI_MAP.keys()) + raise ValueError(f"未知表情: {name},可用表情: {available}") + return EMOJI_MAP[name] + + +def _send_emoji(client_port: str, to_wxid: str, md5: str, total_len: int) -> None: + send_url = f"http://127.0.0.1:{client_port}/api/v1/robot/message/send/emoji" + _http_post_json(send_url, {"to_wxid": to_wxid, "Md5": md5, "TotalLen": total_len}) + + +def main() -> int: + if len(sys.argv) < 2: + sys.stdout.write("缺少表情名称(需要 --name)\n") + return 1 + + try: + names, ended = _parse_cli_params(sys.argv[1:]) + except (ValueError, json.JSONDecodeError) as exc: + sys.stdout.write(f"参数格式错误: {exc}\n") + return 1 + + if not names: + sys.stdout.write("缺少表情名称(需要 --name)\n") + return 1 + + # 查找表情 + emojis: list[dict[str, object]] = [] + try: + for name in names: + emojis.append(_lookup_emoji(name)) + except ValueError as exc: + sys.stdout.write(f"{exc}\n") + return 1 + + client_port = os.environ.get("ROBOT_WECHAT_CLIENT_PORT", "").strip() + if not client_port: + sys.stdout.write("环境变量 ROBOT_WECHAT_CLIENT_PORT 未配置\n") + return 1 + + to_wxid = os.environ.get("ROBOT_FROM_WX_ID", "").strip() + if not to_wxid: + sys.stdout.write("环境变量 ROBOT_FROM_WX_ID 未配置\n") + return 1 + + try: + for emoji in emojis: + md5 = str(emoji["Md5"]) + total_len = int(emoji["TotalLen"]) # type: ignore[arg-type] + _send_emoji(client_port, to_wxid, md5, total_len) + sys.stdout.write("表情发送成功\n") + if ended: + sys.stdout.write("ended") + return 0 + except Exception as exc: + sys.stdout.write(f"表情发送失败: {exc}\n") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main())