diff --git a/skills/create-memory/SKILL.md b/skills/create-memory/SKILL.md new file mode 100644 index 0000000..45d969b --- /dev/null +++ b/skills/create-memory/SKILL.md @@ -0,0 +1,21 @@ +--- +name: create-memory +description: "在当前微信会话中新增长期记忆。当好友或群成员明确说‘记住……’‘帮我记一下……’‘把这个存进记忆’或‘新增记忆’时使用;不要用于查询、回忆或核对已有记忆。" +--- + +# 新增记忆 + +运行 `python3 scripts/create_memory.py`,让机器人客户端根据当前消息创建长期记忆;当前消息存在直接引用时,被引用消息会与当前消息共同参与提取。 + +- 用户不需要指定记忆类型。客户端自行推断名称、画像、事实和关系,一次请求可以创建任意数量、任意组合的结构化记忆。 +- 没有引用时只处理当前消息;有引用时只处理被引用消息和当前消息,不读取两条消息之间的聊天内容。 +- 当前消息必须包含具体指令。只有“记住这个”“记住刚才的”但既没有具体内容也没有引用时,请用户重新说明或引用原消息。 +- 不要向脚本传记忆内容、类型、会话信息或微信 ID;脚本只转发当前消息 ID 和可选的引用消息 ID。 +- 真实创建时不传 `--dry-run`,也不自动重试超时或连接中断的请求。 +- 仅在脚本返回 `ok: true` 后告知用户已记住;失败时直接说明错误,不能声称创建成功。 + +通过本技能成功创建的所有记忆都由客户端强制设为 `confidence=100`。人物或账号无法唯一对应时,不要猜测。 + +```bash +python3 scripts/create_memory.py +``` diff --git a/skills/create-memory/scripts/create_memory.py b/skills/create-memory/scripts/create_memory.py new file mode 100644 index 0000000..d4ec0b6 --- /dev/null +++ b/skills/create-memory/scripts/create_memory.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import json +import os +import sys +import urllib.error +import urllib.request +from typing import Any, NoReturn + +sys.stderr = sys.stdout + + +class SkillArgumentParser(argparse.ArgumentParser): + def error(self, message: str) -> NoReturn: + raise ValueError(f"参数错误:{message}") + + +def _parse_args(argv: list[str]) -> argparse.Namespace: + parser = SkillArgumentParser(description="创建当前微信会话的长期记忆") + parser.add_argument("--dry-run", action="store_true") + return parser.parse_args(argv) + + +def _require_env(name: str) -> str: + value = os.environ.get(name, "").strip() + if not value: + raise ValueError(f"环境变量 {name} 未配置") + return value + + +def _positive_int_env(name: str) -> int: + raw = _require_env(name) + try: + value = int(raw) + except ValueError as exc: + raise ValueError(f"环境变量 {name} 必须是整数") from exc + if value < 1: + raise ValueError(f"环境变量 {name} 必须是正整数") + return value + + +def _optional_positive_int_env(name: str) -> int | None: + raw = os.environ.get(name, "").strip() + if not raw: + return None + try: + value = int(raw) + except ValueError as exc: + raise ValueError(f"环境变量 {name} 必须是整数") from exc + if value < 0: + raise ValueError(f"环境变量 {name} 不能是负数") + return value or None + + +def _client_port() -> int: + raw = _require_env("ROBOT_WECHAT_CLIENT_PORT") + try: + port = int(raw) + except ValueError as exc: + raise ValueError("机器人客户端端口必须是整数") from exc + if port < 1 or port > 65535: + raise ValueError("机器人客户端端口必须在 1 到 65535 之间") + return port + + +def _request_body() -> dict[str, Any]: + body: dict[str, Any] = { + "message_id": _positive_int_env("ROBOT_MESSAGE_ID"), + } + referenced_message_id = _optional_positive_int_env("ROBOT_REF_MESSAGE_ID") + if referenced_message_id is not None: + body["referenced_message_id"] = referenced_message_id + return body + + +def _post_json(url: str, body: dict[str, Any]) -> dict[str, Any]: + request = urllib.request.Request( + url, + data=json.dumps(body, ensure_ascii=False).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=300) as response: + text = response.read().decode("utf-8") + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace").strip() + raise RuntimeError(f"客户端返回 HTTP {exc.code}:{detail or exc.reason}") from exc + except urllib.error.URLError as exc: + if isinstance(exc.reason, TimeoutError): + raise RuntimeError( + "请求超时,记忆可能已经保存,请先查询后再决定是否重试" + ) from exc + raise RuntimeError(f"无法连接机器人客户端:{exc.reason}") from exc + except TimeoutError as exc: + raise RuntimeError("请求超时,记忆可能已经保存,请先查询后再决定是否重试") from exc + except OSError as exc: + raise RuntimeError( + f"请求中断,记忆可能已经保存,请先查询后再决定是否重试:{exc}" + ) from exc + + try: + payload = json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError("机器人客户端返回了无效 JSON") from exc + if not isinstance(payload, dict): + raise RuntimeError("机器人客户端返回格式错误") + return payload + + +def _validate_success(payload: dict[str, Any]) -> None: + if payload.get("code") != 200: + message = str(payload.get("message") or "未知业务错误").strip() + raise RuntimeError(message) + + +def main() -> int: + try: + args = _parse_args(sys.argv[1:]) + body = _request_body() + port = _client_port() + if args.dry_run: + output = {"ok": True, "dry_run": True, "request": body} + else: + url = f"http://127.0.0.1:{port}/api/v1/robot/memories" + payload = _post_json(url, body) + _validate_success(payload) + output = { + "ok": True, + "message": "长期记忆创建成功", + } + sys.stdout.write(json.dumps(output, ensure_ascii=False) + "\n") + return 0 + except (ValueError, RuntimeError) as exc: + sys.stdout.write(f"创建记忆失败:{exc}\n") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main())