|
|
@@ -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 = "";
|
|
|
+ });
|
|
|
+}
|