index.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import fs from "fs";
  2. const LOG_FILE = "/tmp/openclaw-chat.log";
  3. const sessionKeys = new Map<string, string>();
  4. let lastKnownSessionKey = "";
  5. function appendLog(entry: Record<string, unknown>) {
  6. try {
  7. fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + "\n", "utf8");
  8. } catch {}
  9. }
  10. function extractText(content: any): string {
  11. if (typeof content === "string") return content;
  12. if (Array.isArray(content)) {
  13. return content
  14. .filter((b: any) => b.type === "text")
  15. .map((b: any) => b.text as string)
  16. .join("");
  17. }
  18. return "";
  19. }
  20. function extractUserText(content: any): string {
  21. const raw = extractText(content);
  22. const match = raw.match(/\[.*?\]\s*([\s\S]+)$/);
  23. return match ? match[1].trim() : raw.trim();
  24. }
  25. function cleanAssistant(content: any): string {
  26. return extractText(content)
  27. .replace(/^\[\[reply_to_current\]\]\s*/, "")
  28. .trim();
  29. }
  30. function isRealAssistant(msg: any): boolean {
  31. const text = cleanAssistant(msg.content);
  32. return !!text && !text.startsWith("Model reset");
  33. }
  34. function normalizeSessionKey(value: unknown): string | undefined {
  35. if (typeof value !== "string") return undefined;
  36. const v = value.trim();
  37. if (!v || v === "unknown") return undefined;
  38. return v;
  39. }
  40. function deepFindSessionKey(obj: any, maxDepth = 6): string | undefined {
  41. const seen = new Set<any>();
  42. function walk(node: any, depth: number): string | undefined {
  43. if (!node || depth > maxDepth) return undefined;
  44. if (typeof node !== "object") return undefined;
  45. if (seen.has(node)) return undefined;
  46. seen.add(node);
  47. for (const [k, v] of Object.entries(node)) {
  48. if (typeof v === "string") {
  49. const normalized = normalizeSessionKey(v);
  50. if (!normalized) continue;
  51. if (k.toLowerCase().includes("session") && normalized.includes(":")) {
  52. return normalized;
  53. }
  54. if (/^agent:[^:\s]+:[^:\s]+$/.test(normalized)) {
  55. return normalized;
  56. }
  57. }
  58. }
  59. for (const v of Object.values(node)) {
  60. const found = walk(v, depth + 1);
  61. if (found) return found;
  62. }
  63. return undefined;
  64. }
  65. return walk(obj, 0);
  66. }
  67. function pickSessionKey(ctx: any): string | undefined {
  68. const candidate =
  69. ctx.sessionKey ??
  70. ctx.result?.sessionKey ??
  71. ctx.context?.sessionKey ??
  72. ctx.metadata?.sessionKey ??
  73. ctx.message?.sessionKey ??
  74. ctx.session?.sessionKey ??
  75. ctx.session?.key ??
  76. ctx.user?.sessionKey;
  77. const direct = normalizeSessionKey(candidate);
  78. if (direct) return direct;
  79. return deepFindSessionKey(ctx);
  80. }
  81. function cacheSessionKey(ctx: any) {
  82. const key = pickSessionKey(ctx);
  83. if (!key) return;
  84. const ids = [
  85. ctx.sessionId,
  86. ctx.context?.sessionId,
  87. ctx.metadata?.sessionId,
  88. ctx.session?.id,
  89. ctx.session?.sessionId,
  90. ctx.result?.sessionId,
  91. ctx.message?.sessionId,
  92. ].filter((v) => typeof v === "string" && !!v);
  93. for (const id of ids) sessionKeys.set(id, key);
  94. // also keep the key itself as a lookup alias
  95. sessionKeys.set(key, key);
  96. lastKnownSessionKey = key;
  97. }
  98. function resolveSessionKey(ctx: any): string {
  99. const id =
  100. ctx.sessionKey ??
  101. ctx.context?.sessionKey ??
  102. ctx.metadata?.sessionKey ??
  103. ctx.session?.Key ??
  104. ctx.session?.sessionkey ??
  105. ctx.result?.sessionKey ??
  106. ctx.message?.sessionkey;
  107. if (id) {
  108. const stored = sessionKeys.get(id);
  109. if (stored) return stored;
  110. }
  111. const direct = pickSessionKey(ctx);
  112. if (direct) return direct;
  113. if (lastKnownSessionKey) return lastKnownSessionKey;
  114. return "unknown";
  115. }
  116. export default function chatLoggerPlugin(api: any) {
  117. api.on("session_start", (ctx: any) => {
  118. cacheSessionKey(ctx);
  119. });
  120. api.on("message_received", (ctx: any) => {
  121. cacheSessionKey(ctx);
  122. });
  123. api.on("agent_end", (ctx: any) => {
  124. cacheSessionKey(ctx);
  125. const messages: any[] = ctx.result?.messages ?? ctx.messages ?? [];
  126. const sessionKey = resolveSessionKey(ctx);
  127. const turns: { role: string; content: string }[] = [];
  128. for (const msg of messages) {
  129. if (msg.role === "user") {
  130. const text = extractUserText(msg.content);
  131. if (text) turns.push({ role: "user", content: text });
  132. } else if (msg.role === "assistant" && isRealAssistant(msg)) {
  133. turns.push({ role: "assistant", content: cleanAssistant(msg.content) });
  134. }
  135. }
  136. const last = turns.slice(-2);
  137. if (last.length === 0) return;
  138. appendLog({
  139. ts: new Date().toISOString(),
  140. sessionKey,
  141. messages: last,
  142. });
  143. if (sessionKey === "unknown") {
  144. console.warn("[chat-logger] could not resolve sessionKey on agent_end");
  145. }
  146. });
  147. api.on("gateway_start", () => {
  148. sessionKeys.clear();
  149. lastKnownSessionKey = "";
  150. });
  151. }