Ver código fonte

first commit

Lukas Goldschmidt 1 mês atrás
commit
4a8732cf1c
5 arquivos alterados com 216 adições e 0 exclusões
  1. 17 0
      PROJECT.md
  2. 32 0
      README.md
  3. 151 0
      index.ts
  4. 8 0
      openclaw.plugin.json
  5. 8 0
      package.json

+ 17 - 0
PROJECT.md

@@ -0,0 +1,17 @@
+# PROJECT: chat-logger-plugin
+
+## Purpose
+Provide a lightweight log of recent conversation turns so other hooks (notably mem0-auto-capture) can retrieve the previous assistant reply when storing a new user memory.
+
+## Scope
+- Capture the last two turns (user + assistant) on `agent_end`
+- Resolve and cache `sessionKey` when available
+- Write JSON lines to `/tmp/openclaw-chat.log`
+
+## Integration notes
+- mem0-auto-capture reads the log and uses `messages[1]` when `role === "assistant"`.
+- If the session key is missing or unknown, the hook will fall back to the latest assistant message globally.
+
+## Operational notes
+- Keep `workspace/chat-logger-plugin` as the source of truth.
+- Ensure the gateway is restarted after edits.

+ 32 - 0
README.md

@@ -0,0 +1,32 @@
+# chat-logger-plugin
+
+Logs recent chat turns to `/tmp/openclaw-chat.log` so the mem0 auto-capture hook can attach the *previous assistant* reply when storing conversational memories.
+
+## What it does
+- Listens to `session_start`, `message_received`, and `agent_end` events
+- Resolves the best available `sessionKey` and caches it
+- Writes the **last two turns** (user + assistant) to `/tmp/openclaw-chat.log`
+
+## Why it matters
+The mem0 hook reads the log to retrieve the last assistant reply and include it in the capture payload. This avoids losing context when auto-capturing a new user message.
+
+## Output format
+Each log line is a JSON object:
+```json
+{
+  "ts": "2026-03-16T02:30:46.827Z",
+  "sessionKey": "agent:main:main",
+  "messages": [
+    { "role": "user", "content": "..." },
+    { "role": "assistant", "content": "..." }
+  ]
+}
+```
+
+## Install / reload
+1) Edit in `workspace/chat-logger-plugin/`
+2) Restart gateway so the plugin is reloaded
+
+## Related
+- Hook: `workspace/hooks/mem0-auto-capture/hook/handler.ts`
+- The hook falls back to the latest assistant message if sessionKey is missing.

+ 151 - 0
index.ts

@@ -0,0 +1,151 @@
+import fs from "fs";
+const LOG_FILE = "/tmp/openclaw-chat.log";
+const sessionKeys = new Map<string, string>();
+let lastKnownSessionKey = "";
+function appendLog(entry: Record<string, unknown>) {
+  try {
+    fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + "\n", "utf8");
+  } catch {}
+}
+function extractText(content: any): string {
+  if (typeof content === "string") return content;
+  if (Array.isArray(content)) {
+    return content
+      .filter((b: any) => b.type === "text")
+      .map((b: any) => b.text as string)
+      .join("");
+  }
+  return "";
+}
+function extractUserText(content: any): string {
+  const raw = extractText(content);
+  const match = raw.match(/\[.*?\]\s*([\s\S]+)$/);
+  return match ? match[1].trim() : raw.trim();
+}
+function cleanAssistant(content: any): string {
+  return extractText(content)
+    .replace(/^\[\[reply_to_current\]\]\s*/, "")
+    .trim();
+}
+function isRealAssistant(msg: any): boolean {
+  const text = cleanAssistant(msg.content);
+  return !!text && !text.startsWith("Model reset");
+}
+function normalizeSessionKey(value: unknown): string | undefined {
+  if (typeof value !== "string") return undefined;
+  const v = value.trim();
+  if (!v || v === "unknown") return undefined;
+  return v;
+}
+function deepFindSessionKey(obj: any, maxDepth = 6): string | undefined {
+  const seen = new Set<any>();
+  function walk(node: any, depth: number): string | undefined {
+    if (!node || depth > maxDepth) return undefined;
+    if (typeof node !== "object") return undefined;
+    if (seen.has(node)) return undefined;
+    seen.add(node);
+    for (const [k, v] of Object.entries(node)) {
+      if (typeof v === "string") {
+        const normalized = normalizeSessionKey(v);
+        if (!normalized) continue;
+        if (k.toLowerCase().includes("session") && normalized.includes(":")) {
+          return normalized;
+        }
+        if (/^agent:[^:\s]+:[^:\s]+$/.test(normalized)) {
+          return normalized;
+        }
+      }
+    }
+    for (const v of Object.values(node)) {
+      const found = walk(v, depth + 1);
+      if (found) return found;
+    }
+    return undefined;
+  }
+  return walk(obj, 0);
+}
+function pickSessionKey(ctx: any): string | undefined {
+  const candidate =
+    ctx.sessionKey ??
+    ctx.result?.sessionKey ??
+    ctx.context?.sessionKey ??
+    ctx.metadata?.sessionKey ??
+    ctx.message?.sessionKey ??
+    ctx.session?.sessionKey ??
+    ctx.session?.key ??
+    ctx.user?.sessionKey;
+  const direct = normalizeSessionKey(candidate);
+  if (direct) return direct;
+  return deepFindSessionKey(ctx);
+}
+function cacheSessionKey(ctx: any) {
+  const key = pickSessionKey(ctx);
+  if (!key) return;
+  const ids = [
+    ctx.sessionId,
+    ctx.context?.sessionId,
+    ctx.metadata?.sessionId,
+    ctx.session?.id,
+    ctx.session?.sessionId,
+    ctx.result?.sessionId,
+    ctx.message?.sessionId,
+  ].filter((v) => typeof v === "string" && !!v);
+  for (const id of ids) sessionKeys.set(id, key);
+  // also keep the key itself as a lookup alias
+  sessionKeys.set(key, key);
+  lastKnownSessionKey = key;
+}
+function resolveSessionKey(ctx: any): string {
+  const id =
+    ctx.sessionKey ??
+    ctx.context?.sessionKey ??
+    ctx.metadata?.sessionKey ??
+    ctx.session?.Key ??
+    ctx.session?.sessionkey ??
+    ctx.result?.sessionKey ??
+    ctx.message?.sessionkey;
+  if (id) {
+    const stored = sessionKeys.get(id);
+    if (stored) return stored;
+  }
+  const direct = pickSessionKey(ctx);
+  if (direct) return direct;
+  if (lastKnownSessionKey) return lastKnownSessionKey;
+  return "unknown";
+}
+export default function chatLoggerPlugin(api: any) {
+  api.on("session_start", (ctx: any) => {
+    cacheSessionKey(ctx);
+  });
+  api.on("message_received", (ctx: any) => {
+    cacheSessionKey(ctx);
+  });
+  api.on("agent_end", (ctx: any) => {
+    cacheSessionKey(ctx);
+    const messages: any[] = ctx.result?.messages ?? ctx.messages ?? [];
+    const sessionKey = resolveSessionKey(ctx);
+    const turns: { role: string; content: string }[] = [];
+    for (const msg of messages) {
+      if (msg.role === "user") {
+        const text = extractUserText(msg.content);
+        if (text) turns.push({ role: "user", content: text });
+      } else if (msg.role === "assistant" && isRealAssistant(msg)) {
+        turns.push({ role: "assistant", content: cleanAssistant(msg.content) });
+      }
+    }
+    const last = turns.slice(-2);
+    if (last.length === 0) return;
+    appendLog({
+      ts: new Date().toISOString(),
+      sessionKey,
+      messages: last,
+    });
+    if (sessionKey === "unknown") {
+      console.warn("[chat-logger] could not resolve sessionKey on agent_end");
+    }
+  });
+  api.on("gateway_start", () => {
+    sessionKeys.clear();
+    lastKnownSessionKey = "";
+  });
+}

+ 8 - 0
openclaw.plugin.json

@@ -0,0 +1,8 @@
+{
+  "id": "chat-logger",
+  "configSchema": {
+    "type": "object",
+    "additionalProperties": false,
+    "properties": {}
+  }
+}

+ 8 - 0
package.json

@@ -0,0 +1,8 @@
+{
+  "name": "chat-logger",
+  "version": "1.0.0",
+  "type": "module",
+  "openclaw": {
+    "extensions": ["./index.ts"]
+  }
+}