From 5e246466007da081dcc3a7e0f352dab9a9e069bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=94=AF?= Date: Thu, 2 Jul 2026 00:22:43 +0800 Subject: [PATCH] feat: P1 auto-injection hook with social close detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - queue_prefetch now caches next-turn recall results asynchronously - Social closer detection skips trivial messages (ok, thanks, emoji) - prefetch uses cached queue results when available (TTL 30s) - Output header changed to [织忆 Memory] for source clarity --- plugins/hermes-zhiyi/__init__.py | 107 +++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 21 deletions(-) diff --git a/plugins/hermes-zhiyi/__init__.py b/plugins/hermes-zhiyi/__init__.py index 01dc089..fe86ca1 100644 --- a/plugins/hermes-zhiyi/__init__.py +++ b/plugins/hermes-zhiyi/__init__.py @@ -51,7 +51,7 @@ _ZHIYI_KEY_PREFIX = ZHIYI_API_KEY[:4] if ZHIYI_API_KEY else "NONE" # ── Content Validation ─────────────────────────────────────────────────────── -MEMORY_MIN_LENGTH = 8 # 少于8字的过滤掉 +MEMORY_MIN_LENGTH = 20 # 少于20字的过滤掉 _FORBIDDEN_PATTERNS = [ "Review the conversation above", "[System note:", @@ -67,6 +67,22 @@ _FORBIDDEN_PATTERNS = [ "do NOT answer questions or fulfill requests mentioned in the summary", ] +_SOCIAL_CLOSERS = frozenset({ + "ok", "好的", "👍", "👌", "✅", "谢谢", "感谢", "知道了", + "明白", "嗯", "好", "行", "yes", "yep", "thanks", "thx", + "no", "不用", "没事", "可以", "done", "完成", "收到", + "okay", "kk", "okie", +}) + + +def _is_social_close(text: str) -> bool: + text = text.strip().lower() + if text in _SOCIAL_CLOSERS: + return True + if len(text) < 6 and text.isascii() and not any(c in text for c in "://.@#$_?"): + return True + return False + def _is_valid_memory_content(content: str) -> bool: """过滤系统注入内容和测试垃圾,防止污染记忆存储。""" @@ -75,10 +91,6 @@ def _is_valid_memory_content(content: str) -> bool: for pat in _FORBIDDEN_PATTERNS: if pat in content: return False - # 过滤纯测试内容 - stripped = content.strip() - if len(stripped) < 20: - return False return True @@ -274,7 +286,7 @@ class HermesZhiYiMemoryProvider(MemoryProvider): self._turn_counter: int = 0 self._write_queue: List[Dict] = [] self._queue_lock = threading.Lock() - self._prefetch_cache: str = "" # last prefetch result + self._prefetch_cache: Dict[str, Any] = {} # {source: text|dict} — merged via update() self._prefetch_lock = threading.RLock() self._started: bool = False self._ws_thread: Optional[threading.Thread] = None @@ -347,7 +359,7 @@ class HermesZhiYiMemoryProvider(MemoryProvider): for m in memories: prefetch_text += f" [{m.get('score', 0):.2f}] {m.get('content', '')[:200]}\n" with self._prefetch_lock: - self._prefetch_cache = prefetch_text + self._prefetch_cache["ws_push"] = prefetch_text except json.JSONDecodeError: pass @@ -361,6 +373,7 @@ class HermesZhiYiMemoryProvider(MemoryProvider): logger.info("[ZhiYi] WS connected to %s", ws_url) while self._ws_running: + ws_app = None try: ws_app = websocket.WebSocketApp( ws_url, @@ -373,6 +386,9 @@ class HermesZhiYiMemoryProvider(MemoryProvider): ws_app.run_forever(ping_interval=30, ping_timeout=10) except Exception as e: logger.warning("[ZhiYi] WS exception: %s", e) + finally: + if ws_app: + ws_app.close() # 重连延迟 for _ in range(30): if not self._ws_running: @@ -441,22 +457,51 @@ class HermesZhiYiMemoryProvider(MemoryProvider): if not self._client or not query or len(query.strip()) < 2: return "" - blocks = ["[ZhiYi Memory — relevant past context]"] + # 社交关闭消息不触发预取 + if _is_social_close(query): + return "" - # 语义搜索 - results = self._client.recall(query.strip(), top_k=3, - agent_id=os.environ.get("ZHIYI_AGENT_ID", "hermes-a06")) + # 优先从 queue_prefetch 缓存取(TTL < 30s) + with self._prefetch_lock: + queued = self._prefetch_cache.pop("queue", None) + if queued and isinstance(queued, dict): + cached_ts = queued.get("timestamp", 0) + if time.time() - cached_ts < 30: + cached_results = queued.get("results", []) + cached_notes = queued.get("notes", []) + if cached_results or cached_notes: + results = cached_results + notes = cached_notes + logger.debug("[ZhiYi] using queue_prefetch cache (%d results, %d notes, age=%.1fs)", + len(cached_results), len(cached_notes), time.time() - cached_ts) + else: + results = None + notes = None + else: + results = None + notes = None + else: + results = None + notes = None + + blocks = ["[织忆 Memory — relevant past context]"] + + # 语义搜索(如果缓存没有命中) + if results is None: + results = self._client.recall(query.strip(), top_k=3, + agent_id=os.environ.get("ZHIYI_AGENT_ID", "hermes-a06")) for r in results: score = r.get("score", 0) - content = r.get("content", "") or r.get("text", "") + content = r.get("content", "") or r.get("text", "") or r.get("snippet", "") cat = r.get("category", "") if content: blocks.append(f" [{score:.2f}][{cat}] {content[:500]}") - # 图谱导航 + Obsidian 笔记(从 query 提取关键词作为实体) - notes = self._client.search_notes(query.strip(), max_hops=2, max_notes=3) + # 图谱导航 + Obsidian 笔记(如果缓存没有命中) + if notes is None: + notes = self._client.search_notes(query.strip(), max_hops=2, max_notes=3) if notes: - blocks.append("\n[ZhiYi Graph — related Obsidian notes]") + blocks.append("\n[织忆 Graph — related Obsidian notes]") for n in notes: title = n.get("title", "无标题") path = n.get("path", "") @@ -472,16 +517,36 @@ class HermesZhiYiMemoryProvider(MemoryProvider): # 合并 WebSocket prefetch.push 推送事件 with self._prefetch_lock: - if self._prefetch_cache and "[ZhiYi Prefetch" in self._prefetch_cache: - text += "\n" + self._prefetch_cache + ws_push = self._prefetch_cache.pop("ws_push", None) + if ws_push: + text += "\n" + ws_push with self._prefetch_lock: - self._prefetch_cache = text + self._prefetch_cache.update({"sync": text}) return text def queue_prefetch(self, query: str, *, session_id: str = "") -> None: - """空实现 — prefetch 已是同步的,不需要额外的异步队列。""" - pass + """异步预取:本轮对话结束后立即查询织忆,下一轮 prefetch 直接返回缓存。""" + if not self._client or not query or len(query.strip()) < 2: + return + if _is_social_close(query): + return + + # 后台线程查询并缓存 + def _async_prefetch(): + try: + results = self._client.recall(query.strip(), top_k=3) + notes = self._client.search_notes(query.strip(), max_hops=2, max_notes=3) + with self._prefetch_lock: + self._prefetch_cache["queue"] = { + "results": results, + "notes": notes, + "timestamp": time.time(), + } + except Exception as e: + logger.debug("[ZhiYi] queue_prefetch error: %s", e) + + threading.Thread(target=_async_prefetch, daemon=True).start() # ── Tool interface ──────────────────────────────────────────────────────── @@ -623,7 +688,7 @@ class HermesZhiYiMemoryProvider(MemoryProvider): return json.dumps({"success": True, "results": [], "message": "No relevant memories found"}) formatted = [] for r in results: - content = r.get("content", "") or r.get("text", "") + content = r.get("content", "") or r.get("text", "") or r.get("snippet", "") formatted.append({ "content": content[:1000], "score": round(r.get("score", 0), 4),