index.ts 5.0 KB

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