feat: 手动创建记忆
This commit is contained in:
parent
a909f2d01a
commit
0094304e75
21
skills/create-memory/SKILL.md
Normal file
21
skills/create-memory/SKILL.md
Normal file
@ -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
|
||||
```
|
||||
143
skills/create-memory/scripts/create_memory.py
Normal file
143
skills/create-memory/scripts/create_memory.py
Normal file
@ -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())
|
||||
Loading…
Reference in New Issue
Block a user