Lukas Goldschmidt 14 цаг өмнө
parent
commit
adb914e755
3 өөрчлөгдсөн 198 нэмэгдсэн , 7 устгасан
  1. 16 0
      hook/HOOK.md
  2. 40 0
      hook/command-list.json
  3. 142 7
      hook/handler.ts

+ 16 - 0
hook/HOOK.md

@@ -0,0 +1,16 @@
+---
+name: mem0-auto-capture
+description: "Auto-capture (please remember + last 2) + auto-recall for mem0"
+metadata:
+  { "openclaw": { "emoji": "🧠", "events": ["message:received", "message:preprocessed"] } }
+---
+
+# mem0-auto-capture
+
+- **Capture**: on `message:received`, only stores when the message contains **"please remember"** (case-insensitive). Captures the **last 2 messages** (previous + current).
+- **Recall**: on `message:preprocessed`, injects top mem0 matches into `bodyForAgent`.
+
+Config flags used:
+- `plugins.entries.openclaw-mem0-python.config.autoCapture`
+- `plugins.entries.openclaw-mem0-python.config.autoRecall`
+- `plugins.entries.openclaw-mem0-python.config.recallLimit`

+ 40 - 0
hook/command-list.json

@@ -0,0 +1,40 @@
+[
+  "start",
+  "stop",
+  "restart",
+  "status",
+  "logs",
+  "log",
+  "model",
+  "models",
+  "agent",
+  "agents",
+  "session",
+  "sessions",
+  "memory",
+  "memories",
+  "hook",
+  "hooks",
+  "gateway",
+  "cron",
+  "config",
+  "commands",
+  "help",
+  "version",
+  "update",
+  "exec",
+  "run",
+  "send",
+  "message",
+  "search",
+  "fetch",
+  "schedule",
+  "install",
+  "uninstall",
+  "status",
+  "refresh",
+  "save",
+  "list",
+  "deploy",
+  "upload"
+]

+ 142 - 7
hook/handler.ts

@@ -1,6 +1,53 @@
 import fs from "fs";
 import os from "os";
 import path from "path";
+import { fileURLToPath } from "url";
+
+const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
+const COMMAND_LIST_PATH = path.join(MODULE_DIR, "command-list.json");
+const OPENCLAW_CONFIG_PATH = path.join(os.homedir(), ".openclaw", "openclaw.json");
+const DEFAULT_COMMANDS = [
+  "start",
+  "stop",
+  "restart",
+  "status",
+  "logs",
+  "log",
+  "model",
+  "models",
+  "agent",
+  "agents",
+  "session",
+  "sessions",
+  "memory",
+  "memories",
+  "hook",
+  "hooks",
+  "gateway",
+  "cron",
+  "config",
+  "commands",
+  "help",
+  "version",
+  "update",
+  "exec",
+  "run",
+  "send",
+  "message",
+  "search",
+  "fetch",
+  "schedule",
+  "install",
+  "uninstall",
+  "refresh",
+  "save",
+  "list",
+  "deploy",
+  "upload"
+].map((cmd) => cmd.toLowerCase());
+const STATIC_COMMANDS = loadCommandList();
+const COMMAND_BLACKLIST = new Set(STATIC_COMMANDS);
+const ALIAS_BLACKLIST = buildAliasBlacklist();
 
 // ---------------------------------------------------------------------------
 // Config — openclaw does NOT inject cfg into hook events.
@@ -58,6 +105,63 @@ function loadPluginCfg() {
   };
 }
 
+function loadCommandList(): string[] {
+  try {
+    const raw = fs.readFileSync(COMMAND_LIST_PATH, "utf8");
+    const parsed = JSON.parse(raw);
+    if (Array.isArray(parsed)) {
+      return parsed
+        .map((item) => String(item || "").trim().toLowerCase())
+        .filter(Boolean);
+    }
+  } catch (err) {
+    console.warn("[mem0] failed to load command list, using defaults:", err?.message || err);
+  }
+  return DEFAULT_COMMANDS;
+}
+
+function buildAliasBlacklist(): Set<string> {
+  const aliases = new Set<string>();
+  try {
+    const raw = fs.readFileSync(OPENCLAW_CONFIG_PATH, "utf8");
+    const config = JSON.parse(raw);
+    const addAlias = (alias: unknown) => {
+      if (typeof alias === "string" && alias.trim()) {
+        aliases.add(alias.trim().toLowerCase());
+      }
+    };
+    const addModelAliases = (models: Record<string, any> | undefined) => {
+      if (!models) return;
+      for (const modelConfig of Object.values(models)) {
+        addAlias(modelConfig?.alias);
+      }
+    };
+    addModelAliases(config?.agents?.defaults?.models);
+    if (Array.isArray(config?.agents?.list)) {
+      for (const agent of config.agents.list) {
+        addModelAliases(agent?.models);
+      }
+    }
+  } catch (err) {
+    console.warn("[mem0] could not build alias blacklist:", err?.message || err);
+  }
+  return aliases;
+}
+
+function extractSlashToken(text: string): string | null {
+  const trimmed = text.trim();
+  if (!trimmed.startsWith("/")) return null;
+  const token = trimmed.slice(1).split(/\s+/)[0];
+  if (!token) return null;
+  return token.toLowerCase();
+}
+
+function isSlashCommand(text: string): boolean {
+  const token = extractSlashToken(text);
+  if (!token) return false;
+  return COMMAND_BLACKLIST.has(token) || ALIAS_BLACKLIST.has(token);
+}
+
 // ---------------------------------------------------------------------------
 // Types
 // ---------------------------------------------------------------------------
@@ -76,6 +180,7 @@ interface MemoryResult {
   score: number;
   rerank_score: number;
   metadata?: Record<string, any>;
+  created_at?: string;
 }
 
 interface SearchResponse {
@@ -250,6 +355,27 @@ function formatKnowledgeCitation(meta: Record<string, any> = {}): string {
   return parts.length > 0 ? `(from: ${parts.join(", ")})` : "";
 }
 
+function formatMemoryAge(createdAt?: string): string | null {
+  if (!createdAt) return null;
+  const epoch = Date.parse(createdAt);
+  if (Number.isNaN(epoch)) return null;
+  const deltaMs = Date.now() - epoch;
+  if (deltaMs < 0) return null;
+  const minutes = Math.floor(deltaMs / 60_000);
+  if (minutes < 1) return "[just now]";
+  if (minutes < 60) return `[${minutes}m ago]`;
+  const hours = Math.floor(minutes / 60);
+  if (hours < 24) return `[${hours}h ago]`;
+  const days = Math.floor(hours / 24);
+  if (days < 7) return `[${days}d ago]`;
+  const weeks = Math.floor(days / 7);
+  if (weeks < 4) return `[${weeks}w ago]`;
+  const months = Math.floor(days / 30);
+  if (months < 12) return `[${months}mo ago]`;
+  const years = Math.floor(days / 365);
+  return `[${years}y ago]`;
+}
+
 function buildInjectionBlock(
   personalResults: MemoryResult[] | null,
   knowledgeResults: MemoryResult[] | null,
@@ -266,7 +392,11 @@ function buildInjectionBlock(
     // endpoint was down — omit the section entirely, already logged
   } else if (personal.length > 0) {
     const lines = personal
-      .map((r) => `- ${r.memory}`)
+      .map((r) => {
+        const age = formatMemoryAge(r.created_at);
+        const prefix = age ? `${age} ` : "";
+        return `- ${prefix}${r.memory}`;
+      })
       .join("\n");
     sections.push(`[MEMORY - Personal]\n${lines}`);
   }
@@ -284,7 +414,10 @@ function buildInjectionBlock(
     const lines = knowledge
       .map((r) => {
         const citation = formatKnowledgeCitation(r.metadata || {});
-        return citation ? `- ${citation} ${r.memory}` : `- ${r.memory}`;
+        const age = formatMemoryAge(r.created_at);
+        const prefix = age ? `${age} ` : "";
+        const body = citation ? `${citation} ${r.memory}` : r.memory;
+        return `- ${prefix}${body}`;
       })
       .join("\n");
     sections.push(`[MEMORY - Knowledge Base]\n${lines}`);
@@ -385,12 +518,13 @@ export default async function handler(event: HookEvent) {
       else if (typeof m?.text === "string") text = m.text.trim();
       else if (typeof m?.body === "string") text = m.body.trim();
     }
-    return text;
+    return text.trim();
   }
 
   // ── Auto-recall: queries both endpoints in parallel ───────────────────────
   const runAutoRecall = async (text: string) => {
     if (!pluginCfg.autoRecall) return;
+    if (!text || isSlashCommand(text)) return;
 
     const {
       baseUrl,
@@ -431,7 +565,7 @@ export default async function handler(event: HookEvent) {
   if (event.action === "received") {
     if (!pluginCfg.autoCapture) return;
     const text = resolveText();
-    if (!text) return;
+    if (!text || isSlashCommand(text)) return;
     if (isMediaPlaceholder(text) && !transcriptNow) return;
 
     const recent = pushRecent(
@@ -452,6 +586,7 @@ export default async function handler(event: HookEvent) {
     if (!shouldCapture) return;
 
     const captureText = recent.join("\n");
+    if (!captureText.trim()) return;
     const hash = simpleHash(captureText);
     const lastCapture = recentlyCaptured.get(hash);
     if (lastCapture && Date.now() - lastCapture < CAPTURE_DEDUP_MS) {
@@ -488,7 +623,7 @@ export default async function handler(event: HookEvent) {
   // ── preprocessed: recall ─────────────────────────────────────────────────
   if (event.action === "preprocessed") {
     const text = resolveText();
-    if (!text) return;
+    if (!text || isSlashCommand(text)) return;
     if (!transcriptNow && isMediaPlaceholder(text)) return;
     await runAutoRecall(text);
     return;
@@ -496,8 +631,8 @@ export default async function handler(event: HookEvent) {
 
   // ── transcribed: recall ──────────────────────────────────────────────────
   if (event.action === "transcribed") {
-    const text = extractMessageText(event.context);
-    if (!text) return;
+    const text = extractMessageText(event.context).trim();
+    if (!text || isSlashCommand(text)) return;
     await runAutoRecall(text);
   }
 }